hashCode,就是哈希值,可以理解爲一個對象的標識(好的hash,能確保不同的對象有不同的hash值),Object含有hashCode方法,用來返回對象的hash值。hashCode方法多用在基於散列值的集合類,比如HashMap、HashSet和Hashtable。
下面是hashCode的約束規範,
在一個應用程序執行期間,如果一個對象的equals方法做比較所用到的信息沒有被修改的話,那麼,對該對象調用hashCode方法多次,它必須返回同一個整數。在同一個應用多次執行過程中,這個整數可以不同。
如果兩個對象根據equlas方法是相等的,那麼調用這兩個對象的hashCode方法必須產生同樣的整數結果。
如果兩個對象根據equals方法是不相等的。那麼調用這兩個對象的hashCode方法,不要求必須產生不同的整數結果。
如果你重寫了類的equals方法,那麼必須也重寫hashCode方法。否則,就違反了上述的規範。這是因爲,兩個在邏輯上相等的對象(調用equals相等),必須擁有相同的hashCode,但是根據Object的hashCode,它們僅僅是兩個對象,沒有共同的地方。所以違背了規範2. 此時,我們就要重寫hashCode方法。
實例代碼1.
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point))
return false;
Point p = (Point) obj;
return p.x == x && p.y == y;
}
public static void main(String[] args) {
Point p1 = new Point(2, 5);
Point p2 = new Point(2, 5);
System.out.println("p1 equals p2? " + p1.equals(p2));
System.out.println(p1.hashCode());
System.err.println(p2.hashCode());
}
}
在上面的例子上,我們重寫了equals方法,而且能成功判斷p1和p2是相等的,但是沒有重寫hashCode方法,所以我們調用p1.hashCode和p2.hashCode返回的值是不一樣的。
那麼,hashCode方法應該是怎麼樣的呢?編寫一個合法的hashCode並不難,比如,
public int hashCode() {
return 41;
}
由於hashCode規範,並沒有要求不同的對象必須有不同的hashCode,所以我們可以給每個對象都返回一個相同的值。雖然這樣,並沒有違背hashCode的規範。但是在一些散列值存儲中(HashSet、HashMap以及HashTable),卻帶來了災難。
或許,你並不太瞭解散列值存儲,我們以HashMap爲例,HashMap提供了鍵值對(key-value)的存儲,使用範例如下,
Point p1 = new Point(2, 5);
Point p2 = new Point(2, 5);
HashMap<Point, String> hm = new HashMap<Point, String>();
hm.put(p1, "p1");
hm.put(p2, "p2");
System.out.println(hm.get(p1));
System.out.println(hm.get(p2));
那麼,hashCode對於HashMap的作用是什麼呢?
我們知道,在HashMap中,不允許兩個存在兩個相同的對象,那麼如何判斷兩個對象是否相等呢?你或許會說,肯定是調用equals,是的,調用equals沒有問題,但是,如果HashMap含有數萬條數據,對每個對象都調用equals方法,效率肯定是一個問題。
此時hashCode方法的作用就體現出來了,當集合要添加新的對象時,先調用這個對象的hashCode方法,得到對應的hashcode值,實際上在HashMap的具體實現中會用一個table保存已經存進去的對象的hashcode值,如果table中沒有該hashcode值,它就可以直接存進去,不用再進行任何比較了;如果存在該hashcode值, 就調用它的equals方法與新元素進行比較,相同的話就不存了,不相同就散列其它的地址,所以這裏存在一個衝突解決的問題,這樣一來實際調用equals方法的次數就大大降低了,說通俗一點:Java中的hashCode方法就是根據一定的規則將與對象相關的信息(比如對象的存儲地址,對象的字段等)映射成一個數值,這個數值稱作爲散列值。下面這段代碼是java.util.HashMap的中put方法的具體實現:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
put方法是用來向HashMap中添加新的元素,從put方法的具體實現可知,會先調用hashCode方法得到該元素的hashCode值,然後查看table中是否存在該hashCode值,如果存在則調用equals方法重新確定是否存在該元素,如果存在,則更新value值,否則將新的元素添加到HashMap中。從這裏可以看出,hashCode方法的存在是爲了減少equals方法的調用次數,從而提高程序效率。
注意,HashMap在插入的時候,判斷的是key的值是否相同。
問題來了,如果我們沒有重寫hashCode方法,那麼即使對於兩個相同的對象,hashCode的結果也是不一樣的(例子1),那麼往HashMap中插入數據的時候,就會重複插入(注意,此時的Point並沒有實現hashCode方法),
Point p1 = new Point(2, 5);
Point p2 = new Point(2, 5);
HashMap<Point, String> hm = new HashMap<Point, String>();
hm.put(p1, "p1");
hm.put(p2, "p2");
System.out.println("HashMap size: " + hm.size());
System.out.println(hm.get(p1));
System.out.println(hm.get(p2));
運行程序,我們可以發現,HashMap的大小是2. p1和p2都可以從表中取出。
如果一個類是非可變的,並且計算hashCode的代價比較大,那麼應該考慮把hashCode緩存在對象內部,而不是每次都重新計算,如果對於該類的大多數對象都被用於散列鍵,那麼可以在實例被創建的時候就計算hashCode。否則的話,可以選擇遲緩初始化hashCode,一直到hashCode第一次使用才初始化。
對於前者,代碼可以如下,
private int hashCode;
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
/**
* 在此處初始化hashCode
*/
}
@Override
public int hashCode() {
return hashCode();
}
在對象初始化的時候,就計算hashCode,然後在hashCode()方法中,直接返回hashCode。
對於後者,代碼可以寫成這樣,
private int hashCode = -1;
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public int hashCode() {
if (hashCode() == -1) {
/**
*此處計算hashCode
*/
}
return hashCode();
}
這樣就可以保證hashcode只計算一次,防止多次調用hashcode帶來的大量的計算。