openplanning

Tìm hiểu về Java System.identityHashCode, Object.hashCode và Object.equals

  1. Hợp đồng equals()
  2. System.identityHashCode(Object)
  3. Object.hashcode()
  4. Vi phạm tính nhất quán hashCode() & equals()

1. Hợp đồng equals()

Phương thức equals(Object) được sử dụng để so sánh đối tượng hiện tại với một đối tượng khác dựa trên các giá trị của các property của mỗi đối tượng. Bạn có thể ghi đè (override) phương thức này trong lớp của mình.
public boolean equals(Object other)
Ví dụ: Lớp Money với 2 property: currencyCode & amount (Mã tiền tệ và số tiền). Hai đối tượng Money được coi là bằng nhau theo phương thức equals() nếu chúng có cùng currencyCodeamount:
Money.java
package org.o7planning.equals.ex;

import java.util.Objects;

public class Money {
    private String currencyCode;
    private int amount;

    public Money(String currencyCode, int amount) {
        this.amount = amount;
        this.currencyCode = currencyCode;
    }

    public int getAmount() {
        return amount;
    }

    public String getCurrencyCode() {
        return currencyCode;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof Money)) {
            return false;
        }
        Money o = (Money) other;
        return this.amount == o.amount //
                && Objects.equals(this.currencyCode, o.currencyCode);
    }
}
Khi ghi đè (override) phương thức equals() bạn cần tuân thủ các tiêu chí dưới đây, chúng được gọi là một hợp đồng equals():
1
Reflexive
(Tính phản xạ)
Một đối tượng phải bằng chính nó.
2
Symmetric
(Tính đối xứng)
x.equals(y) phải trả về cùng một giá trị như y.equals(x).
3
Transitive
(Tính bắc cầu)
Nếu x.equals(y)y.equals(z) thì x.equals(z).
4
Consistent
(Tính nhất quán)
Giá trị của x.equals(y) không thay đổi nếu các property tham gia vào sự so sánh không thay đổi. (Không cho phép sự ngẫu nhiên).
Symmetric
Tính đối xứng (symmetric) của equals() cần được đảm bảo, hay nói cách khác nếu x.equals(y) thì y.equals(x). Điều này tưởng có vẻ đơn giản nhưng đôi khi bạn vi phạm nó một cách không chủ ý.
Ví dụ: Lớp WrongVoucher dưới đây vi phạm tính đối xứng của hợp đồng equals():
WrongVoucher.java
package org.o7planning.equals.ex;

import java.util.Objects;

public class WrongVoucher extends Money {
    private String store;

    public WrongVoucher(String store, String currencyCode, int amount) {
        super(currencyCode, amount);
        this.store = store;
    }

    public String getStore() {
        return store;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof WrongVoucher)) {
            return false;
        }
        WrongVoucher o = (WrongVoucher) other;
        
        return this.getAmount() == o.getAmount() //
                && Objects.equals(this.getCurrencyCode(), o.getCurrencyCode()) //
                && Objects.equals(this.store, o.store);
    }
}
Thoạt nhìn, lớp WrongVoucher và phương thức equals() của nó có vẻ đúng. Nó hoạt động hoàn hảo nếu so sánh 2 đối tượng WrongVoucher với nhau, nhưng bạn sẽ thấy vấn đề nếu so sánh đối tượng WrongVoucher với Money và ngược lại.
WrongVoucherTest.java
package org.o7planning.equals.ex;

public class WrongVoucherTest {
    
    public static void main(String[] args)  {
        Money m = new Money("USD", 100);
        WrongVoucher wv = new WrongVoucher("Chicago S1", "USD", 100);
        
        System.out.println("m.equals(wv): " + m.equals(wv)); // true
        System.out.println("wv.equals(m): " + wv.equals(m)); // false
    }
}
Output:
m.equals(wv): true
wv.equals(m): false
Để tránh cạn bẫy trên chúng ta có thể viết lại lớp Voucher và sử dụng Money như một property thay vì thừa kế từ Money.
Voucher.java
package org.o7planning.equals.ex;

import java.util.Objects;

public class Voucher {
    private String store;
    private Money money;

    public Voucher(String store, String currencyCode, int amount) {
        this.store = store;
        this.money = new Money(currencyCode, amount);
    }

    public String getStore() {
        return store;
    }

    public Money getMoney() {
        return money;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof Voucher)) {
            return false;
        }
        Voucher o = (Voucher) other;
        
        return Objects.equals(this.store, o.store) //
                && this.money.equals(o.money);
    }
}

2. System.identityHashCode(Object)

Trong Java, phương thức tĩnh System.identityHashCode(obj) trả về identity hashcode (mã băm danh tính) của đối tượng obj, nó là một số nguyên không âm nằm trong khoảng [0, 2^31-1]. Identity hashcode của một đối tượng null là 0.
@HotSpotIntrinsicCandidate
public static native int identityHashCode(Object x);
Theo ý tưởng thiết kế, identity hashcode của các đối tượng khác nhau phải khác nhau. Tuy nhiên điều này không được đảm bảo một cách tuyệt đối, thuật toán của JVM chỉ có thể đảm bảo rằng xác suất trùng lặp của identity hashcode là rất nhỏ. Identity hashcode của một đối tượng chỉ được tính tại thời điểm đầu tiên khi nó thực sự được sử dụng và được lưu vào Header của đối tượng.
Identity hashcode chắc chắn không được tạo ra dựa trên địa chỉ của đối tượng trên bộ nhớ. Thật đáng tiếc không có tài liệu nào nói về thuật toán tạo ra identity hashcode, bí mật đó nằm tại mã nguồn của JVM được viết bằng ngôn ngữ C++. Tôi sẽ cập nhập về thuật toán này nếu có thêm thông tin.

3. Object.hashcode()

Phương thức hashCode() của lớp java.lang.Object trả về hashcode của đối tượng hiện tại, nó chính xác là identity hashcode của đối tượng đó.
public class Object  {
    
    public int hashCode() {
        return System.identityHashCode(this);
    }
}
Ví dụ về hashcodeidentity hashcode của một đối tượng thuần thuý (new java.lang.Object()).
HashCodeEx1.java
package org.o7planning.hashcode.ex;

public class HashCodeEx1 {

    public static void main(String[] args) {
        Object obj1 = new Object();
        
        int idHashcode = System.identityHashCode(obj1);
        int hashcode = obj1.hashCode();
        
        System.out.println("Identity Hashcode: " + idHashcode);
        System.out.println("Hashcode: " + hashcode);
    }
}
Output:
Identity Hashcode: 1651191114
Hashcode: 1651191114
Các lớp hậu duệ của java.lang.Object có thể ghi đè (override) phương thức hashCode() để trả về một giá trị tuỳ biến nhưng cần đảm bảo các quy tắc dưới đây, nó cũng được gọi là bản hợp đồng hashCode().
1
Equals consistency
(Tính nhất quán bằng)
Nếu 2 đối tượng bằng nhau theo phương thức equals(Object) thì phương thức hashCode() của chúng phải trả về cùng một giá trị.
2
Internal consistency
(Tính nhất quán nội bộ)
Giá trị của hashCode() có thể thay đổi chỉ khi các property tham gia trong phương thức equals(Object) thay đổi.
Hai đối tượng không bằng nhau theo phương thức equals(Object) không nhất thiết phải có hashcode khác nhau. Tuy nhiên hai đối tượng khác nhau có các giá trị hashcode khác nhau sẽ cải thiện hiệu suất của Hash table (Xem thêm giải thích trong bài viết về HashMapHashSet).
Xem thêm:
  • Hướng dẫn và ví dụ Java HashSet
HashCodeEx2.java
package org.o7planning.hashcode.ex;

public class HashCodeEx2 {

    public static void main(String[] args) {
        Employee tom = new Employee("Tom");
        Employee jerry = new Employee("Jerry");
        
        System.out.println("Employee: " + tom.getFullName());
        System.out.println("  - Identity hashcode: " + System.identityHashCode(tom));
        System.out.println("  - Hashcode: " + tom.hashCode());
        
        System.out.println("\nEmployee: " + jerry.getFullName());
        System.out.println("  - Identity hashcode: " + System.identityHashCode(jerry));
        System.out.println("  - Hashcode: " + jerry.hashCode());
    }
}

class Employee {
    private String fullName;

    public Employee(String fullName) {
        this.fullName = fullName;
    }
    
    public String getFullName()  {
        return this.fullName;    
    }

    @Override
    public int hashCode() {
        if (this.fullName == null || this.fullName.isEmpty()) {
            return 0;
        }
        char ch = this.fullName.charAt(0);
        return (int) ch;
    }
}
Output:
Employee: Tom
  - Identity hashcode: 1579572132
  - Hashcode: 84

Employee: Jerry
  - Identity hashcode: 359023572
  - Hashcode: 74

4. Vi phạm tính nhất quán hashCode() & equals()

Về cơ bản khi lớp của bạn ghi đè (override) phương thức equals(Object) bạn cũng phải ghi đè phương thức hashCode() để đảm bảo rằng 2 đối tượng bằng nhau theo phương thức equals(Object) sẽ có cùng hashcode. Điều này là cần thiết và an toàn khi bạn sử dụng đối tượng của lớp này như một khoá của *HashMap (HashMap, WeakHashMap, IdentityHashMap,...).
Lớp BadTeam dưới đây vi phạm Equals consistency (Tính nhất quán bằng):
BadTeam.java
package org.o7planning.equals.ex;

import java.util.Objects;

public class BadTeam {
    private String name;
    private int numberOfMembers;
    
    public BadTeam(String name, int numberOfMembers) {
        this.name = name;
        this.numberOfMembers = numberOfMembers;
    }
    public String getName() {
        return name;
    }
    public int getNumberOfMembers() {
        return numberOfMembers;
    }
    
    @Override
    public boolean equals(Object other)  {
        if(this == other)  {
            return true;
        }
        if(!(other instanceof BadTeam))  {
            return false;
        }
        BadTeam o = (BadTeam) other;
        return Objects.equals(this.name, o.name);
    }
    
    @Override
    public int hashCode()  {
        return this.numberOfMembers;
    }
}
BadTeamTest.java
package org.o7planning.equals.ex;

public class BadTeamTest {

    public static void main(String[] args) {
        BadTeam team1 = new BadTeam("Team 1", 3);
        BadTeam team2 = new BadTeam("Team 1", 5);
        
        boolean isEquals = team1.equals(team2); // true
        
        int hashcode1 = team1.hashCode(); // 3
        int hashcode2 = team2.hashCode(); // 5
        
        System.out.println("team1.equals(team2): " + isEquals); // true
        System.out.println("hashcode1 == hashcode2: " + (hashcode1 == hashcode2)); // false
    }
}
Output:
team1.equals(team2): true
hashcode1 == hashcode2: false
Việc vi phạm hợp đồng hashCode() có thể gây ra hậu quả khi bạn sử dụng lớp *HashMap(HashMap, WeakHashMap, IdentityHashMap,..). Mọi thứ có thể không hoạt động như mong đợi của bạn.
HashMap_BadTeam_Test.java
package org.o7planning.equals.ex;

import java.util.HashMap;

public class HashMap_BadTeam_Test {

    public static void main(String[] args) {

        // BadTeam team --> String leader.
        HashMap<BadTeam, String> map = new HashMap<>();

        BadTeam team1 = new BadTeam("Team 1", 3);
        BadTeam team2 = new BadTeam("Team 1", 5);
        
        map.put(team1, "Tom");
        map.put(team2, "Jerry");
        
        BadTeam team = new BadTeam("Team 1", 10);
        
        String leader = map.get(team);
        System.out.println("Leader of " + team.getName() + " is " + leader);
    }
}
Output:
Leader of Team 1 is null
Xem thêm cách HashMap, WeakHashMapIdentityHashMap lưu trữ dữ liệu để hiểu thêm những gì đã được đề cập ở trên:

Java cơ bản

Show More