java對象的比較
等號(==):
對比對象實例的內存地址(也即對象實例的ID),來判斷是否是同一對象實例;又可以說是判斷對象實例是否物理相等;
equals():
對比兩個對象實例是否相等。
當對象所屬的類沒有重寫根類Object的equals()方法時,equals()判斷的是對象實例的ID(內存地址),是否是同一對象實例;該方法就是使用的等號(==)的判斷結果,如Object類的源代碼所示:
public boolean equals(Object obj) {
return (this == obj);
}
當對象所屬的類重寫equals()方法(可能因爲需要自己特有的“邏輯相等”概念)時,equals()判斷的根據就因具體實現而異,有些類是需要比較對象的某些值或內容,如String類重寫equals()來判斷字符串的值是否相等。判斷邏輯相等。
hashCode():
計算出對象實例的哈希碼,並返回哈希碼,又稱爲散列函數。根類Object的hashCode()方法的計算依賴於對象實例的ID(內存地址),故每個Object對象的hashCode都是唯一的;當然,當對象所對應的類重寫了hashCode()方法時,結果就截然不同了。
二、Java的類爲什麼需要hashCode?---hashCode的作用,從Java中的集合的角度看。
總的來說,Java中的集合(Collection)有兩類,一類是List,再有一類是Set。你知道它們的區別嗎?前者集合內的元素是有序的,元素可以重複;後者元素無序,但元素不可重複。那麼這裏就有一個比較嚴重的問題了:要想保證元素不重複,可兩個元素是否重複應該依據什麼來判斷呢?這就是 Object.equals方法了。但是,如果每增加一個元素就檢查一次,那麼當元素很多時,後添加到集合中的元素比較的次數就非常多了。也就是說,如果集合中現在已經有1000個元素,那麼第1001個元素加入集合時,它就要調用1000次equals方法。這顯然會大大降低效率。
於是,Java採用了哈希表的原理。哈希算法也稱爲散列算法,當集合要添加新的元素時,將對象通過哈希算法計算得到哈希值(正整數),然後將哈希值和集合(數組)長度進行&運算,得到該對象在該數組存放的位置索引。如果這個位置上沒有元素,它就可以直接存儲在這個位置上,不用再進行任何比較了;如果這個位置上已經有元素了,就調用它的equals方法與新元素進行比較,相同的話就不存了,不相同就表示發生衝突了,散列表對於衝突有具體的解決辦法,但最終還會將新元素保存在適當的位置。
這樣一來,實際調用equals方法比較的次數就大大降低了,幾乎只需要一兩次。
簡而言之,在集合查找時,hashcode能大大降低對象比較次數,提高查找效率!
三、Java 對象的equal方法和hashCode方法的關係
首先,Java對象相同指的是兩個對象通過eqauls方法判斷的結果爲true
Java對象的eqauls方法和hashCode方法是這樣規定的:
1、相等(相同)的對象必須具有相等的哈希碼(或者散列碼)。
2、如果兩個對象的hashCode相同,它們並不一定相同。
關於第一點,
想象一下,假如兩個Java對象A和B,A和B相等(eqauls結果爲true),但A和B的哈希碼不同,則A和B存入HashMap時的哈希碼計算得到的HashMap內部數組位置索引可能不同,那麼A和B很有可能允許同時存入HashMap,顯然相等/相同的元素是不允許同時存入HashMap,HashMap不允許存放重複元素。
關於第二點,
也就是說,不同對象的hashCode可能相同;假如兩個Java對象A和B,A和B不相等(eqauls結果爲false),但A和B的哈希碼相等,將A和B都存入HashMap時會發生哈希衝突,也就是A和B存放在HashMap內部數組的位置索引相同這時HashMap會在該位置建立一個鏈接表,將A和B串起來放在該位置,顯然,該情況不違反HashMap的使用原則,是允許的。當然,哈希衝突越少越好,儘量採用好的哈希算法以避免哈希衝突。
四、深入解析HashMap類的底層數據結構
Map接口
Map沒有繼承Collection接口,Map提供key到value的映射。一個Map中不能包含相同的key,每個key只能映射一個 value。Map接口提供3種集合的視圖,Map的內容可以被當作一組key集合,一組value集合,或者一組key-value映射。
Hashtable類
Hashtable繼承Map接口,實現一個key-value映射的哈希表。任何非空(non-null)的對象都可作爲key或者value。添加數據使用put(key, value),取出數據使用get(key),這兩個基本操作的時間開銷爲常數。
Hashtable通過initial capacity和load factor兩個參數調整性能。通常缺省的load factor 0.75較好地實現了時間和空間的均衡。增大load factor可以節省空間但相應的查找時間將增大,這會影響像get和put這樣的操作。
使用Hashtable的簡單示例如下,將1,2,3放到Hashtable中,他們的key分別是”one”,”two”,”three”:
Hashtable numbers = new Hashtable();
numbers.put(“one”, new Integer(1));
numbers.put(“two”, new Integer(2));
numbers.put(“three”, new Integer(3));
要取出一個數,比如2,用相應的key:
Integer n = (Integer)numbers.get(“two”);
System.out.println(“two = ” + n);
HashMap
HashMap是基於哈希表的Map接口的非同步實現。此實現提供所有可選的映射操作,並允許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。
HashMap的數據結構:
HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體。首先,HashMap類的屬性中定義了Entry類型的數組。Entry類實現java.ultil.Map.Entry接口,同時每一對key和value是作爲Entry類的屬性被包裝在Entry的類中。
如圖所示,HashMap的數據結構:
HashMap的部分源碼如下:
transient Entry[] table;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
……
}
可以看出,HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個HashMap的時候,就會初始化一個數組。table數組的元素是Entry類型的。每個 Entry元素其實就是一個key-value對,並且它持有一個指向下一個 Entry元素的引用,這就說明table數組的每個Entry元素同時也作爲某個Entry鏈表的首節點,指向了該鏈表的下一個Entry元素,這就是所謂的“鏈表散列”數據結構,即數組和鏈表的結合體。
HashMap的存取實現:
1) 添加元素:
當我們往HashMap中put元素的時候,先根據key的重新計算元素的hashCode,根據hashCode得到這個元素在table數組中的位置(即下標),如果數組該位置上已經存放有其他元素了,那麼在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。
HashMap的部分源碼如下:
public V put(K key, V value) {
// HashMap允許存放null鍵和null值。
// 當key爲null時,調用putForNullKey方法,將value放置在數組第一個位置。
if (key == null)
return putForNullKey(value);
// 根據key的keyCode重新計算hash值。
int hash = hash(key.hashCode());
// 搜索指定hash值在對應table中的索引。
int i = indexFor(hash, table.length);
// 如果 i 索引處的 Entry 不爲 null,通過循環不斷遍歷 e 元素的下一個元素。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 如果發現 i 索引處的鏈表的某個Entry的hash和新Entry的hash相等且兩者的key相同,則新Entry覆蓋舊Entry,返回。
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 如果i索引處的Entry爲null,表明此處還沒有Entry。
modCount++;
// 將key、value添加到i索引處。
addEntry(hash, key, value, i);
return null;
2) 讀取元素:
有了上面存儲時的hash算法作爲基礎,理解起來這段代碼就很容易了。從上面的源代碼中可以看出:從HashMap中get元素時,首先計算key的hashCode,找到數組中對應位置的某一元素,然後通過key的equals方法在對應位置的鏈表中找到需要的元素。
HashMap的部分源碼如下:
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
3) 歸納起來簡單地說,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 對象。HashMap 底層採用一個 Entry[] 數組來保存所有的 key-value 對,當需要存儲一個 Entry 對象時,會根據hash算法來決定其在數組中的存儲位置,在根據equals方法決定其在該數組位置上的鏈表中的存儲位置;當需要取出一個Entry時,也會根據hash算法找到其在數組中的存儲位置,再根據equals方法從該位置上的鏈表中取出該Entry。
五、實現相等的對象必須具有相等的哈希碼
如果相同的對象有不同的hashCode,對哈希表的操作會出現意想不到的結果(期待的get方法返回null),要避免這種問題,只需要牢記一條:要同時複寫equals方法和hashCode方法,而不要只寫其中一個。
同時複寫equals方法和hashCode方法,必須保證“相等的對象必須具有相等的哈希碼”,也就是當兩個對象通過equals()比較的結果爲true時,這兩個對象調用hashCode()方法生成的哈希碼必須相等。
如何保證相等,可以參考下面的方法:
複寫equals方法和hashCode方法時,equals方法的判斷根據和計算hashCode的依據相同。如String的equals方法是比較字符串每個字符,String的hashCode也是通過對該字符串每個字符的ASC碼簡單的算術運算所得,這樣就可以保證相同的字符串的hashCode相同且equals()爲真。
String類的equals方法的源代碼:
/**
* Compares this string to the specified object. The result is {@code
* true} if and only if the argument is not {@code null} and is a {@code
* String} object that represents the same sequence of characters as this
* object.
*
* @param anObject
* The object to compare this {@code String} against
*
* @return {@code true} if the given object represents a {@code String}
* equivalent to this string, {@code false} otherwise
*
* @see #compareTo(String)
* @see #equalsIgnoreCase(String)
*/
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
}
String類的hashCode方法計算hashCode的源代碼:
/**
* Returns a hash code for this string. The hash code for a
* <code>String</code> object is computed as
* <blockquote><pre>
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* </pre></blockquote>
* using <code>int</code> arithmetic, where <code>s[i]</code> is the
* <i>i</i>th character of the string, <code>n</code> is the length of
* the string, and <code>^</code> indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/
public int hashCode() {
int h = hash;
int len = count;
if (h == 0 && len > 0) {
int off = offset;
char val[] = value;
for (int i = 0; i < len; i++) {
h = 31*h + val[off++];
}
hash = h;
}
return h;
}