`toString`, `equals`, `hashCode` — ba method phải override đúng
Override Object.toString để debug dễ, equals/hashCode đúng contract Java, Objects.equals & Objects.hash, và vì sao HashMap hỏng khi bạn override equals mà quên hashCode.
Mọi class trong Java ngầm kế thừa từ java.lang.Object. Object có 3 method nổi tiếng mà bạn sẽ override sớm hay muộn: toString(), equals(), hashCode(). Không override → class bạn viết hành xử khác mong đợi: in ra User@7a3c2d, không so sánh được nội dung, bỏ vào HashSet thành 2 entry trùng.
Bài này giải thích 3 method, contract Java quy định, vì sao override equals mà quên hashCode là bug rất phổ biến, và dùng Objects.equals/hash để viết nhanh và đúng.
1. Analogy — thẻ căn cước
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 "đây có phải cùng một người?" (equals), hệ thống so số căn cước — duy nhất. Và để 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 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 = debug print. equals = so nội dung. hashCode = index để hash table dùng. Hai cái sau đi đôi — không ai chỉ override một.
2. toString — chuỗi mô tả object
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.
3. equals — so nội dung
Default Object.equals() = this == other — so địa chỉ, gần như luôn false với 2 object khác nhau dù nội dung giống.
Book b1 = new Book("Java", 2018);
Book b2 = new Book("Java", 2018);
System.out.println(b1.equals(b2)); // false — default equals
Override để so nội dung:
@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);
}
Rule (contract) JLS quy định equals phải thoả:
- Reflexive:
x.equals(x) == true. - Symmetric:
x.equals(y) == trueiffy.equals(x) == true. - Transitive:
x.equals(y) && y.equals(z)→x.equals(z) == true. - Consistent: gọi nhiều lần cùng kết quả (với object không đổi).
- Non-null:
x.equals(null) == false.
Viết sai contract → HashSet, HashMap, List.contains, Arrays.equals hành xử ngẫu nhiên. Contract này không phải "best practice" — là yêu cầu của spec.
3.1 Viết equals đúng — dùng Objects.equals
Objects.equals(a, b) xử lý null 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);
}
Pattern này:
this == o— fast path cho cùng object.instanceof Type var(Java 16+ pattern matching) — kiểm kiểu + bind trong 1 dòng.Objects.equals— null-safe so sánh.
4. hashCode — PHẢI override cùng với equals
Contract: 2 object equal phải có cùng hashCode.
x.equals(y) == true IMPLIES x.hashCode() == y.hashCode()
(Ngược lại không đảm bảo — 2 object cùng hash không nhất thiết equal; đó là collision bình thường.)
Default Object.hashCode() dựa vào địa chỉ — 2 object có nội dung giống vẫn hash khác. Override equals mà quên hashCode → vi phạm contract → bug tinh vi:
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);
}
// KHONG override hashCode
}
Set<Book> s = new HashSet<>();
s.add(new Book("Java"));
System.out.println(s.contains(new Book("Java"))); // false!
HashSet.contains hash object mới → lookup bucket đó → không tìm thấy vì new Book("Java") cho hash khác với book đã add. Tưởng bug ngẫu nhiên, thực chất vi phạm contract.
Điều đáng nhìn kỹ là thứ tự: hashCode chạy trước và chọn bucket, equals chỉ được gọi cho những phần tử nằm trong đúng bucket đó — nên equals viết chuẩn đến mấy cũng không có cơ hội chạy.

4.1 Viết hashCode đúng — dùng Objects.hash
@Override
public int hashCode() {
return Objects.hash(title, author, year);
}
Objects.hash(...) kết hợp các field bằng công thức chuẩn, ổn định, null-safe. Chỉ cần truyền đúng bộ field giống với equals dùng.
Nếu equals so 3 field title/author/year, hashCode cũng phải hash 3 field đó. Khác bộ → contract vỡ.
// SAI
public boolean equals(Object o) { ...so 3 field... }
public int hashCode() { return title.hashCode(); } // chi 1 field -> van dung contract
// Nhung neu nguoc lai:
public boolean equals(Object o) { ...so 2 field title, author... }
public int hashCode() { return Objects.hash(title, author, year); }
// Hai book khac year nhung cung title+author -> equals = true, hashCode khac -> BUG
5. IDE generate — dùng nhưng hiểu
Mọi IDE (IntelliJ, Eclipse, VSCode) có "Generate → equals and hashCode". Chọn các field cần so, IDE sinh code chuẩn. Dùng thoải mái, nhưng hiểu code sinh ra để debug khi cần.
Output IDE IntelliJ:
@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);
}
Một khác biệt so với ví dụ trên: getClass() != o.getClass() thay vì instanceof. Lý do liên quan đến kế thừa và symmetric — instanceof có thể vi phạm symmetric với subclass. getClass cứng hơn. Chi tiết bàn ở module kế thừa.
6. Record — Java 16+ tự sinh đủ 3 method
public record Book(String title, String author, int year) { }
Record tự sinh:
- Constructor canonical
Book(String, String, int). - Accessor
title(),author(),year()(không phảigetTitle). equals,hashCode,toStringchuẩn, dựa trên mọi component.
Book b1 = new Book("Java", "Bloch", 2018);
Book b2 = new Book("Java", "Bloch", 2018);
System.out.println(b1); // Book[title=Java, author=Bloch, year=2018]
System.out.println(b1.equals(b2)); // true
System.out.println(b1.hashCode() == b2.hashCode()); // true
Bài sau sẽ đào sâu record. Hiện tại nhớ: nếu class là data holder thuần (immutable, so content), record thay được cả 4 method và constructor.
7. Khi nào KHÔNG override equals/hashCode?
- Identity-based object — mỗi instance là duy nhất về bản chất, không so nội dung. Vd
Thread,ServerSocket,EventLoop. Default = địa chỉ là đúng. - Entity có ID — 2 entity cùng
idbằng nhau. Override nhưng chỉ dựa vàoid, không so field khác. (Đặc biệt quan trọng với JPA entity — contract riêng).
8. Pitfall tổng hợp
❌ Nhầm 1: Override equals quên hashCode.
✅ Luôn override cặp. IDE "Generate equals and hashCode" sinh luôn cả 2.
❌ Nhầm 2: equals parameter type sai.
public boolean equals(Book other) { ... } // overload, KHONG override Object.equals
✅ public boolean equals(Object o) — parameter phải Object. Luôn dùng @Override — compiler bắt sai signature.
❌ Nhầm 3: Dùng == cho String / wrapper trong equals.
return title == other.title; // so dia chi — vo voi String tao bang new
✅ Objects.equals(title, other.title).
❌ Nhầm 4: hashCode trả constant (vd return 1;).
public int hashCode() { return 1; } // dung contract nhung HashMap O(n) — kham kho
✅ Tuân contract không đủ — phải phân bố đều. Dùng Objects.hash(fields).
❌ Nhầm 5: toString include field nhạy cảm.
public String toString() { return "User{email=" + email + ", password=" + password + "}"; }
✅ Loại field như password, token, credit card khỏi toString. Log là nơi thường xuyên dump object.
9. 📚 Deep Dive Oracle
Spec / reference chính thức:
- Object.equals — javadoc — contract đầy đủ.
- Object.hashCode — javadoc — rule "equal objects must have equal hash codes".
- Object.toString — javadoc — recommendation for all subclasses.
- Objects class — helper
equals,hash,toString,requireNonNull. - JEP 395 — Records — record final trong Java 16, tự sinh đúng 3 method.
- Effective Java (Joshua Bloch), Items 10–12: "Obey the general contract when overriding equals", "Always override hashCode when you override equals", "Always override toString".
Ghi chú: Item 10 của Effective Java là bản spec "giải thích đời" cho equals contract — đọc được 20 phút và tiết kiệm cả năm debug. Bloch thiết kế cả Objects.hash và record trong JDK, nên hiểu sâu nhất.
10. Tóm tắt
Objectcó 3 method thường override:toString,equals,hashCode.- Default
toStringtrảClassName@hex— vô nghĩa; override để debug dễ. - Default
equalsso địa chỉ; override để so nội dung. equalsphải tuân 5 rule: reflexive, symmetric, transitive, consistent, non-null.- Override
equals→ bắt buộc overridehashCodevới cùng bộ field. Vi phạm →HashSet/HashMaphành xử sai. - Dùng
Objects.equals(a, b)vàObjects.hash(fields...)để viết nhanh và đúng. @Overrideannotation bắt lỗi nếu signature sai (đặc biệtequalshay bị overload).- Record (Java 16+) tự sinh đủ 3 method cho class data-holder — giảm boilerplate.
- Loại field nhạy cảm (password, token) khỏi
toString.
11. Tự kiểm tra
- 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 đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên