一、java對象的比較
等號(==):
對比對象實例的內存地址(也即對象實例的ID),來判斷是否是同一對象實例;又可以說是判斷對象實例是否物理相等;
equals():
對比兩個對象實例是否相等。
當對象所屬的類沒有重寫根類Object的equals()方法時,equals()判斷的是對象實例的ID(內存地址),是否是同一對象實例;該方法就是使用的等號(==)的判斷結果,如Object類的源代碼所示:
- public boolean equals(Object obj) {
- return (this == obj);
- }
當對象所屬的類重寫equals()方法(可能因爲需要自己特有的“邏輯相等”概念)時,equals()判斷的根據就因具體實現而異,有些類是需要比較對象的某些指或內容,如String類重寫equals()來判斷字符串的值是否相等。判斷邏輯相等。
hashCode():
計算出對象實例的哈希碼,並返回哈希碼,又稱爲散列函數。根類Object的hashCode()方法的計算依賴於對象實例的D(內存地址),故每個Object對象的hashCode都是唯一的;當然,當對象所對應的類重寫了hashCode()方法時,結果就截然不同了。
二、Java的類爲什麼需要hashCode?---hashCode的作用,從Java中的集合的角度看。
總的來說,Java中的集合(Collection)有兩類,一類是List,再有一類是Set。你知道它們的區別嗎?前者集合內的元素是有序的,元素可以重複;後者元素無序,但元素不可重複。那麼這裏就有一個比較嚴重的問題了:要想保證元素不重複,可兩個元素是否重複應該依據什麼來判斷呢?這就是 Object.equals方法了。但是,如果每增加一個元素就檢查一次,那麼當元素很多時,後添加到集合中的元素比較的次數就非常多了。也就是說,如果集合中現在已經有1000個元素,那麼第1001個元素加入集合時,它就要調用1000次equals方法。這顯然會大大降低效率。
於是,Java採用了哈希表的原理。哈希算法也稱爲散列算法,是將數據依特定算法直接指定到一個地址上。關於哈希算法,這裏就不詳細介紹。可以這樣簡單理解,hashCode方法實際上返回的就是對象存儲位置的映像。
這樣一來,當集合要添加新的元素時,先調用這個元素的hashCode方法,就能定位到它應該放置的存儲位置。如果這個位置上沒有元素,它就可以直接存儲在這個位置上,不用再進行任何比較了;如果這個位置上已經有元素了,就調用它的equals方法與新元素進行比較,相同的話就不存了,不相同就表示發生衝突了,散列表對於衝突有具體的解決辦法,但最終還會將新元素保存在適當的位置。這樣一來,實際調用equals方法的次數就大大降低了,幾乎只需要一兩次。
所以,Java對於eqauls方法和hashCode方法是這樣規定的:
1、相等的對象必須具有相等的哈希碼(或者散列碼)。
2、如果兩個對象的hashCode相同,它們並不一定相同。
上述的對象相同指的是通過eqauls方法判斷,結果爲true。
你當然可以不按要求去做了,但你會發現,相同的對象可以出現在Set集合中。同時,增加新元素的效率會大大下降。
三、深入解析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);
1. HashMap概述:
HashMap是基於哈希表的Map接口的非同步實現。此實現提供所有可選的映射操作,並允許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。
2. HashMap的數據結構:
HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體。首先,HashMap類的屬性中定義了Entry類型的數組。Entry類實現java.ultil.Map.Entry接口,同時每一對key和value是作爲Entry類的屬性被包裝在Entry的類中。
如圖所示,HashMap的數據結構:
HashMap的部分源碼如下:
- /**
- * The table, resized as necessary. Length MUST Always be a power of two.
- */
- 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元素,這就是所謂的“鏈表散列”數據結構,即數組和鏈表的結合體。
3. 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。
四、關於“相等的對象必須具有相等的哈希碼”
由於作爲key的對象將通過計算其散列函數hashCode()來確定與之對應的value的位置,因此任何作爲key的對象都必須實現hashCode和equals方法。hashCode和equals方法繼承自根類Object,如果你用自定義的類當作key的話,要相當小心,按照散列函數的定義,如果兩個對象相同,即obj1.equals(obj2)=true,則它們的hashCode必須相同,但如果兩個對象不同,則它們的hashCode不一定不同,如果兩個不同對象的hashCode相同,這種現象稱爲衝突,衝突會導致操作哈希表的時間開銷增大,所以儘量定義好的hashCode()方法,能加快哈希表的操作。
如果相同的對象有不同的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;
- }
來自:http://kakajw.iteye.com/blog/935226