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() |
💡 💡 Cách nhớ
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.
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.
⚠️ ⚠️ Phải dùng CÙNG bộ field cho equals và hashCode
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
ℹ️ 📚 Deep Dive Oracle (optional)
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;
}
}
▸
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;
}
}Bug 1 — signature sai: equals(Book other) nhận Book, không phải Object. Đây là overload, không override Object.equals. HashSet/HashMap vẫn dùng default equals(Object) → so địa chỉ → hành xử sai. Thiếu @Override annotation — nếu có, compiler sẽ bắt ngay.
Bug 2 — so String bằng ==: this.title == other.title so reference. Hai Book có cùng nội dung "Java" nhưng tạo từ new String("Java") sẽ cho địa chỉ khác.
Bonus bug 3: quên override hashCode — sẽ vi phạm contract nếu dùng trong HashSet/HashMap.
Fix:
@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);
}
@Override
public int hashCode() {
return Objects.hash(title, 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 override hashCode.)
▸
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 override hashCode.)
In false — hầu như chắc chắn.
HashSet.contains gọi hashCode() trước để tìm bucket, rồi mới dùng equals trong bucket đó. hashCode default (của Object) dựa vào địa chỉ → hai Book cùng nội dung có hash khác → fall vào bucket khác → contains không tìm thấy, return false.
Đây là bug class: vi phạm contract "equals → cùng hashCode". Fix: override hashCode dựa trên cùng bộ field với equals. Dùng Objects.hash(title, year).
Q3Giải thích tại sao return 1; trong hashCode "đúng contract" mà vẫn là bug nghiêm trọng.▸
return 1; trong hashCode "đúng contract" mà vẫn là bug nghiêm trọng.Contract chỉ yêu cầu: x.equals(y) => x.hashCode() == y.hashCode(). return 1 thoả — mọi object cùng hash nên kéo theo object equal cũng cùng hash.
Nhưng contract chỉ là correctness. HashMap / HashSet dựa vào phân bố đều của hash để đạt O(1) lookup. Nếu mọi object cùng hash 1 → tất cả rơi vào 1 bucket → lookup degenerate thành O(n) (linked list / tree trong bucket). 1 triệu entry → tra cứu 1 phần tử mất ~1 triệu phép so sánh equals.
Vì vậy: hash phải phân bố tốt theo nội dung. Dùng Objects.hash(fields) hoặc logic tương đương — xáo trộn bit đủ đều để hash map hoạt động hiệu quả.
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);
▸
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);toString include password và apiToken — mọi lệnh log object này sẽ dump credential vào log file. Log file thường:
- Lưu lâu dài, rotate nhưng archive.
- Được gửi lên log aggregator (Elastic, Splunk, Datadog).
- Truy cập được bởi nhiều người (dev, ops, SRE).
- Backup vào S3/Drive.
→ Credential rò rỉ cho bất kỳ ai có quyền đọc log. Incident thật đã xảy ra với nhiều công ty.
Fix: loại field nhạy cảm khỏi toString, hoặc mask:
public String toString() {
return "User{username='" + username + "', password='***', apiToken='" + mask(apiToken) + "'}";
}
private static String mask(String s) {
return s == null ? null : (s.length() <= 4 ? "****" : s.substring(0, 4) + "****");
}Rule chung: mọi field được đánh dấu sensitive (password, token, SSN, credit card) phải mask hoặc bỏ qua trong toString.
Q5Viết equals, hashCode, toString cho class:public class Point {
double x;
double y;
public Point(double x, double y) { this.x = x; this.y = y; }
}
▸
equals, hashCode, toString cho class:public class Point {
double x;
double y;
public Point(double x, double y) { this.x = x; this.y = y; }
}@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point other)) return false;
return Double.compare(x, other.x) == 0
&& Double.compare(y, other.y) == 0;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point{x=" + x + ", y=" + y + "}";
}Lưu ý Double.compare thay vì x == other.x: với double, NaN == NaN là false và -0.0 == +0.0 là true — hành vi khác contract equals. Double.compare xử lý đúng cả 2 case.
Hoặc ngắn hơn với record Java 16+:
public record Point(double x, double y) {}Record tự sinh 3 method (dùng Double.compare cho double component).
Bài tiếp theo: Record — data class với 1 dòng