
equals() method: Đừng để bị lừa bởi phép 'so sánh' hời hợt!
Chào các chiến thần code tương lai, anh Creyt đây! Hôm nay chúng ta sẽ mổ xẻ một khái niệm mà nhiều bạn trẻ mới vào nghề hay mắc lỗi, thậm chí cả dân lão làng đôi khi cũng quên béng: chính là thằng equals() method trong Java. Nghe có vẻ đơn giản, nhưng nếu không hiểu rõ, nó có thể biến project của em thành một mớ bòng bong không lối thoát đấy!
1. equals() là gì và tại sao chúng ta cần nó?
Để anh Creyt kể em nghe chuyện này. Tưởng tượng em có hai cái điện thoại iPhone 15 Pro Max. Cả hai đều màu Xanh Titan, đều 256GB, đều mới toanh từ hộp. Nếu anh hỏi em: "Hai cái điện thoại này có giống nhau không?", em sẽ trả lời là "Giống chứ anh! Y chang nhau!" đúng không?
Nhưng trong thế giới của máy tính, đặc biệt là Java, câu trả lời không đơn giản vậy đâu.
-
Toán tử
==(Hai dấu bằng): Thằng này giống như em hỏi: "Hai cái điện thoại này có phải LÀ MỘT CÁI điện thoại duy nhất không?". Trong Java, với các đối tượng (object),==dùng để so sánh địa chỉ bộ nhớ. Nó chỉ trả vềtruekhi cả hai biến tham chiếu đến cùng một đối tượng trong RAM. Giống như em cầm hai cái iPhone, dù chúng y chang nhau, nhưng chúng vẫn là HAI CÁI ĐIỆN THOẠI VẬT LÝ khác nhau. Địa chỉ nhà của chúng khác nhau. -
Method
equals(): Còn thằng này mới là "đúng bài" khi em muốn hỏi: "Hai cái điện thoại này có CÙNG NỘI DUNG, CÙNG GIÁ TRỊ không?". Tức là, chúng có cùng màu, cùng dung lượng, cùng model không?equals()được thiết kế để so sánh giá trị nội dung của hai đối tượng. Nó cho phép em định nghĩa "giống nhau" có nghĩa là gì đối với các đối tượng của em.
Tóm lại:
==: So sánh địa chỉ (identity) - "Có phải cùng một thực thể không?"equals(): So sánh nội dung (equality) - "Có cùng giá trị không?"
Quan trọng: Mặc định, method equals() của lớp Object (lớp cha của mọi class trong Java) cũng chỉ làm y chang thằng ==! Tức là nó cũng so sánh địa chỉ bộ nhớ. Vì vậy, nếu em muốn so sánh nội dung cho các đối tượng của mình, em BẮT BUỘC phải override (ghi đè) method equals()!
2. Code Ví Dụ Minh Hoạ Rõ Ràng
Giả sử chúng ta có một class SinhVien đơn giản:
class SinhVien {
private String maSV;
private String ten;
private int tuoi;
public SinhVien(String maSV, String ten, int tuoi) {
this.maSV = maSV;
this.ten = ten;
this.tuoi = tuoi;
}
// Getters và Setters (để đơn giản, bỏ qua trong ví dụ này)
public String getMaSV() {
return maSV;
}
public String getTen() {
return ten;
}
public int getTuoi() {
return tuoi;
}
@Override
public String toString() {
return "SinhVien{maSV='" + maSV + "', ten='" + ten + "', tuoi=" + tuoi + "}";
}
}
public class EqualsDemo {
public static void main(String[] args) {
SinhVien sv1 = new SinhVien("SV001", "Nguyen Van A", 20);
SinhVien sv2 = new SinhVien("SV001", "Nguyen Van A", 20);
SinhVien sv3 = sv1;
SinhVien sv4 = new SinhVien("SV002", "Tran Thi B", 21);
System.out.println("=== So sánh với == (Địa chỉ bộ nhớ) ===");
System.out.println("sv1 == sv2: " + (sv1 == sv2)); // false (hai đối tượng khác nhau)
System.out.println("sv1 == sv3: " + (sv1 == sv3)); // true (cùng tham chiếu)
System.out.println("\n=== So sánh với equals() mặc định của Object ===");
System.out.println("sv1.equals(sv2): " + (sv1.equals(sv2))); // false (mặc định cũng so sánh địa chỉ)
System.out.println("sv1.equals(sv3): " + (sv1.equals(sv3))); // true (cùng tham chiếu)
}
}
Kết quả ở trên cho thấy sv1.equals(sv2) vẫn là false mặc dù sv1 và sv2 có nội dung y hệt nhau. Đó là vì chúng ta chưa override equals()!
Ghi đè equals() (Overriding equals())
Đây là cách chúng ta sẽ override equals() để so sánh theo nội dung:
import java.util.Objects;
class SinhVien {
private String maSV;
private String ten;
private int tuoi;
public SinhVien(String maSV, String ten, int tuoi) {
this.maSV = maSV;
this.ten = ten;
this.tuoi = tuoi;
}
public String getMaSV() {
return maSV;
}
public String getTen() {
return ten;
}
public int getTuoi() {
return tuoi;
}
@Override
public String toString() {
return "SinhVien{maSV='" + maSV + "', ten='" + ten + "', tuoi=" + tuoi + "}";
}
@Override
public boolean equals(Object o) {
// 1. Tối ưu: Nếu là cùng một đối tượng trong bộ nhớ, chắc chắn là bằng nhau.
if (this == o) return true;
// 2. Kiểm tra null: Nếu đối tượng truyền vào là null, chắc chắn không bằng.
if (o == null) return false;
// 3. Kiểm tra kiểu: Đảm bảo cùng loại class. (Hoặc dùng instanceof cho linh hoạt hơn tùy trường hợp)
if (getClass() != o.getClass()) return false;
// 4. Ép kiểu: Bây giờ ta biết chắc chắn 'o' là một SinhVien, nên ép kiểu an toàn.
SinhVien sinhVien = (SinhVien) o;
// 5. So sánh các trường quan trọng để định nghĩa "bằng nhau".
// Ở đây, ta coi hai sinh viên là bằng nhau nếu có cùng mã số sinh viên.
// Dùng Objects.equals() để xử lý trường hợp các trường có thể null an toàn.
return Objects.equals(maSV, sinhVien.maSV) &&
Objects.equals(ten, sinhVien.ten) && // Có thể thêm các trường khác nếu muốn tiêu chí chặt chẽ hơn
tuoi == sinhVien.tuoi;
}
// QUAN TRỌNG: LUÔN GHI ĐÈ hashCode() KHI GHI ĐÈ equals()!
// Nếu hai đối tượng bằng nhau (equals() trả về true) thì hashCode() của chúng phải như nhau.
@Override
public int hashCode() {
return Objects.hash(maSV, ten, tuoi);
}
}
public class EqualsDemoUpdated {
public static void main(String[] args) {
SinhVien sv1 = new SinhVien("SV001", "Nguyen Van A", 20);
SinhVien sv2 = new SinhVien("SV001", "Nguyen Van A", 20);
SinhVien sv3 = sv1;
SinhVien sv4 = new SinhVien("SV002", "Tran Thi B", 21);
System.out.println("=== So sánh với equals() SAU KHI GHI ĐÈ ===");
System.out.println("sv1.equals(sv2): " + (sv1.equals(sv2))); // TRUE! (Vì maSV giống nhau)
System.out.println("sv1.equals(sv3): " + (sv1.equals(sv3))); // TRUE
System.out.println("sv1.equals(sv4): " + (sv1.equals(sv4))); // FALSE
System.out.println("\n=== Kiểm tra hashCode() ===");
System.out.println("hashCode của sv1: " + sv1.hashCode());
System.out.println("hashCode của sv2: " + sv2.hashCode());
System.out.println("hashCode của sv4: " + sv4.hashCode());
// sv1 và sv2 có hashCode giống nhau vì chúng equals nhau.
}
}
Giờ thì sv1.equals(sv2) đã trả về true rồi đấy! Thấy chưa, chỉ cần định nghĩa lại "giống nhau" là mọi thứ khác hẳn.

3. Mẹo (Best Practices) từ Giảng viên Creyt
Giờ thì anh Creyt sẽ "rút ruột rút gan" mấy cái kinh nghiệm xương máu cho em:
- Luôn luôn override
hashCode()khi overrideequals()! Đây là quy tắc vàng, là "hợp đồng" của lớpObject. Nếu hai đối tượngequals()nhau, thìhashCode()của chúng phải giống nhau. Nếu không, em sẽ gặp những bug cực kỳ khó hiểu khi dùng cácCollectionnhưHashMap,HashSet(ví dụ: thêm một đối tượng vàoHashSetrồi không tìm thấy nó nữa, dù nó có đó!). Cứ tưởng tượng hai cái iPhone y chang nhau mà mỗi cái lại có một số IMEI khác nhau thì sao dùng được? - Sử dụng
Objects.equals()vàObjects.hash(): Từ Java 7 trở đi, classjava.util.Objectscung cấp các method tĩnh tiện lợi để so sánh các trường (bao gồm cảnull) và tạohashCodemột cách an toàn và ngắn gọn. Cứ dùng đi, khỏi phải loNullPointerExceptionhay viết code dài dòng. - Sử dụng IDE để tạo tự động: Các IDE "xịn xò" như IntelliJ IDEA hay Eclipse đều có chức năng tự động sinh code cho
equals()vàhashCode(). Hãy dùng chúng! Sau đó đọc và hiểu code mà nó sinh ra. Đừng cố gắng viết tay từ đầu, tốn thời gian mà dễ sai. - Quy tắc đối xứng (Symmetric), bắc cầu (Transitive), nhất quán (Consistent), phản xạ (Reflexive): Đây là "hợp đồng" của
equals()mà em cần nhớ (dù không cần viết code cho nó). Hiểu nôm na:x.equals(x)luôntrue(Phản xạ).- Nếu
x.equals(y)làtrue, thìy.equals(x)cũng phảitrue(Đối xứng). - Nếu
x.equals(y)làtruevày.equals(z)làtrue, thìx.equals(z)cũng phảitrue(Bắc cầu). x.equals(y)luôn cho cùng một kết quả nếu không có trường nào dùng để so sánh bị thay đổi (Nhất quán).x.equals(null)luônfalse.
- Cẩn thận với trường mutable (có thể thay đổi): Nếu em dùng các trường có thể thay đổi giá trị làm tiêu chí so sánh trong
equals(), thì trạng thái "bằng nhau" của đối tượng cũng có thể thay đổi theo thời gian. Điều này cực kỳ nguy hiểm nếu đối tượng đó đang nằm trongHashSethoặcHashMap.
4. Ứng dụng thực tế: Ai đã dùng equals()?
Thực ra, equals() được dùng "ngầm" khắp nơi trong các ứng dụng Java mà em không hề hay biết:
- Các Collection Framework: Đây là nơi
equals()toả sáng nhất.HashMapvàHashSet: DùnghashCode()để tìm "vị trí" tiềm năng của đối tượng, sau đó dùngequals()để xác nhận xem đối tượng đó có thực sự tồn tại ở đó không. Nếu em không override đúng,HashMapsẽ trả vềnulldù key đã có,HashSetsẽ thêm trùng lặp.ArrayList.contains(): Kiểm tra xem một phần tử có tồn tại trong danh sách không.List.indexOf(): Tìm vị trí của một phần tử.
- Database ORMs (JPA/Hibernate): Khi em làm việc với các framework này, chúng thường dùng
equals()để xác định xem hai đối tượng entity có đại diện cho cùng một bản ghi trong database không. - Testing Frameworks (JUnit, Mockito): Các hàm
assertEquals()trong JUnit dùngequals()để so sánh hai đối tượng. - Xử lý dữ liệu trùng lặp: Khi cần loại bỏ các bản ghi trùng lặp trong một tập dữ liệu lớn,
equals()là "vũ khí" tối thượng để xác định các bản ghi giống nhau về mặt nội dung.
5. Thử nghiệm đã từng và hướng dẫn nên dùng cho case nào
Anh Creyt đã từng "ăn hành" không ít lần vì quên override hashCode() khi override equals(). Hồi đó, debug cả ngày trời trong một ứng dụng lớn, cứ cho object vào HashMap rồi lấy ra thì null, cứ tưởng lỗi logic, hóa ra là do cái hashCode() "vô tri" kia kìa. Bài học là: KHÔNG BAO GIỜ TÁCH RỜI equals() và hashCode()!
Vậy khi nào em NÊN dùng equals() (và override nó)?
- Khi em cần so sánh nội dung của hai đối tượng: Đây là lý do chính. Ví dụ: hai đối tượng
Usercó cùngusernamevàemailthì coi là một, dù chúng được tạo ra ở hai thời điểm khác nhau. - Khi em làm việc với các
Collectiondựa trênhash(nhưHashMap,HashSet): Đây là bắt buộc nếu em muốn cácCollectionnày hoạt động đúng như mong đợi. - Khi em tạo các "Value Object": Ví dụ như
Point(x, y),Money(số tiền, loại tiền tệ). Các đối tượng này được định nghĩa hoàn toàn bởi giá trị của chúng, không phải bởi định danh duy nhất trong bộ nhớ. - Khi em cần kiểm tra sự tồn tại hoặc trùng lặp của đối tượng trong một danh sách/tập hợp: Ví dụ: kiểm tra xem một
SinhVienđã có trongdanhSachSinhVienchưa.
Khi nào KHÔNG NÊN override equals()?
- Khi mỗi đối tượng chỉ có một định danh duy nhất: Ví dụ, các entity trong database thường có một ID duy nhất. So sánh bằng ID là đủ, và thường thì
==(so sánh tham chiếu) hoặc so sánh ID trực tiếp đã đáp ứng. Overrideequals()có thể gây nhầm lẫn hoặc phức tạp không cần thiết. - Khi performance là cực kỳ quan trọng và em không cần so sánh nội dung: Việc so sánh nhiều trường có thể tốn tài nguyên hơn so với việc chỉ so sánh địa chỉ bộ nhớ.
Nhớ nhé các Gen Z! equals() không phải là một method "có cũng được, không có cũng không sao". Nó là một công cụ cực kỳ mạnh mẽ để em định nghĩa ý nghĩa của sự "giống nhau" trong thế giới lập trình hướng đối tượng. Nắm vững nó, em sẽ tránh được vô vàn lỗi "tưởng dễ mà khó" đấy!
Thuộc Series: Java – OOP
Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!