在Java中,equals()方法與hashCode方法定義在java.lang.Object類中,意味著所有的類都會默認(rèn)有這兩個方法豌鸡。我們來看一下這兩個方法在Object類中的定義:
public boolean equals(Object obj) {
return (this == obj);
}
public native int hashCode();
可以看到潜必,euqals方法是用this和參數(shù)對象obj用==做比較,在這里比較的是兩個對象的引用魂仍,如果兩個對象的引用相同拐辽,那么返回true,即兩個對象是相等的擦酌。hashCode()方法是一個本地方法俱诸,它是根據(jù)對象的內(nèi)存地址導(dǎo)出的一個hash值(對象的存儲地址)。
在實際應(yīng)用中赊舶,比較兩個對象的引用是否相同來確定兩個對象是否相等是沒有任何意義的睁搭,考慮如下代碼:
public class Person{
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
// 省略get,set方法
}
public class Test{
public static void main(String[] args){
Person sanmao = new Person("sanmao",15);
Person zhangsan = new Person("sanmao",15);
System.out.println(sanmao.equals(zhangsan));
}
}
上面我們定義了一個Person類,有兩個屬性笼平,name名稱和age年齡园骆。我們在main()方法中定義了兩個Person,他們的名字和年齡都是一樣的寓调,那么在實際抽象中我們認(rèn)為sanmao和zhangsan指的是同一個人锌唾,但實際結(jié)果上面的運(yùn)行結(jié)果卻輸出的是false。因為我們在Person類中沒有重寫繼承自O(shè)bject中的equals()方法夺英,實際比較的是兩個對象的引用是否相同晌涕,很明顯我們定義了兩個對象滋捶,當(dāng)然輸出的是false。為了實現(xiàn)我們想要的邏輯渐排,應(yīng)該修改Person類炬太,重寫equals()方法,定義自己的比較邏輯:
public class Person{
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
// 省略get,set方法
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
return Objects.equals(name, other.name)
&& age==other.age;
}
}
在這里為了防備name為null的情況我們使用Objects.equals方法驯耻,如果兩個參數(shù)都為null亲族,則該方法返回true,只有一個為null,返回false,兩個都不為null,則調(diào)用a.equals(b)返回實際值。
euqals方法必須遵守以下幾種特性:
- 自反性:對于任何非空引用x可缚,x.euqals(x)應(yīng)返回true霎迫。
- 對稱性:對于任何引用x和y,當(dāng)且僅當(dāng)下帘靡,x.equals(y)返回true時知给,y.equals(x)也應(yīng)該返回true。
- 傳遞性:對于任何引用x,y和z描姚,x.equals(y)返回true涩赢,y.equals(z)也返回true,那x.equals(z)應(yīng)該也返回true
- 一致性:如果x和y的引用沒有發(fā)生變化轩勘,反復(fù)調(diào)用x.equals(y)應(yīng)該返回相同的結(jié)果
- 對于任何非空引用x筒扒,x.equals(null)應(yīng)該返回false
現(xiàn)在我們運(yùn)行main方法可以看到輸出了true。
難道到這里就結(jié)束了嗎绊寻?NO... 我們經(jīng)常聽到重寫euqals方法就必須重寫hashCode方法花墩,它們兩者之間有什么聯(lián)系嗎?我們來修改一下main方法的代碼:
public static void main(String[] args) {
Set<Person> set=new HashSet<>();
Person p1=new Person("sanmao",15);
Person p2=new Person("sanmao",15);
set.add(p1);
set.add(p2);
System.out.println(set.size());
}
上面的代碼運(yùn)行結(jié)果為2澄步,這很奇怪冰蘑,Set中的元素是不會重復(fù)的,我們的p1和p2兩個對象一樣村缸,那我們期望的結(jié)果應(yīng)該是1,為什么會是2呢祠肥。我們來看一下HashSet的源碼:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
static final long serialVersionUID = -5024744406713321676L;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
//省略超多個方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//省略超多個方法
}
可以看到,HashSet內(nèi)部是用一個HashMap來做存儲的梯皿,實際調(diào)用的是HashMap的put方法搪柑,那我們繼續(xù)跟蹤一下HashMap的put方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以看到,在put方法中調(diào)用了一個靜態(tài)的hash方法索烹,在該靜態(tài)方法中調(diào)用了該對象的hashCode方法。問題就在這里了弱睦,我們重寫了Person類的equals方法百姓,p1.equals(p2)返回了true,但是p1和p2的hashCode方法還是調(diào)用的繼承自O(shè)bject類的hashCode方法,即p1.hashCode()和p2.hashCode()返回的還是各自的存儲地址况木,在往map中插入元素的時候垒拢,由于hashCode返回值不相等旬迹,則直接往map中添加元素(這里設(shè)計到map添加元素的過程,這里就不詳細(xì)說了)求类,結(jié)果就導(dǎo)致邏輯上相同的連個對象被添加了兩次奔垦。
為了保證往Set中添加元素得到正確的結(jié)果,我們必須重寫hashCode方法:
public class Person{
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
// 省略get,set方法
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
return Objects.equals(name, other.name)
&& age==other.age;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
}
現(xiàn)在Person類對象的hash值是由對象內(nèi)容導(dǎo)出的尸疆,能夠保證p1.equals(p2)==true時椿猎,p1.hashCode()==p2.hashCode。在和Set這樣的散列表一起工作時得到正確的結(jié)果寿弱。
在重寫euqals方法時犯眠,一定要重寫hashCode方法,確保散列表能夠正確結(jié)果症革。
如果兩個對象equals返回true,則hashCode也返回相同值筐咧。
如果兩個對象equals返回false,則hashCode不一定返回不同值。
如果兩個對象hashCode返回相同值噪矛,equals不一定返回true量蕊。
如果兩個對象hashCode返回不同值,equals一定返回false艇挨。