equals() method: Khi nào 'giống' là 'giống' trong Java?
Java – OOP

equals() method: Khi nào 'giống' là 'giống' trong Java?

Author

Admin System

@root

Ngày xuất bản

21 Mar, 2026

Lượt xem

2 Lượt

equals() method

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ề true khi 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ù sv1sv2nộ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.

Illustration

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 override equals()! Đây là quy tắc vàng, là "hợp đồng" của lớp Object. Nếu hai đối tượng equals() 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ác Collection như HashMap, HashSet (ví dụ: thêm một đối tượng vào HashSet rồ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()Objects.hash(): Từ Java 7 trở đi, class java.util.Objects cung 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ạo hashCode một cách an toàn và ngắn gọn. Cứ dùng đi, khỏi phải lo NullPointerException hay 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()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ôn true (Phản xạ).
    • Nếu x.equals(y)true, thì y.equals(x) cũng phải true (Đối xứng).
    • Nếu x.equals(y)truey.equals(z)true, thì x.equals(z) cũng phải true (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ôn false.
  • 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 trong HashSet hoặc HashMap.

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.
    • HashMapHashSet: Dùng hashCode() để tìm "vị trí" tiềm năng của đối tượng, sau đó dùng equals() để 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, HashMap sẽ trả về null dù key đã có, HashSet sẽ 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ùng equals() để 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()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 User có cùng usernameemail thì 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 Collection dựa trên hash (như HashMap, HashSet): Đây là bắt buộc nếu em muốn các Collection nà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ó trong danhSachSinhVien chư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. Override equals() 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é!

#tech #cyberpunk #laravel
Chỉnh sửa bài viết

Bình luận (0)

Vui lòng Đăng Nhập để Bình luận

Hỗ trợ Markdown cơ bản
Nguyễn Văn A
1 ngày trước

Tính năng này đỉnh quá ad ơi, chờ mãi mới thấy một blog Tiếng Việt có UI/UX xịn như vầy!