一、前言
對於這兩個方法的研究,源於一道比較經典的面試題:“x.equals(y)==true;x,y可有不同的hashcode對嗎?”,其實這道題的關鍵在於考我們對equals()方法和hashCode()方法的理解,網上看了不少文章,有說對的,也有說不對的。在我看來對也不對,具體原因,我們下面慢慢分析。
二、equals()方法
equals()方法是Object中定義的方法,任何類都可以重寫,但是需要遵循一定的規範,我們看一下Object中的預設實現
public boolean equals(Object obj) {
return (this == obj);
}複製程式碼
可以看到就是對兩個物件的比較,也就是兩個物件的地址值是否相等。然而有的時候,預設的比較方式並不能滿足我們的需求,比如要我們判斷兩個學生物件是否是同一個,需要從學號、姓名等方面來比較,這時候就要重寫equals()方法了,這個重寫規則,JAVA是有明確的規範的:
2.1 自反性:x.equals(x)必須為true;
2.2 對稱性:x.equals(y)和y.equals(x)返回值必須相等;
2.3 傳遞性:x.equals(y)為true,和y.equals(z)為true,那麼x.equals(z)也必須為true;
2.4 一致性:如果物件x和y在equals()中使用的資訊沒有改變,那麼x.equals(y)的值始終不變;
2.5 非null : x不是null,y是null,那麼x.equals(y)必須為false。複製程式碼
三、hashCode()方法
這是Android SDK中Object類的hashCode()方法實現:
/**
* Returns a hash code value for the object. This method is
* supported for the benefit of hash tables such as those provided by
* {@link java.util.HashMap}.
* <p>
*/
public int hashCode() {
int lockWord = shadow$_monitor_;
final int lockWordStateMask = 0xC0000000; // Top 2 bits.
final int lockWordStateHash = 0x80000000; // Top 2 bits are value 2 (kStateHash).
final int lockWordHashMask = 0x0FFFFFFF; // Low 28 bits.
if ((lockWord & lockWordStateMask) == lockWordStateHash) {
return lockWord & lockWordHashMask;
}
return System.identityHashCode(this);
}
public static native int identityHashCode(Object x);複製程式碼
通過註釋我們可以得知一個很重要的資訊,那就是hashCode()這個方法主要是為了更好的支援雜湊表(如HashMap、HashSet、HashTable等)。說到這,我們有必要了解一下雜湊表的儲存原理了:
當我們向雜湊表(如HashMap、HashSet、HashTable等)插入一個object時,首先呼叫hashcode()方法獲得該物件的雜湊碼,通過該雜湊碼可以直接定位object在雜湊表中的位置(一般是雜湊碼對雜湊表大小取餘),如果該位置沒有物件,那麼直接將object插入到該位置;如果該位置有物件(可能有多個,通過連結串列實現),則呼叫equals()方法將這些物件與object比較,如果相等,則不需要儲存object,否則,將該物件插入到該連結串列中。
這也就解釋了,為什麼equals()相等,那麼hashCode()必須相等。因為,如果兩個物件的equals()方法返回true,則它們在雜湊表中只應該出現一次;如果hashCode()不相等,那麼它們會被雜湊到表中不同的位置,雜湊表中不止出現一次。
JAVA建議,如果我們重寫了equals()方法,那麼也要重寫hashCode()方法,因為預設的hashCode()方法返回的是該物件記憶體中的地址。還是上面的例子,我們進行兩個學生的比較,依據是學號和姓名,如果都相等,那麼認為是同一個物件,有如下程式碼:
public class Student {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
Student student1 = new Student();
student1.setId(1);
student1.setName("name1");
Student student2 = new Student();
student2.setId(1);
student2.setName("name1");
System.out.println(student1.equals(student2));
}
}複製程式碼
這時候返回的是false,肯定不是我們想要的,所以我們需要重寫equals()方法,讓它符合我們的業務需求,重寫後的程式碼如下:
public class Student {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
if (getId() != student.getId()) return false;
return getName() != null ? getName().equals(student.getName()) : student.getName() == null;
}
public class Main {
public static void main(String[] args) {
Student student1 = new Student();
student1.setId(1);
student1.setName("name1");
Student student2 = new Student();
student2.setId(1);
student2.setName("name1");
System.out.println(student1.equals(student2));
}
}
}複製程式碼
通過重寫equals()方法之後,列印結果變成了true,但是這樣就完了嗎?我們進行另外一種測試:
public class Main {
public static void main(String[] args) {
Student student1 = new Student();
student1.setId(1);
student1.setName("name1");
Student student2 = new Student();
student2.setId(1);
student2.setName("name1");
System.out.println(student1.equals(student2));
Set<Student> set = new HashSet<>();
set.add(student1);
set.add(student2);
System.out.println("setSize: " + set.size());
}
}複製程式碼
上面student1.equals(student2)返回的是true,setSize理應返回的結果是1才對,但是返回的確是2,這是因為它們的hashCode是不一樣的,所以Set集合認為它們是兩個不同的物件,因此新增了兩次。這時候hashCode()就排上用場了,我們需要重寫hashCode()方法,這就是為什麼JDK說,如果重寫了equals()方法,必須要重寫hashCode()方法的原因,:
public class Student {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
if (getId() != student.getId()) return false;
return getName() != null ? getName().equals(student.getName()) : student.getName() == null;
}
@Override
public int hashCode() {
int result = id;
result = 31 * result + name.hashCode();
return result;
}
}複製程式碼
這樣才算完事了。
hashCode()方法重寫的一些原則:
a.如果重寫equals()方法,檢查條件“兩個物件通過equals()方法判斷相等,那麼它們的hashCode()也應該相等”是否成立,如果不成立,則重寫hashCode()方法。
b.hashCode()不能太簡單,否則容易造成hash衝突;
c.hashCode()不能太複雜,否則會影響效能。複製程式碼
但是一般來說我們不需要自己去寫,這裡有幾種便捷的實現方式:
(1) Google的Guava專案裡有處理hashCode()和equals()的工具類com.google.common.base.Objects;
(2) Apache Commons也有類似的工具類EqualsBuilder和HashCodeBuilder;
(3) Java 7 也提供了工具類java.util.Objects;
(4) 常用IDE都提供hashCode()和equals()的程式碼生成。複製程式碼
至此,我們已經可以解答最開始的問題:“x.equals(y)==true;x,y可有不同的hashcode對嗎?”
嚴格來說,如果兩個物件x.equals(y)==true,那麼它們的hashCode()也要相等,如果不相等,當把它們新增到雜湊表中時就會出現資料混亂(如重複新增);但是,由於hashCode()方法並不是強制實現的,所以,是可能存在不同的hashcode的。
三、總結
如果想寫出優秀的程式碼又不想踩坑就遵循官方的規範吧。