Tìm hiểu về Java System.identityHashCode, Object.hashCode và Object.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 currencyCode và amount:
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) và 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ề hashcode và identity 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ề HashMap và HashSet).
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, WeakHashMap và IdentityHashMap lưu trữ dữ liệu để hiểu thêm những gì đã được đề cập ở trên:
Java cơ bản
- Tùy biến trình biên dịch java xử lý Annotation của bạn (Annotation Processing Tool)
- Lập trình Java theo nhóm sử dụng Eclipse và SVN
- Hướng dẫn và ví dụ Java WeakReference
- Hướng dẫn và ví dụ Java PhantomReference
- Hướng dẫn nén và giải nén trong Java
- Cấu hình Eclipse để sử dụng JDK thay vì JRE
- Phương thức String.format() và printf() trong Java
- Cú pháp và các tính năng mới trong Java 5
- Cú pháp và các tính năng mới trong Java 8
- Hướng dẫn sử dụng biểu thức chính quy trong Java
- Hướng dẫn lập trình đa luồng trong Java - Java Multithreading
- Thư viện điều khiển các loại cơ sở dữ liệu khác nhau trong Java
- Hướng dẫn sử dụng Java JDBC kết nối cơ sở dữ liệu
- Lấy các giá trị của các cột tự động tăng khi Insert một bản ghi sử dụng JDBC
- Hướng dẫn và ví dụ Java Stream
- Functional Interface trong Java
- Giới thiệu về Raspberry Pi
- Hướng dẫn và ví dụ Java Predicate
- Abstract class và Interface trong Java
- Access modifier trong Java
- Hướng dẫn và ví dụ Java Enum
- Hướng dẫn và ví dụ Java Annotation
- So sánh và sắp xếp trong Java
- Hướng dẫn và ví dụ Java String, StringBuffer và StringBuilder
- Hướng dẫn xử lý ngoại lệ trong Java - Java Exception Handling
- Hướng dẫn và ví dụ Java Generics
- Thao tác với tập tin và thư mục trong Java
- Hướng dẫn và ví dụ Java BiPredicate
- Hướng dẫn và ví dụ Java Consumer
- Hướng dẫn và ví dụ Java BiConsumer
- Bắt đầu với Java cần những gì?
- Lịch sử của Java và sự khác biệt giữa Oracle JDK và OpenJDK
- Cài đặt Java trên Windows
- Cài đặt Java trên Ubuntu
- Cài đặt OpenJDK trên Ubuntu
- Cài đặt Eclipse
- Cài đặt Eclipse trên Ubuntu
- Học nhanh Java cho người mới bắt đầu
- Lịch sử của bit và byte trong khoa học máy tính
- Các kiểu dữ liệu trong Java
- Các toán tử Bitwise
- Câu lệnh rẽ nhánh (if else) trong Java
- Câu lệnh rẽ nhánh switch trong Java
- Vòng lặp trong Java
- Mảng (Array) trong Java
- JDK Javadoc định dạng CHM
- Thừa kế và đa hình trong Java
- Hướng dẫn và ví dụ Java Function
- Hướng dẫn và ví dụ Java BiFunction
- Ví dụ về Java encoding và decoding sử dụng Apache Base64
- Hướng dẫn và ví dụ Java Reflection
- Hướng dẫn gọi phương thức từ xa với Java RMI
- Hướng dẫn lập trình Java Socket
- Các nền tảng nào bạn nên chọn để lập trình ứng dụng Java Desktop?
- Hướng dẫn và ví dụ Java Commons IO
- Hướng dẫn và ví dụ Java Commons Email
- Hướng dẫn và ví dụ Java Commons Logging
- Tìm hiểu về Java System.identityHashCode, Object.hashCode và Object.equals
- Hướng dẫn và ví dụ Java SoftReference
- Hướng dẫn và ví dụ Java Supplier
- Lập trình Java hướng khía cạnh với AspectJ (AOP)
Show More
- Hướng dẫn lập trình Java Servlet/JSP
- Các hướng dẫn Java Collections Framework
- Java API cho HTML & XML
- Các hướng dẫn Java IO
- Các hướng dẫn Java Date Time
- Các hướng dẫn Spring Boot
- Các hướng dẫn Maven
- Các hướng dẫn Gradle
- Các hướng dẫn Java Web Services
- Các hướng dẫn lập trình Java SWT
- Các hướng dẫn lập trình JavaFX
- Các hướng dẫn Java Oracle ADF
- Các hướng dẫn Struts2 Framework
- Các hướng dẫn Spring Cloud