Phương Thức equals() & hashCode()

"Trong Java, equals() và hashCode() là hai phương thức quan trọng được sử dụng để so sánh các đối tượng và tạo mã băm tương ứng. Sự phối hợp giữa chúng đảm bảo tính nhất quán trong các cấu trúc dữ liệu như HashMap, HashSet, và HashTable. Dưới đây là những điều cơ bản bạn cần biết về equals() và hashCode() trong Java:"

Ⅰ. Lớp java.lang.Object: Cốt lõi của mọi đối tượng trong Java

Lớp java.lang.Object là lớp gốc của tất cả các lớp trong Java. Điều này có nghĩa là mọi lớp mà bạn tạo ra, dù trực tiếp hay gián tiếp, đều kế thừa từ lớp Object. Vì thế, mọi đối tượng trong Java đều có sẵn các phương thức được định nghĩa trong lớp Object.

Tại sao lớp Object lại quan trọng?

↳ Cốt lõi của hệ thống: Object là nền tảng để xây dựng các lớp khác. Nó cung cấp các phương thức cơ bản nhất mà mọi đối tượng cần có.

↳ Đa hình: Nhờ cơ chế kế thừa, các lớp con có thể ghi đè (override) các phương thức của lớp cha, tạo ra tính đa hình. Điều này cho phép chúng ta viết code linh hoạt và mở rộng.

↳ Garbage Collection: Lớp Object cũng đóng một vai trò quan trọng trong cơ chế thu gom rác của Java. Khi một đối tượng không còn được tham chiếu đến, nó sẽ được bộ thu gom rác tự động xóa khỏi bộ nhớ.

Các phương thức của lớp Objects

↳ static <T> int compare(T a, T b, Comparator<? super T> c): So sánh hai đối tượng ab bằng cách sử dụng bộ so sánh c. Nếu hai đối tượng giống nhau, trả về 0; nếu không, trả về kết quả so sánh.

↳ static boolean deepEquals(Object a, Object b): Kiểm tra xem hai đối tượng có bằng nhau khi so sánh sâu (so sánh tất cả các phần tử của các đối tượng lồng ghép không). Nếu có, trả về true; nếu không, trả về false.

↳ static boolean equals(Object a, Object b): So sánh hai đối tượng a và b để xem chúng có bằng nhau không. Nếu có, trả về true; nếu không, trả về false.

↳ static int hash(Object... values): Sinh mã băm cho một tập hợp các giá trị đầu vào. Ví dụ: nếu bạn có nhiều giá trị và muốn tạo mã băm cho tất cả, bạn có thể dùng phương thức này.

↳ static int hashCode(Object o): Trả về mã băm của đối tượng o. Nếu o không phải là null, trả về mã băm của nó; nếu o là null, trả về 0.

↳ static boolean isNull(Object obj): Kiểm tra xem đối tượng obj có phải là null không. Nếu có, trả về true; nếu không, trả về false.

↳ static boolean nonNull(Object obj): Kiểm tra xem đối tượng obj có phải là khác null không. Nếu không phải là null, trả về true; nếu là null, trả về false.

↳ static <T> requireNonNull(T obj): Đảm bảo rằng đối tượng obj không phải là null. Nếu là null, ném ra ngoại lệ NullPointerException.

↳ static <T> requireNonNull(T obj, String message): Đảm bảo rằng đối tượng obj không phải là null. Nếu là null, ném ra ngoại lệ NullPointerException với thông báo lỗi được chỉ định.

↳ static <T> requireNonNull(T obj, Supplier<String> messageSupplier): Đảm bảo rằng đối tượng obj không phải là null. Nếu là null, ném ra ngoại lệ NullPointerException với thông báo lỗi do messageSupplier cung cấp.

↳ static String toString(Object o): Chuyển đổi đối tượng o thành chuỗi. Nếu o không phải là null, gọi phương thức toString của nó; nếu o là null, trả về chuỗi "null".

↳ static String toString(Object o, String nullDefault): Chuyển đổi đối tượng o thành chuỗi. Nếu o không phải là null, gọi phương thức toString của nó; nếu o là null, trả về chuỗi nullDefault.

Phương thức equals() và hashCode() là hai phương thức quan trọng được cung cấp bởi lớp Object để so sánh các đối tượng. Vì lớp Object là lớp cha của tất cả các đối tượng Java, nên tất cả các đối tượng đều kế thừa triển khai mặc định của hai phương thức này. Trong phần này, chúng ta sẽ xem xét chi tiết về các phương thức equals() và hashCode(), mối liên hệ giữa chúng và cách chúng ta có thể triển khai hai phương thức này trong Java.

Ⅱ. Phương thức equals()

Phương thức equals() trong Java được sử dụng để kiểm tra xem hai đối tượng có "bằng nhau" hay không.

Hành vi mặc định của phương thức equals()

↳ So sánh tham chiếu: Theo mặc định, phương thức equals() chỉ kiểm tra xem hai đối tượng có cùng địa chỉ bộ nhớ hay không. Nói cách khác, nó kiểm tra xem hai biến đang trỏ đến cùng một đối tượng hay không.

↳Không so sánh nội dung: Phương thức mặc định không so sánh nội dung của hai đối tượng.

Các đặc tính của phương thức equals()

Để đảm bảo tính nhất quán, phương thức equals() phải tuân theo các quy tắc tham chiếu đối tượng không null sau:

↳ Phản xạ (Reflexive): Đối với bất kỳ giá trị tham chiếu không null x, x.equals(x) phải trả về true.

↳ Đối xứng (Symmetric): Đối với bất kỳ giá trị tham chiếu không null x và y, x.equals(y) phải trả về true nếu và chỉ nếu y.equals(x) trả về true.

↳ Bắc cầu (Transitive): Đối với bất kỳ giá trị tham chiếu không null x, y và z, nếu x.equals(y) trả về true và y.equals(z) trả về true, thì x.equals(z) phải trả về true.

↳ Nhất quán (Consistent): Đối với bất kỳ giá trị tham chiếu không null x và y, nhiều lần gọi x.equals(y) nhất quán trả về true hoặc nhất quán trả về false, với điều kiện không có thông tin nào được sử dụng trong so sánh equals trên các đối tượng bị sửa đổi.

↳ Không so sánh với null (Non-nullity): Đối với bất kỳ giá trị tham chiếu không null x, x.equals(null) phải trả về false.

Cú pháp của phương thức equals():

Cú Pháp

public boolean equals(Object obj)

Giải thích từng phần:

↳ Tham số obj: Đối tượng tham chiếu cần được so sánh.

↳ Trả về: true nếu hai đối tượng giống nhau, ngược lại trả về false

Dưới đây là một ví dụ minh họa về phương thức equals() trong Java. Phương thức equals() thường được sử dụng để so sánh hai đối tượng xem chúng có bằng nhau không, dựa trên các tiêu chí bạn định nghĩa.

Ví dụ: Person.java

import java.util.Objects;

class Person {
  private String name;
  private int age;

  // Constructor
  public Person(String name, int age) {
      this.name = name;
      this.age = age;
  }

  // Ghi đè phương thức equals()
  @Override
  public boolean equals(Object obj) {
      // Nếu so sánh chính đối tượng hiện tại
      if (this == obj) {
          return true;
      }
      // Nếu obj là null hoặc không cùng kiểu với Person
      if (obj == null || getClass() != obj.getClass()) {
          return false;
      }
      // Ép kiểu obj về kiểu Person để so sánh
      Person person = (Person) obj;
      // So sánh tên và tuổi
      return age == person.age && name.equals(person.name);
  }

  // Phương thức hashCode() cũng nên được ghi đè khi ghi đè equals()
  @Override
  public int hashCode() {
      return Objects.hash(name, age);
  }
}

public class Main {
  public static void main(String[] args) {
      Person person1 = new Person("Dương", 25);
      Person person2 = new Person("Dương", 25);
      Person person3 = new Person("Trường", 30);

      // So sánh person1 và person2
      System.out.println("person1 và person2 bằng nhau: " + person1.equals(person2)); // Kết quả: true

      // So sánh person1 và person3
      System.out.println("person1 và person3 bằng nhau: " + person1.equals(person3)); // Kết quả: false
  }
}

Kết quả của chương trình là:

person1 và person2 bằng nhau: true
person1 và person3 bằng nhau: false

Hy vọng ví dụ này giúp bạn hiểu rõ hơn về cách sử dụng phương thức equals() trong Java!

Dưới đây là ví dụ về cách so sánh chuỗi trong Java để thấy sự khác nhau giữa phương thức equals() và toán tử ==.

Ví dụ: Main.java

public class Main {
    public static void main(String[] args) {
        // Tạo hai chuỗi bằng cách sử dụng ký tự chuỗi
        String str1 = "Hello";
        String str2 = "Hello";

        // Tạo hai chuỗi bằng cách sử dụng từ khóa new
        String str3 = new String("Hello");
        String str4 = new String("Hello");

        // So sánh chuỗi bằng toán tử ==
        System.out.println("So sánh str1 và str2 bằng toán tử ==: " + (str1 == str2)); // Kết quả: true
        System.out.println("So sánh str1 và str3 bằng toán tử ==: " + (str1 == str3)); // Kết quả: false
        System.out.println("So sánh str3 và str4 bằng toán tử ==: " + (str3 == str4)); // Kết quả: false

        // So sánh chuỗi bằng phương thức equals()
        System.out.println("So sánh str1 và str2 bằng phương thức equals(): " + str1.equals(str2)); // Kết quả: true
        System.out.println("So sánh str1 và str3 bằng phương thức equals(): " + str1.equals(str3)); // Kết quả: true
        System.out.println("So sánh str3 và str4 bằng phương thức equals(): " + str3.equals(str4)); // Kết quả: true
    }
}

Kết quả của chương trình là:

So sánh str1 và str2 bằng toán tử ==: true
So sánh str1 và str3 bằng toán tử ==: false
So sánh str3 và str4 bằng toán tử ==: false
So sánh str1 và str2 bằng phương thức equals(): true
So sánh str1 và str3 bằng phương thức equals(): true
So sánh str3 và str4 bằng phương thức equals(): true

Giải thích đoạn mã trên:

Tạo chuỗi:

str1 và str2 được tạo ra bằng cách sử dụng ký tự chuỗi. Java tối ưu hóa bộ nhớ bằng cách lưu trữ các chuỗi giống nhau trong một vùng nhớ chung gọi là "string pool". Do đó, str1 và str2 sẽ trỏ đến cùng một đối tượng chuỗi trong bộ nhớ.

str3 và str4 được tạo ra bằng từ khóa new, nghĩa là mỗi lần sẽ tạo ra một đối tượng chuỗi mới trong bộ nhớ heap.

So sánh chuỗi bằng Toán Tử ==:

Toán tử == so sánh tham chiếu của hai đối tượng, tức là so sánh xem hai đối tượng có cùng trỏ đến một vùng nhớ hay không.

str1 == str2 sẽ trả về true vì cả hai đều trỏ đến cùng một đối tượng trong ""string pool".

str1 == str3 và str3 == str4 sẽ trả về false vì chúng trỏ đến các đối tượng khác nhau trong bộ nhớ heap.

So sánh chuỗi bằng phương thức equals():

Phương thức equals() so sánh nội dung của hai đối tượng chuỗi.

str1.equals(str2), str1.equals(str3) và str3.equals(str4) đều trả về true vì nội dung của các chuỗi đều giống nhau.

Kết luận:

Toán tử == so sánh tham chiếu của các đối tượng.

Phương thức equals() so sánh nội dung của các đối tượng.

Hy vọng ví dụ này giúp bạn hiểu rõ sự khác nhau giữa toán tử == và phương thức equals() trong Java khi so sánh chuỗi!

Phương thức contains(Object) của interface List trong Java sử dụng phương thức equals() để kiểm tra xem một đối tượng cụ thể có tồn tại trong danh sách hay không. Khi bạn gọi contains(Object) trên một danh sách, nó sẽ duyệt qua tất cả các phần tử trong danh sách và sử dụng phương thức equals() của đối tượng để so sánh từng phần tử với đối tượng bạn muốn kiểm tra.

Dưới đây là ví dụ về cách sử dụng phương thức contains(Object) với một danh sách các đối tượng Person, trong đó phương thức equals() của lớp Person đã được ghi đè:

Ví dụ: Main.java

import java.util.ArrayList;
import java.util.List;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    // Ghi đè phương thức equals() để so sánh dựa trên thuộc tính name và age
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        Person person = (Person) obj;
        return age == person.age && name.equals(person.name);
    }

    // Ghi đè phương thức hashCode() để hỗ trợ việc so sánh trong các collection
    @Override
    public int hashCode() {
        int result = name.hashCode();
        result = 31 * result + age;
        return result;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + '}';
    }
}

public class Main {
    public static void main(String[] args) {
        List<Person> personList = new ArrayList<>();

        Person person1 = new Person("Dương", 30);
        Person person2 = new Person("Trường", 25);
        Person person3 = new Person("Dương", 30); // Có cùng thuộc tính với person1

        personList.add(person1);
        personList.add(person2);

        // Kiểm tra xem person3 có tồn tại trong danh sách hay không
        boolean containsPerson3 = personList.contains(person3); // Kết quả: true
        System.out.println("Danh sách có chứa person3: " + containsPerson3);

        // Thêm person3 vào danh sách
        personList.add(person3);

        // In ra các phần tử trong danh sách
        System.out.println("Các phần tử trong danh sách:");
        for (Person person : personList) {
            System.out.println(person);
        }
    }
}

Kết quả của chương trình là:

Danh sách có chứa person3: true
Các phần tử trong danh sách:
Person{name='Dương', age=30}
Person{name='Trường', age=25}
Person{name='Dương', age=30}

Phương thức contains(Object) của List trong Java sử dụng phương thức equals() của đối tượng để kiểm tra sự tồn tại của đối tượng trong danh sách. Khi equals() được ghi đè đúng cách, contains(Object) sẽ hoạt động chính xác, giúp bạn kiểm tra sự tồn tại của các đối tượng trong danh sách dựa trên các tiêu chí so sánh đã định nghĩa trong equals().

Ⅲ. Phương thức hashCode()

Phương thức hashCode() trong Java trả về một giá trị số nguyên (hash code) cho một đối tượng. Giá trị này được sử dụng để hỗ trợ các cấu trúc dữ liệu như bảng băm (hash table) như HashMap.

Các đặc tính của phương thức hashCode()

Để đảm bảo tính nhất quán, phương thức hashCode() tuân theo các quy tắc sau:

↳ Nhất quán (Consistent): Khi gọi phương thức hashCode() trên cùng một đối tượng nhiều lần trong cùng một chương trình, kết quả trả về phải là cùng một số nguyên, trừ khi thông tin được sử dụng để so sánh bằng phương thức equals() thay đổi.

↳ Quan hệ với equals(): Nếu hai đối tượng bằng nhau theo phương thức equals(), thì chúng phải có cùng một giá trị hash code.

↳ Không yêu cầu khác biệt (Non-required uniqueness): Nếu hai đối tượng khác nhau theo phương thức equals(), thì không bắt buộc chúng phải có giá trị hash code khác nhau. Tuy nhiên, việc tạo ra các giá trị hash code khác nhau cho các đối tượng khác nhau sẽ giúp cải thiện hiệu suất của bảng băm.

↳ Giá trị mặc định: Phương thức hashCode() mặc định của lớp Object thường trả về một giá trị dựa trên địa chỉ bộ nhớ của đối tượng, nhưng điều này không bắt buộc.

Cú pháp của phương thức hashCode()

Cú Pháp

public int hashCode()

↳ Trả về: Trả về giá trị mã băm cho các đối tượng đã cho.

Dưới đây là một ví dụ về cách triển khai phương thức hashCode() và equals() trong một lớp Java. Chúng ta sẽ tạo một lớp Person với các thuộc tính name và age, và triển khai cả hai phương thức hashCode() và equals() để đảm bảo rằng hai đối tượng Person có cùng name và age được coi là bằng nhau.

Ví dụ: Main.java

public class Example {
  public static void main(String[] args) {
      // Tạo các đối tượng SinhVien
      SinhVien sv1 = new SinhVien("Nguyễn Văn A", 20, "K62");
      SinhVien sv2 = new SinhVien("Trần Thị B", 19, "K62");

      // In thông tin sinh viên
      sv1.inThongTin();
      System.out.println();
      sv2.inThongTin();
  }
}

Kết quả của chương trình là:

person1 equals person2: true
person1 hashCode: 2058250920
person2 hashCode: 2058250920
person1 equals person3: false
person1 hashCode: 2058250920
person3 hashCode: 97582

Khi bạn chạy chương trình, bạn sẽ thấy rằng person1 và person2 có cùng giá trị mã băm và được coi là bằng nhau bởi phương thức equals(). Tuy nhiên, person1 và person3 có các giá trị mã băm khác nhau và không bằng nhau theo phương thức equals().

Ⅳ. Mối quan hệ giữa hashCode() và equals() trong Java

hashCode() và equals() là hai phương thức cơ bản trong Java, được sử dụng để so sánh và xác định tính duy nhất của các đối tượng. Mặc dù chúng có chức năng khác nhau nhưng lại có mối liên hệ chặt chẽ với nhau để đảm bảo tính nhất quán và hiệu quả của các cấu trúc dữ liệu trong Java.

Mối quan hệ giữa hai phương thức:

↳ Nguyên tắc: Nếu hai đối tượng bằng nhau theo equals(), thì chúng phải có cùng một giá trị hash. Điều này đảm bảo rằng các đối tượng bằng nhau sẽ luôn được nhóm lại cùng nhau trong bảng băm.

↳ Không bắt buộc: Nếu hai đối tượng không bằng nhau theo equals(), thì chúng không nhất thiết phải có giá trị hash khác nhau. Tuy nhiên, để tăng hiệu suất của bảng băm, các đối tượng khác nhau nên có giá trị hash khác nhau càng nhiều càng tốt.

Lưu ý: Theo tài liệu của Java, cả hai phương thức equals() và hashCode() cần được ghi đè để có được cơ chế so sánh đầy đủ; chỉ sử dụng equals() là không đủ. Điều này có nghĩa là, nếu bạn ghi đè phương thức equals(), bạn cũng phải ghi đè phương thức hashCode().

Tại sao cần ghi đè cả hai phương thức?

↳ Đảm bảo tính nhất quán: Khi ghi đè equals(), việc ghi đè hashCode() là cần thiết để đảm bảo rằng hai đối tượng bằng nhau theo equals() sẽ luôn có cùng một giá trị hash.

↳ Tăng hiệu suất: Một hàm hashCode() được thiết kế tốt có thể giúp giảm thiểu các cuộc gọi đến equals(), đặc biệt là trong các cấu trúc dữ liệu lớn.

Nếu bạn ghi đè equals() mà không ghi đè hashCode() hoặc ngược lại, bạn sẽ gặp phải các vấn đề về tính nhất quán và hiệu suất trong các cấu trúc dữ liệu dựa trên băm (như HashMap, HashSet). Dưới đây là những điều có thể xảy ra trong từng trường hợp:

Ví dụ minh họa ghi đè cả hai phương thức equals() và hashCode():

Ví dụ: Example.java

import java.util.HashSet;
import java.util.Objects;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

public class Example {
    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<>();
        Person person1 = new Person("Alice", 30);
        Person person2 = new Person("Alice", 30);

        set.add(person1);
        set.add(person2);

        System.out.println("Kích thước của set: " + set.size());
    }
}

Kết quả của chương trình là:

Kích thước của set: 1

Ghi đè equals() mà không ghi đè hashCode()

Nếu bạn chỉ ghi đè phương thức equals() mà không ghi đè phương thức hashCode(), hai đối tượng có thể được coi là bằng nhau theo equals(), nhưng vẫn có thể có các mã băm khác nhau. Điều này vi phạm hợp đồng của phương thức hashCode(), và dẫn đến các vấn đề sau:

↳ HashSet: Khi bạn thêm các đối tượng vào HashSet, hai đối tượng bằng nhau (theo equals()) có thể không được coi là cùng một đối tượng. Điều này có thể dẫn đến việc HashSet chứa các bản sao không mong muốn của cùng một đối tượng.

↳ HashMap: Khi bạn sử dụng đối tượng làm khóa trong HashMap, hai khóa bằng nhau có thể được lưu trữ như hai mục nhập riêng biệt nếu chúng có các mã băm khác nhau. Điều này có thể gây ra các vấn đề khi tìm kiếm, cập nhật hoặc xóa các mục nhập trong bản đồ.

Ví dụ minh họa ghi đè equals() mà không ghi đè hashCode()

Ví dụ: Example.java

import java.util.HashSet;
import java.util.Objects;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

    // Không ghi đè hashCode()
}

public class Example {
    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<>();
        Person person1 = new Person("Alice", 30);
        Person person2 = new Person("Alice", 30);

        set.add(person1);
        set.add(person2);

        System.out.println("Kích thước của set: " + set.size()); // Có thể là 2 thay vì 1
    }
}

Kết quả của chương trình là:

Kích thước của set: 2

Ghi đè hashCode() mà không ghi đè equals()

Nếu bạn chỉ ghi đè phương thức hashCode() mà không ghi đè phương thức equals(), bạn có thể gặp phải các vấn đề về tính nhất quán sau:

↳ HashSet: Nếu hai đối tượng có cùng mã băm nhưng không được coi là bằng nhau theo equals(), HashSet sẽ coi chúng là hai đối tượng khác nhau. Điều này có thể dẫn đến việc HashSet chứa các bản sao không mong muốn của cùng một đối tượng.

↳ HashMap: Khi sử dụng đối tượng làm khóa trong HashMap, hai đối tượng có cùng mã băm nhưng không bằng nhau (theo equals()) có thể ghi đè lẫn nhau. Điều này dẫn đến việc mất dữ liệu không mong muốn.

Ví dụ ghi đè hashCode() mà không ghi đè equals()

Ví dụ: Example.java

import java.util.HashSet;
import java.util.Objects;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    // Không ghi đè equals()
}

public class Example {
    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<>();
        Person person1 = new Person("Alice", 30);
        Person person2 = new Person("Alice", 30);

        set.add(person1);
        set.add(person2);

        System.out.println("Kích thước của set: " + set.size()); // Có thể là 2 thay vì 1
    }
}

Kết quả của chương trình là:

Kích thước của set: 2

Để đảm bảo tính nhất quán và hiệu suất của các cấu trúc dữ liệu dựa trên băm, bạn cần ghi đè cả hai phương thức equals() và hashCode() khi bạn triển khai một trong hai phương thức này. Điều này đảm bảo rằng các đối tượng được coi là bằng nhau bởi equals() cũng sẽ có cùng mã băm, và các cấu trúc dữ liệu dựa trên băm sẽ hoạt động đúng đắn.

Câu Nói Truyền Cảm Hứng

“Bắt đầu ở đâu không quan trọng, quan trọng là bạn sẵn sàng bắt đầu.” – W. Clement Stone

Không Gian Tích Cực

“Chúc bạn luôn giữ vững niềm tin và sức mạnh để vượt qua mọi thử thách trong cuộc sống.”