Bộ ba toString, equals và hashCode: ghi đè chuẩn
Nắm vững contract của toString, equals và hashCode trong Java. Sử dụng Objects.equals, Objects.hash và hiểu vì sao HashMap lỗi khi thiếu hashCode.
TL;DR: Ba phương thức kế thừa từ Object quyết định cách biểu diễn và so sánh đối tượng: toString() phục vụ in ấn/debug, equals() so sánh giá trị logic thay vì địa chỉ ô nhớ, còn hashCode() tạo mã băm cho hash table. Quy tắc bất biến: hai đối tượng bằng nhau qua equals bắt buộc phải có cùng hashCode. Nếu ghi đè equals mà quên hashCode, HashMap và HashSet sẽ không thể tìm thấy phần tử. Nhớ lại bài Class và object, == so sánh địa chỉ ô nhớ, và bài Encapsulation nhấn mạnh việc che giấu dữ liệu.
Mọi class trong Java ngầm kế thừa từ java.lang.Object. Lớp cha Object có 3 method nổi tiếng mà bạn sẽ cần ghi đè sớm hay muộn: toString(), equals() và hashCode(). Nếu không override, class sẽ hành xử ngoài dự kiến: in ra chuỗi vô nghĩa User@7a3c2d, không so sánh được nội dung thực tế, hoặc làm hỏng cấu trúc dữ liệu khi đưa vào HashSet hay HashMap.
1. Thẻ căn cước công dân phản ánh vai trò của bộ ba method ra sao?
Bạn có 2 người tên "Nguyễn Văn A" sinh cùng ngày. Hai thẻ căn cước của họ mô tả (toString) nhìn giống nhau đến 90%. Nhưng để xác định xem có phải cùng một người hay không (equals), hệ thống so sánh số căn cước duy nhất. Để tra cứu nhanh trong cơ sở dữ liệu (hashCode), hệ thống băm số căn cước thành một chỉ mục (index).
| Đời thường | Java |
|---|---|
| Thông tin in trên thẻ để đọc | toString() |
| So số căn cước để biết cùng người | equals() |
| Băm để tra cứu nhanh trong file | hashCode() |
toString dùng để debug/log. equals so sánh nội dung logic. hashCode tạo index cho hash table. Hai phương thức sau luôn đi đôi với nhau, không bao giờ chỉ ghi đè một phương thức đơn lẻ.
2. Phương thức toString giúp ích gì cho quá trình gỡ lỗi?
Default Object.toString() trả ClassName@hexHashCode:
public class Book { String title; int year; }
System.out.println(new Book()); // Book@1540e19d — vo nghia
Override để dễ debug, đẹp log:
public class Book {
String title;
int year;
@Override
public String toString() {
return "Book{title='" + title + "', year=" + year + "}";
}
}
System.out.println(new Book()); // Book{title='null', year=0}
@Override annotation — không bắt buộc nhưng nên có: nếu bạn gõ sai tên (vd toStirng), compiler bắt ngay. Không @Override → Java hiểu bạn khai method mới, không phải override → bug im lặng.
2.1 Trình bày
Convention phổ biến:
ClassName{field1=value1, field2=value2, ...}
Hoặc dùng String.format / text block:
@Override
public String toString() {
return String.format("Book{title='%s', year=%d}", title, year);
}
Với class nhiều field, cân nhắc library helper như StringBuilder hoặc annotation (Lombok @ToString) — dù module này không nhập library ngoài.
2.2 Khi toString được gọi ngầm
System.out.println(obj)→ gọiobj.toString().- String concat:
"name=" + obj→ gọiobj.toString(). - Debugger / IDE hiển thị object.
- Logger:
log.info("processing {}", order)— tuỳ logger implementation gọi ngầm.
Kết quả: toString tốt làm mọi bug dễ debug. Viết tốn 3 phút, tiết kiệm 3 giờ săn bug.
Thử ngẫmmột vòng lặp log hàng chục nghìn object mỗi giây bằng log.info("{}", obj) — toString() bạn viết có làm gì tốn kém (query DB, loop lồng nhau) mà bạn quên mất nó chạy ngầm mỗi lần log không?
3. Phương thức equals định nghĩa quan hệ bằng nhau theo contract nào?
Mặc định, Object.equals() chỉ so sánh địa chỉ (this == other), gần như luôn trả về false với 2 object khác nhau dù cùng nội dung.
Book b1 = new Book("Java", 2018);
Book b2 = new Book("Java", 2018);
System.out.println(b1.equals(b2)); // false — default equals
Ghi đè để so sánh nội dung logic:
@Override
public boolean equals(Object o) {
if (this == o) return true; // cung object
if (!(o instanceof Book other)) return false; // kieu khac hoac null
return year == other.year && Objects.equals(title, other.title);
}
Hợp đồng (contract) của equals trong Java bắt buộc phải thỏa mãn 5 điều kiện:
- Reflexive:
x.equals(x) == true. - Symmetric:
x.equals(y) == truekhi và chỉ khiy.equals(x) == true. - Transitive:
x.equals(y)vày.equals(z)thì suy rax.equals(z) == true. - Consistent: gọi nhiều lần cho cùng một kết quả nếu trạng thái không đổi.
- Non-null:
x.equals(null) == false.
Vi phạm hợp đồng này sẽ khiến HashSet, HashMap hoặc List.contains chạy sai lệch.
3.1 Viết equals an toàn với Objects.equals
Objects.equals(a, b) xử lý trường hợp null một cách an toàn:
import java.util.Objects;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Book other)) return false;
return year == other.year
&& Objects.equals(title, other.title)
&& Objects.equals(author, other.author);
}
4. Vì sao bắt buộc phải ghi đè hashCode đồng thời với equals?
Quy tắc bất biến: Hai object bằng nhau (equals) bắt buộc phải có cùng hashCode:
x.equals(y) == true => x.hashCode() == y.hashCode()
Mặc định, Object.hashCode() dựa vào địa chỉ ô nhớ. Nếu bạn override equals mà quên hashCode, hai đối tượng có cùng nội dung sẽ sinh ra hai mã băm khác nhau:
class Book {
String title;
public Book(String title) { this.title = title; }
@Override
public boolean equals(Object o) {
return o instanceof Book b && Objects.equals(title, b.title);
}
// QUEN override hashCode
}
Set<Book> s = new HashSet<>();
s.add(new Book("Java"));
System.out.println(s.contains(new Book("Java"))); // false!
HashSet.contains tính mã băm trước để tìm bucket lưu trữ. Do mã băm lệch nhau, tìm kiếm rơi vào bucket rỗng nên trả về false mà không hề gọi tới equals.

4.1 Tạo mã băm chuẩn bằng Objects.hash
@Override
public int hashCode() {
return Objects.hash(title, author, year);
}
Nếu equals so sánh 3 field title, author, year thì hashCode cũng bắt buộc phải băm chính xác 3 field đó. Lệch trường so sánh sẽ phá vỡ toàn bộ hợp đồng của Hash Table.
5. IDE tự động sinh equals và hashCode theo cơ chế nào?
Các IDE hiện đại đều hỗ trợ tính năng "Generate equals and hashCode":
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return year == book.year && Objects.equals(title, book.title);
}
@Override
public int hashCode() {
return Objects.hash(title, year);
}
Kiểm tra getClass() != o.getClass() nghiêm ngặt hơn instanceof, bảo đảm tính đối xứng tuyệt đối khi có các lớp con kế thừa.
6. Record trong Java tự sinh ba phương thức này ra sao?
Từ Java 16, record tự động sinh trọn gói constructor canonical, accessor, cùng với bộ ba equals, hashCode, toString:
public record Book(String title, String author, int year) { }
Book b1 = new Book("Java", "Bloch", 2018);
Book b2 = new Book("Java", "Bloch", 2018);
System.out.println(b1.equals(b2)); // true
System.out.println(b1.hashCode() == b2.hashCode()); // true
7. Những trường hợp nào không nên ghi đè equals và hashCode?
- Đối tượng định danh (Identity-based): Mỗi instance mang bản sắc riêng độc nhất (như
Thread,Socket,Connection), so sánh theo địa chỉ ô nhớ là hoàn toàn chính xác. - Entity có khóa chính (ID): Trong JPA/Hibernate, các entity thường chỉ so sánh bằng duy nhất trường
id, không so sánh toàn bộ các trường dữ liệu khác.
8. Những cạm bẫy kinh điển khi override bộ ba method là gì?
❌ Nhầm 1: Override equals nhưng quên hashCode.
✅ Luôn ghi đè theo cặp hoặc để IDE tự động sinh code.
❌ Nhầm 2: Sai kiểu tham số trong equals.
public boolean equals(Book other) { ... } // DAY LA OVERLOAD, KHONG PHAI OVERRIDE
✅ Bắt buộc dùng public boolean equals(Object o) kèm chú thích @Override.
❌ Nhầm 3: Dùng == để so sánh chuỗi hoặc đối tượng trong equals.
return this.title == other.title;
✅ Luôn dùng Objects.equals(title, other.title).
❌ Nhầm 4: Trả về hằng số cố định trong hashCode (ví dụ return 1;).
✅ Làm hash table suy biến thành $O(n)$, hãy dùng Objects.hash(...) để phân bố đều.
❌ Nhầm 5: Để lộ mật khẩu hoặc token bí mật trong toString.
✅ Loại bỏ hoặc che mờ (mask) các trường nhạy cảm khi hiển thị log.
9. Oracle JLS quy định chi tiết về contract Object ra sao?
Spec / reference chính thức:
- Object.equals Javadoc: đặc tả chi tiết về hợp đồng tương đương.
- Object.hashCode Javadoc: quy tắc ánh xạ giữa equals và mã băm.
- Objects utility class: tiện ích hỗ trợ so sánh và băm an toàn.
- JEP 395: Records: cơ chế sinh mã tự động cho cấu trúc record.
10. Những điểm cốt lõi nào cần ghi nhớ về toString, equals và hashCode?
- Mặc định
toStringtrả về mã băm khó đọc: luôn ghi đè để hiển thị thông tin rõ ràng (nhớ loại bỏ mật khẩu, token nhạy cảm). - Phương thức
equalsmặc định so sánh địa chỉ ô nhớ: cần ghi đè bằngObjects.equalstrên các field logic để so sánh giá trị. - Bắt buộc ghi đè
hashCodecùng lúc vớiequalstrên cùng tập thuộc tính để bảo toàn tính đúng đắn choHashMapvàHashSet. - Tận dụng
Objects.hash(...)hoặc cú pháprecordtừ Java 16+ để loại bỏ mã lặp boilerplate một cách an toàn.
11. Tự kiểm tra mức độ nắm vững toString, equals và hashCode
- Q1Đoạn sau có 2 bug. Chỉ ra và sửa:
public class Book { String title; int year; public Book(String title, int year) { this.title = title; this.year = year; } public boolean equals(Book other) { return this.title == other.title && this.year == other.year; } } - Q2Đoạn sau in gì?
Set<Book> s = new HashSet<>(); s.add(new Book("Java", 2018)); System.out.println(s.contains(new Book("Java", 2018)));(Giả sử
Book.equalsđã override đúng nhưng KHÔNG overridehashCode.) - Q3Giải thích tại sao
return 1;tronghashCode"đúng contract" mà vẫn là bug nghiêm trọng. - Q4Đoạn sau có vấn đề security gì?
public class User { String username; String password; String apiToken; @Override public String toString() { return "User{username='" + username + "', password='" + password + "', apiToken='" + apiToken + "'}"; } } log.info("processing user {}", user); - Q5Viết
equals,hashCode,toStringcho class:public class Point { double x; double y; public Point(double x, double y) { this.x = x; this.y = y; } }
Bài tiếp theo: Record — data class với 1 dòng
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Tính đóng gói trong Java: private, public và protected
Bài tiếpRecord trong Java: lớp mang dữ liệu bất biến một dòng
Lộ trình Junior Java Backend Developer
Chặng 3/21 · OOP: class, kế thừa, đa hình, abstraction — bài 5/16 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này