技術問答集錦(一)

猿碼道發表於2017-12-28

1 hashCode和equals方法,在HashMap中如何使用?hashCode和equals方法之間的關係及為什麼?

Object的equals和hashCode方法。如下:

 // hashCode()方法預設是native方法;
 public native int hashCode();
 
 // equals(obj)預設比較的是記憶體地址;
 public boolean equals(Object obj) {
     return (this == obj);
 }
複製程式碼

hashCode()方法有三個關注點:

關注點1:主要是說這個hashCode方法對哪些類是有用的,並不是任何情況下都要使用這個方法(此時是根本沒有必要來複寫此方法),而是 當你涉及到像HashMap、HashSet(他們的內部實現中使用到了hashCode方法)等與hash有關的一些類時,才會使用到hashCode方法

關注點2:推薦按照這樣的原則來設計,即 當equals(object)相同時,hashCode()的返回值也要儘量相同,當equals(object)不相同時,hashCode()的返回沒有特別的要求,但是也是儘量不相同以獲取好的效能

關注點3:預設的hashCode實現一般是記憶體地址對應的數字,所以不同的物件,hashCode()的返回值是不一樣的。

在這種預設實施情況下,只有它們引用真正同一個物件時這兩個引用才是相等的。同樣,Object提供的hashCode()的預設實施通過將物件的記憶體地址對映於一個整數值來生成。

接下來HashMap的原始碼分析hashCode和equals方法使用過程:

 static final Entry<?,?>[] EMPTY_TABLE = {}; 
 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
複製程式碼

HashMap內部是由Entry<K,V>型別的陣列table來儲存資料的。來看下Entry<K,V>的程式碼:

 static class Entry<K,V> implements Map.Entry<K,V> { 
     final K key; 
     V value; 
     Entry<K,V> next; 
     // key的hashCode方法的返回值經過hash運算得到的值
     int hash; 

     /** * Creates new entry. */ 
     Entry(int h, K k, V v, Entry<K,V> n) { 
         value = v; 
         next = n; 
         key = k; 
         hash = h; 
     }
     //略
 }
複製程式碼

所以我們可以畫出HashMap的儲存結構:

HashMap儲存結構

圖中的每一個方格就表示一個Entry<K,V>物件,其中的橫向則構成一個Entry<K,V>[] table陣列,而豎向則是由Entry<K,V>的next屬性形成的連結串列。

首先看下它HashMap是如何來新增的,即 put(K key, V value)方法:

 public V put(K key, V value) { 
     if (table == EMPTY_TABLE) { 
         inflateTable(threshold);
     } 
     if (key == null) return putForNullKey(value); 
     // 位置[1]
     int hash = hash(key);
     // 位置[2]
     int i = indexFor(hash, table.length); 
     for (Entry<K,V> e = table[i]; e != null; e = e.next) { 
         Object k; 
         // 位置[4] 遍歷連結串列,若連結串列已存在一致的物件,則替換
         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; 
 }
複製程式碼

重點關注它的存的過程,位置[1]首先就是計算key的hash值,這個hash計算的過程位置[3]便用到了key物件的hashCode方法,如下:

 final int hash(Object k) { 
     int h = hashSeed; 
     if (0 != h && k instanceof String) { 
         return sun.misc.Hashing.stringHash32((String) k); 
     } 
     // 位置[3]
     h ^= k.hashCode(); 

     h ^= (h >>> 20) ^ (h >>> 12); 
     return h ^ (h >>> 7) ^ (h >>> 4); 
 }
複製程式碼

得到這個hash值後,位置[2]緊接著執行了int i = indexFor(hash, table.length);就是找到這個hash值在table陣列中的索引值,具體方法indexFor(hash, table.length)為:

 static int indexFor(int h, int length) { 
     return h & (length-1); 
 }
複製程式碼

**位置[4]判斷是否一致的條件是:**e.hash == hash && ((k = e.key) == key || key.equals(k)),一定要牢牢記住這個條件。

**必須滿足的條件1:**hash值一樣,hash值的來歷就是根據key的hashCode再進行一個複雜的運算,當兩個key的hashCode一致的時候,計算出來的hash也是必然一樣的。

**必須滿足的條件2:**兩個key的引用一樣或者equals相同。

綜上所述,HashMap對於key的重複性判斷是基於兩個內容的判斷,一個就是hash值是否一樣(會演變成key的hashCode是否一樣),另一個就是equals方法是否一樣(引用一樣則肯定一樣)

所以,hashCode的重寫的原則:當equals方法返回true,則兩個物件的hashCode必須一樣

equals()方法,在get()方法中的使用過程分析:

 public V get(Object key) {    
     Node<K,V> e;
     return (e = getNode(hash(key), key)) == null ? null : e.value;
 }
 
 final Node<K,V> getNode(int hash, Object key) {
     Node<K,V>[] tab; 
     Node<K,V> first, e; 
     int n; K k;    
     if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
         // 元素相等判斷
         if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
             return first;        
         if ((e = first.next) != null) {            
             if (first instanceof TreeNode)                
                 return ((TreeNode<K,V>)first).getTreeNode(hash, key);                 
             do { 
                 // 連結串列上的元素相等判斷           
                 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                     return e;            
             } while ((e = e.next) != null);
         }
     }
     return null;
 }
複製程式碼

為什麼 Override equals()和hashCode()?

由於作為key的物件將通過計算其hashCode來確定與之對應的value的位置,因此任何作為key的物件都必須實現 hashCode和equals方法。hashCode和equals方法繼承自根類Object,如果你用自定義的類當作key的話,要相當小心,按照雜湊函式的定義,如果兩個物件相同,即obj1.equals(obj2)=true,則它們的hashCode必須相同,但如果兩個物件不同,則它們的 hashCode不一定不同,如果兩個不同物件的hashCode相同,這種現象稱為衝突,衝突會導致操作雜湊表的時間開銷增大,hashCode()方法目的純粹用於提高效率,所以儘量定義好的 hashCode()方法,能加快雜湊表的操作。

如果相同的物件有不同的hashCode,對雜湊表的操作會出現意想不到的結果(期待的get方法返回null),要避免這種問題,只需要牢記一條:要同時複寫equals方法和hashCode方法,而不要只寫其中一個

hashcode與equals方法使用:http://my.oschina.net/xianggao/blog/90110

2 記憶體洩漏與記憶體溢位的區別?

Java虛擬機器在執行Java程式的過程中把它所管理的記憶體劃分為若干個不同的資料區域。這些區域都有各自的用途,以及建立和銷燬的時間,有的區域隨著虛擬機器程式的啟動而存在,有些區域則依賴使用者執行緒的啟動和結束而建立和銷燬。如下圖所示:

JVM執行時資料區域

虛擬機器棧在執行時使用一種叫做棧幀的資料結構儲存上下文資料。在棧幀中,存放了方法的區域性變數表,運算元棧,動態連線方法和返回地址等資訊。每一個方法的呼叫都伴隨著棧幀的入棧操作。相應地,方法的返回則表示棧幀的出棧操作。如果方法呼叫時,方法的引數和區域性變數相對較多,那麼棧幀中的區域性變數表就會比較大,棧幀會膨脹以滿足方法呼叫所需傳遞的資訊。因此,單個方法呼叫所需的棧空間大小也會比較多。

虛擬機器棧結構

注意:函式巢狀呼叫的次數由棧的大小決定。棧越大,函式巢狀呼叫次數越多。對一個函式而言,它的引數越多,內部區域性變數越多,它的棧幀就越大,其巢狀呼叫次數就會越少。

可達性分析:Java中對記憶體物件的訪問,使用的是引用的方式。在Java程式碼中我們維護一個記憶體物件的引用變數,通過這個引用變數的值,我們可以訪問到對應的記憶體地址中的記憶體物件空間。在Java程式中,這個引用變數本身既可以存放堆記憶體中,又可以放在程式碼棧的記憶體中(與基本資料型別相同)。GC執行緒會從程式碼棧中的引用變數開始跟蹤,從而判定哪些記憶體是正在使用的。如果GC執行緒通過這種方式,無法跟蹤到某一塊堆記憶體,那麼GC就認為這塊記憶體將不再使用了(因為程式碼中已經無法訪問這塊記憶體了)。

可達性分析

通過這種有向圖的記憶體管理方式,當一個記憶體物件失去了所有的引用之後,GC 就可以將其回收。反過來說,如果這個物件還存在引用,那麼它將不會被GC回收,哪怕是Java虛擬機器丟擲OutOfMemoryError。

Java中的記憶體洩漏,主要指的是是在記憶體物件明明已經不需要的時候,還仍然保留著這塊記憶體和它的訪問方式(引用)

在不涉及複雜資料結構的一般情況下,Java的記憶體洩露表現為一個記憶體物件的生命週期超出了程式需要它的時間長度。我們有時也將其稱為“物件遊離”。

3 深入HashMap,及併發讀寫情況下死迴圈問題?

深入JDK原始碼之HashMap類:http://my.oschina.net/xianggao/blog/386697

HashMap多執行緒併發問題分析:http://my.oschina.net/xianggao/blog/393990

ConcurrentHashMap深入分析:http://my.oschina.net/xianggao/blog/212060

HashMap vs ConcurrentHashMap:http://my.oschina.net/xianggao/blog/394213

相關文章