Phương thức equals() và hashCode() trong java

equals hashCode trong java

Bài viết này giúp bạn hiểu khái niệm 2 phương thức quan trọng: Phương thức equals() và hashCode() trong Java.

Phương thức equals() và hashCode() trong java

Khi sử dụng các collection, Để nhận được các hành vi mong muốn, chúng ta nên ghi đè các phương thức equals() và hashCode() trong các lớp của các phần tử được thêm vào collection.

Lớp Object (lớp cha của tất cả các lớp trong Java) định nghĩa hai phương thức equal() và hashCode(). Điều đó có nghĩa là tất cả các lớp trong Java (bao gồm cả các lớp bạn đã tạo) thừa kế các phương thức này. Về cơ bản, lớp Object thực hiện các phương thức này cho mục đích chung.

Tuy nhiên, bạn sẽ phải ghi đè chúng một cách cụ thể cho các lớp có đối tượng được thêm vào các collection, đặc biệt là các collection dựa trên bảng băm như HashSet và HashMap.


Nội dung chính:

 

Phương thức equals() trong Java

Khi so sánh hai đối tượng với nhau, Java gọi phương thức equals() của chúng trả về true nếu hai đối tượng bằng nhau hoặc false nếu hai đối tượng là khác nhau. Lưu ý rằng phép so sánh sử dụng phương thức equals() so với sử dụng toán tử == là khác nhau.

Đây là sự khác biệt:

Phương thức equals() được thiết kế để so sánh hai đối tượng về mặt ngữ nghĩa (bằng cách so sánh các thành viên dữ liệu của lớp), trong khi toán tử == so sánh hai đối tượng về mặt kỹ thuật (bằng cách so sánh các tham chiếu của chúng, nghĩa là địa chỉ bộ nhớ).

LƯU Ý: Việc cài đặt phương thức equals() trong lớp Object so sánh các tham chiếu của hai đối tượng. Điều đó có nghĩa là bạn nên ghi đè nó trong các lớp của bạn để so sánh ngữ nghĩa. Hầu hết các lớp trong JDK ghi đè phương thức equals() của riêng chúng, chẳng hạn như String, Date, Integer, Double, v.v.

Ví dụ phương thức equals() của đối tượng String

Ví dụ điển hình so sánh chuỗi trong Java, để thấy sự khác nhau giữa phương thức equal() và toán tử ==.

package vn.viettuts;
public class EqualExample1 {
    public static void main(String[] args) {
        String s1 = new String("This is a string");
        String s2 = new String("This is a string");
        System.out.println("s1 == s2: " + (s1 == s2));
        System.out.println("s1.equals(s2): " + (s1.equals(s2)));
    }
}

Kết quả:

s1 == s2: false
s1.equals(s2): true

So sánh tham chiếu (toán tử ==) trả về false vì s1 và s2 là hai đối tượng khác nhau được lưu trữ ở các vị trí khác nhau trong bộ nhớ. Trong khi so sánh ngữ nghĩa trả về true bởi vì s1 và s2 có cùng giá trị (“THis is a string”) có thể được coi là bằng nhau về mặt ngữ nghĩa.

Ví dụ ghi đè phương thức equals()

Tương tự như vậy, giả sử chúng ta có lớp Student và cài đặt phương thức equal() như sau:

Trong thực tế, chúng ta có thể xem xét hai đối tượng Student có ngữ nghĩa tương đương nhau nếu chúng có cùng thuộc tính (id, name, email và age). Bây giờ, hãy xem cách ghi đè phương thức equals() trong lớp này để xác nhận rằng hai đối tượng Student có các thuộc tính giống nhau được coi là bằng nhau:

package vn.viettuts;
public class Student {
    private String id;
    private String name;
    private String email;
    private int age;
    public Student(String id, String name, String email, int age) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.age = age;
    }
    public String toString() {
        String studentInfo = "Student " + id;
        studentInfo += ": " + name;
        studentInfo += " - " + email;
        studentInfo += " - " + age;
        return studentInfo;
    }
    
    public boolean equals(Object obj) {
        if (obj instanceof Student) {
            Student another = (Student) obj;
            if (this.id.equals(another.id) &&
                this.name.equals(another.name) &&
                this.email.equals(another.email) &&
                this.age == another.age) {
                    return true;
            }
        }
        return false;
    }
}

Tạo ra lớp EqualStudent.java để kiểm tran phương thức equal() trong lớp Student.

package vn.viettuts;
public class EqualStudent {
    public static void main(String[] args) {
        Student student1 = new Student("123", "Cong", "cong@gmail.com", 22);
        Student student2 = new Student("123", "Cong", "cong@gmail.com", 22);
        Student student3 = new Student("456", "Dung", "dung@gmail.com", 18);
        System.out.println("student1 == student2: " + (student1 == student2));
        System.out.println("student1.equals(student2): "
                + (student1.equals(student2)));
        System.out.println("student2.equals(student3): "
                + (student2.equals(student3)));
    }
}

Kết quả:

student1 == student2: false
student1.equals(student2): true
student2.equals(student3): false

Ví dụ ghi đè phương thức equals() của phần tử của collection

Ta có lớp Student.java có nội dung như sau:

package vn.viettuts;
public class Student {
    private String id;
    private String name;
    private String email;
    private int age;
    public Student(String id) {
        this.id = id;
    }
    public Student(String id, String name, String email, int age) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.age = age;
    }
    public String toString() {
        String studentInfo = "Student " + id;
        studentInfo += ": " + name;
        studentInfo += " - " + email;
        studentInfo += " - " + age;
        return studentInfo;
    }
    public boolean equals(Object obj) {
        if (obj instanceof Student) {
            Student another = (Student) obj;
            if (this.id.equals(another.id)) {
                return true;
            }
        }
        return false;
    }
}

Ở đây, phương thức equals() này chỉ so sánh thuộc tính ID của hai đối tượng Student.

Chúng ta sẽ coi mỗi đối tượng Student có một ID duy nhất, và xem hai đối tượng sinh viên là bằng nhau nếu chúng có ID giống nhau.

Phương thức contains(Object) của interface List trong java có thể được sử dụng để kiểm tra nếu đối tượng được chỉ định tồn tại trong danh sách. Về bản chất, phương thức equal() được gọi bên trong phương thức contains(Object).

package vn.viettuts;
import java.util.ArrayList;
import java.util.List;
public class EqualStudent2 {
    public static void main(String[] args) {
        Student student1 = new Student("123", "Cong", "cong@gmail.com", 22);
        Student student2 = new Student("123", "Cong", "cong@gmail.com", 22);
        Student student3 = new Student("456", "Dung", "dung@gmail.com", 18);
        // tạo danh sách student
        List<Student> listStudents = new ArrayList<>();
        // thêm các đối tượng student vào listStudents
        listStudents.add(student1);
        listStudents.add(student2);
        listStudents.add(student3);
        
        // tạo các đối tượng student chỉ có thuộc tính ID
        Student searchStudent1 = new Student("123");
        Student searchStudent4 = new Student("789");
         
        // tìm kiếm student trong danh sách
        System.out.println("Search student1: "
                + listStudents.contains(searchStudent1));
        System.out.println("Search student4: "
                + listStudents.contains(searchStudent4));
    }
}

Kết quả:

Search student1: true
Search student4: false

Phương thức hashCode() trong Java

Định nghĩa phương thức hashCode() trong lớp Object:

public native int hashCode();

Bạn có thể thấy phương thức này trả về một số nguyên. Vậy nó được sử dụng ở đâu?

Đây là bí mật:

Số băm này được sử dụng bởi các collection dựa trên bảng băm như Hashtable , HashSet và HashMap để lưu trữ các đối tượng trong các container nhỏ được gọi là “nhóm”. Mỗi nhóm được liên kết với mã băm và mỗi nhóm chỉ chứa các đối tượng có mã băm giống hệt nhau.

Nói cách khác, một bảng băm nhóm các phần tử của nó bằng các giá trị mã băm của chúng. Sự sắp xếp này giúp cho bảng băm định vị một phần tử một cách nhanh chóng và hiệu quả bằng cách tìm kiếm trên các phần nhỏ của collection thay vì toàn bộ collection.

Dưới đây là các bước để định vị một phần tử trong một bảng băm:

  • Nhận giá trị mã băm của phần tử được chỉ định bằng cách gọi phương thức hashCode().
  • Tìm nhóm thích hợp được liên kết với mã băm đó.
  • Bên trong nhóm, tìm phần tử chính xác bằng cách so sánh phần tử được chỉ định với tất cả các phần tử trong nhóm. Bằng phương thức equals() của phần tử đã chỉ định được gọi.

Có nói rằng, khi chúng ta thêm các đối tượng của một lớp vào một collection dựa trên bảng băm (HashSet, HashMap ), phương thức hashCode() của lớp được gọi để tạo ra một số nguyên (có thể là một giá trị tùy ý). Con số này được sử dụng bởi bộ sưu tập để lưu trữ và định vị các đối tượng một cách nhanh chóng và hiệu quả, vì collection dựa trên bảng băm không duy trì thứ tự các phần tử của nó.

LƯU Ý: Việc thực thi phương thức mặc định hashCode() trong lớp Object trả về một số nguyên là địa chỉ bộ nhớ của đối tượng. Bạn nên ghi đè phương thức trong các lớp của bạn. Hầu hết các lớp trong JDK ghi đè phương thức hashCode() của riêng chúng, chẳng hạn như String , Date , Integer , Double , v.v.

Các quy tắc cho phương thức equals() và hashCode() trong Java

Như đã giải thích ở trên, collection dựa trên bảng băm xác định một phần tử bằng cách gọi phương thức hashCode() và equals() của nó, vì vậy khi ghi đè các phương thức này chúng ta phải tuân theo các quy tắc sau:

  • Khi phương thức equals() được ghi đè, phương thức hashCode() cũng phải được ghi đè.
  • Nếu hai đối tượng bằng nhau, mã băm của chúng phải bằng nhau.
  • Nếu hai đối tượng không bằng nhau, không có ràng buộc về mã băm của chúng (mã băm của chúng có thể bằng nhau hay không).
  • Nếu hai đối tượng có mã băm giống nhau, thì không có ràng buộc nào về sự bình nhau của chúng (chúng có thể bằng nhau hay không).
  • Nếu hai đối tượng có mã băm khác nhau, chúng không được bằng nhau.

Nếu chúng ta vi phạm các quy tắc này, các collection sẽ hoạt động có thể không đúng như các đối tượng không thể tìm thấy, hoặc các đối tượng sai được trả về thay vì các đối tượng chính xác.

Ví dụ ghi đề phương thức equals(), không ghi đè hashCode()

Hãy xem phương thức hashCode() và equals() ảnh hưởng như thế nào đến hành vi của một đối tượng Set.

Lớp Student.java

package vn.viettuts;
public class Student {
    private String id;
    private String name;
    private String email;
    private int age;
    public Student(String id) {
        this.id = id;
    }
    public Student(String id, String name, String email, int age) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.age = age;
    }
    public String toString() {
        String studentInfo = "Student " + id;
        studentInfo += ": " + name;
        studentInfo += " - " + email;
        studentInfo += " - " + age;
        return studentInfo;
    }
    public boolean equals(Object obj) {
        if (obj instanceof Student) {
            Student another = (Student) obj;
            if (this.id.equals(another.id)) {
                return true;
            }
        }
        return false;
    }
}

Lớp EqualStudent3 .java

package vn.viettuts;
import java.util.HashSet;
import java.util.Set;
public class EqualStudent3 {
    public static void main(String[] args) {
        Student student1 = new Student("123", "Cong", "cong@gmail.com", 22);
        Student student2 = new Student("123", "Cong", "cong@gmail.com", 22);
        Student student3 = new Student("456", "Dung", "dung@gmail.com", 18);
        Set<Student> setStudents = new HashSet<Student>();
        setStudents.add(student1);
        setStudents.add(student2);
        setStudents.add(student3);
        // in các phần tử của set ra màn hình
        for (Student student : setStudents) {
            System.out.println(student);
        }
    }
}

Kết quả:

Student 456: Dung - dung@gmail.com - 18
Student 123: Cong - cong@gmail.com - 22
Student 123: Cong - cong@gmail.com - 22

Hãy nhìn xem, bạn có nhận thấy rằng có 2 sinh viên trùng lặp (ID: 123), phải không?

Đây là lý do:

Tập Set gọi các phương thức equals() và hashCode() trên mỗi đối tượng được thêm vào để đảm bảo không có sự trùng lặp. Trong trường hợp của chúng ta, lớp Student chỉ ghi đè phương thức equals(). Và phương thức hashCode() thừa kế từ lớp Object trả về các địa chỉ bộ nhớ của mỗi đối tượng không nhất quán với phương thức equals(). Do đó, đối tượng Set xử lý đối tượng student1 và student2 thành hai phần tử khác nhau.

Bây giờ, chúng ta hãy ghi đè phương thức hashCode() trong lớp Student như sau:

public int hashCode() {
    return 31 + id.hashCode();
}

Kết quả:

Student 123: Cong - cong@gmail.com - 22
Student 456: Dung - dung@gmail.com - 18

Good! Phần tử trùng lặp hiện đã bị xóa. Đó chính là điều chúng tôi muốn.

Với các phương thức equals() và hashCode() được ghi đè đúng cách, chúng ta cũng có thể thực hiện tìm kiếm trên tập hợp như sau:

Student searchStudent = new Student("456");
boolean found = setStudents.contains(searchStudent);
System.out.println("Search student: " + found);

Kết quả:

Search student: true

Để tự mình thử nghiệm nhiều hơn, hãy thử loại bỏ phương thức equals() hoặc hashCode() và quan sát kết quả.

Hy vọng bài này giúp bạn hiểu cách sử dụng các phương thức equals() và hashCode(). Và áp dụng chúng cho các collection.

 

Trả lời