實在沒想到系列——HashMap實現底層細節之keySet,values,entrySet的一個底層實現細節

 

我在看HashMap源碼的時候發現了一個沒思考過的問題,在此次以前能夠說是徹底沒有思考過,因此一開始對這個點有疑問的時候,也沒有想到竟然有這麼個語法細節存在,弄得我百思不得其解,直到本身動手作實驗改寫了代碼才徹底明白。java

HashMap裏面保存的數據最底層是一個Entry型的數組,這個Entry則保留了一個鍵值對,還有一個指向下一個Entry的指針。因此HashMap是一種結合了數組和鏈表的結構。正由於如此,你有3種對數據的觀測方式:keySet,values,entrySet。第一個是體現從key的值角度出發的結果。它裏面包含了這個鍵值對錶裏面的全部鍵的值的集合,由於HashMap明確規定一個鍵只能對應一個值,因此不會有重複的key存在,這也就是爲何能夠用集合來裝key。第二個values則是從鍵值對的值的角度看這個映射表,由於能夠有多個key對應一個值,因此可能有多個相同的values。(這個觀點和函數的觀點類似)第三個角度是最基本的角度,也就是從鍵值對的角度思考這個問題。它返回一個鍵值對的集合。(鍵值對相等當且僅當鍵和值都相等)。數組

 

以上是大體的理解。在此基礎上,java的源碼:(這裏我只用了keySet說明這個問題)ide

 

 1     public Set<K> keySet() {
 2         Set<K> ks = keySet;
 3         return (ks != null ? ks : (keySet = new KeySet()));
 4     }
 5 
 6     private final class KeySet extends AbstractSet<K> {
 7         public Iterator<K> iterator() {
 8             return newKeyIterator();
 9         }
10         public int size() {
11             return size;
12         }
13         public boolean contains(Object o) {
14             return containsKey(o);
15         }
16         public boolean remove(Object o) {
17             return HashMap.this.removeEntryForKey(o) != null;
18         }
19         public void clear() {
20             HashMap.this.clear();
21         }
22     }

 

看上去簡單明瞭,但是我發現了一個細節而且與之糾纏了一個下午(這個語法細節隱藏的很深)。函數

這個地方咱們能夠看到,當向一個HashMap調用keySet()方法的時候就是返回一個集合,其內容是全部的key的值。但是問題是這個地方究竟是怎麼實現的。從代碼能夠看到這個地方直接返回了一個叫keySet的東西。那麼這個東西到底是什麼呢?按住command鍵能夠直接去看這個變量聲明的地方:this

在AbstractMap.class裏面:spa

1     transient volatile Set<K>        keySet = null;
2     transient volatile Collection<V> values = null;

也就是說,這個地方是從HashMap的父類AbstractMap裏面繼承過來的兩個集合類型(第一個就是我說的keySet,第二個和這個徹底同樣的過程)。指針

但是問題仍是沒有解決,這個keySet爲何能返回當前HashMap的key的值得集合呢?我一開始只是抱着「簡單看看」的想法來看這個地方,由於個人想象是可能能在哪裏找到一個顯而易見的同步方法,使得keySet的裏面的值隨着table(這也就是那個基礎數組,儲存了全部的鍵值對Entry)的值變化而變化。但是我發現:「沒有」。調試

第一時間我以爲我可能沒有找對位置,由於通常它提供的這些類的繼承關係比較複雜,可能不在這個地方,可能在別的地方實現了,但是我翻來覆去找半天確實發現沒有,也就是說:「沒有明確的代碼讓keySet同步HashMap」。這下問題就變大了,事實上若是你在AbstractMap裏面找只找獲得以下代碼:code

 1     public Set<K> keySet() {
 2         if (keySet == null) {
 3             keySet = new AbstractSet<K>() {
 4                 public Iterator<K> iterator() {
 5                     return new Iterator<K>() {
 6                         private Iterator<Entry<K,V>> i = entrySet().iterator();
 7 
 8                         public boolean hasNext() {
 9                             return i.hasNext();
10                         }
11 
12                         public K next() {
13                             return i.next().getKey();
14                         }
15 
16                         public void remove() {
17                             i.remove();
18                         }
19                     };
20                 }
21 
22                 public int size() {
23                     return AbstractMap.this.size();
24                 }
25 
26                 public boolean isEmpty() {
27                     return AbstractMap.this.isEmpty();
28                 }
29 
30                 public void clear() {
31                     AbstractMap.this.clear();
32                 }
33 
34                 public boolean contains(Object k) {
35                     return AbstractMap.this.containsKey(k);
36                 }
37             };
38         }
39         return keySet;
40     }

看上去徹底不是一個同步過程,至少在個人理解中把一個容器的東西搬運到另一個容器須要用循環把東西一個一個搬運過去,哪怕只是淺拷貝把指針的值丟過去。這一節代碼怎麼看都和「讓keySet這個set持有table裏面的key的值的集合」沒有任何關係。可是確確實實是這個地方實現了同步。對象

看以下代碼:

 1 public class Main {
 2 
 3     public static void main(String[] args) {
 4 
 5         testIterator t = new testIterator();
 6         Set<Integer> set = t.keySet();
 7         System.out.println(set);
 8 
 9     }
10 }
11 
12 
13 class testIterator {
14     public Set<Integer> keySet() {
15 
16         final ArrayList<Integer> result = new ArrayList<Integer>();
17         result.add(1);
18         result.add(2);
19         result.add(3);
20 
21         Set<Integer> keySet = new AbstractSet<Integer>() {
22             public Iterator<Integer> iterator() {
23                 return new Iterator<Integer>() {
24                     private Iterator<Integer> i = result.iterator();
25 
26                     @Override
27                     public boolean hasNext() {
28                         return i.hasNext();
29                     }
30 
31                     @Override
32                     public Integer next() {
33                         return i.next();
34                     }
35 
36                     @Override
37                     public void remove() {
38                         i.remove();
39                     }
40                 };
41             }
42 
43             @Override
44             public int size() {
45                 return 0;
46             }
47         };
48 
49         return keySet;
50     }
51 }

這個地方的結果是:

[1, 2, 3]

 

爲何呢?這個地方的代碼是按照HashMap的代碼改寫的,我再改寫一下以下所示:

 

 1 public class Main {
 2 
 3     public static void main(String[] args) {
 4         ArrayList<Integer> array = new ArrayList<Integer>();
 5         array.add(1);
 6         array.add(2);
 7         array.add(3);
 8         
 9         mySet set = new mySet(array.iterator());
10         System.out.println(set);
11     }
12 
13 }
14 
15 class mySet extends AbstractSet<Integer> {
16 
17     private Iterator<Integer> iter;
18 
19     public mySet(Iterator<Integer> i) {
20         iter = i;
21     }
22 
23     @Override
24     public Iterator<Integer> iterator() {
25         return iter;
26     }
27 
28     @Override
29     public int size() {
30         return 0;
31     }
32 
33 }

也是同樣的效果。換句話說,直接讓一個set它持有一個別人的Iterrator,它會認爲本身是它。同時若是調試運行會發現set的值真的變了。同時這麼作是有問題的,調試運行的結果和直接運行不同同時再加上一句:System.out.println(set); 會發現第一次打印了1,2,3,第二次爲null。換句話說這樣的代碼產生了不肯定的行爲。可是這代碼能夠說明一些問題,至少表示離問題近了。

 

到目前爲止,能夠知道keySet返回的並非個「新」的東西,因此也沒有把HashMap裏面的key的值一個一個放到set的這個過程,而是經過生成一個set,這個set直接和HashMap的Iterator掛鉤來反映HashMap的變化。這個地方的「掛鉤」的具體過程是keySet繼承了AbstractSet這個抽象類,這個抽象類須要重寫iterator() 方法。

 

具體的代碼調用過程以下:

當你調用HashMap的keySet()方法的時候:

 1 public Set<K> keySet() {
 2         Set<K> ks = keySet;
 3         return (ks != null ? ks : (keySet = new KeySet()));
 4     }
 5 
 6     private final class KeySet extends AbstractSet<K> {
 7         public Iterator<K> iterator() {
 8             return newKeyIterator();
 9         }
10         public int size() {
11             return size;
12         }
13         public boolean contains(Object o) {
14             return containsKey(o);
15         }
16         public boolean remove(Object o) {
17             return HashMap.this.removeEntryForKey(o) != null;
18         }
19         public void clear() {
20             HashMap.this.clear();
21         }
22     }

可見:會返回一個名字叫keySet的Set。可是這個keySet如上面所寫的是來自AbstractMap的一個引用。我前面思路錯的緣由是由於我一直認爲須要去AbstractMap裏面找它的具體實現,其實不是的。這個ks的第一次初始化就反映了問題的本質是經過引用。看它的初始化過程:返回了一個「newKeyIterator();」對象。那麼這個對象是什麼呢?

再往前的代碼:

1     Iterator<K> newKeyIterator()   {
2         return new KeyIterator();
3     }

 

它調用了一個方法返回了一個 KeyIterator 對象。這個對象的代碼如圖所示:

1     private final class KeyIterator extends HashIterator<K> {
2         public K next() {
3             return nextEntry().getKey();
4         }
5     }

它又基礎自HashIterator。看上去這個過程比較複雜,其實看源代碼的話能夠很清楚它的意圖:keySet和values和entrySet本質既然同樣,就能夠經過封裝其相同的部分(也就是這裏的HashIterator),再各自實現最重要的next方法。

這是HashIterator的源代碼:

 1     private abstract class HashIterator<E> implements Iterator<E> {
 2         Entry<K,V> next;        // next entry to return
 3         int expectedModCount;   // For fast-fail
 4         int index;              // current slot
 5         Entry<K,V> current;     // current entry
 6 
 7         HashIterator() {
 8             expectedModCount = modCount;
 9             if (size > 0) { // advance to first entry
10                 Entry[] t = table;
11                 while (index < t.length && (next = t[index++]) == null)
12                     ;
13             }
14         }
15 
16         public final boolean hasNext() {
17             return next != null;
18         }
19 
20         final Entry<K,V> nextEntry() {
21             if (modCount != expectedModCount)
22                 throw new ConcurrentModificationException();
23             Entry<K,V> e = next;
24             if (e == null)
25                 throw new NoSuchElementException();
26 
27             if ((next = e.next) == null) {
28                 Entry[] t = table;
29                 while (index < t.length && (next = t[index++]) == null)
30                     ;
31             }
32             current = e;
33             return e;
34         }

 

可見,對於迭代器的操做,其實都是根據底層的table來實現的,也就是直接操做鍵值對。在獲得Entry以後再得到它的key或者value。正由於如此,迭代器的底層直接根據table進行操做,因此若是有別的容器持有了這個迭代器內部類,就能夠直接實現同步中的可見性:對HashMap的改變體如今table,而傳遞出去的內部類能夠訪問table。

而這之因此能夠實現的更底層一步的地方是迭代器的具體實現。一方面它是一個內部類能夠直接訪問HashMap的table,另一個方面是它用了相似指針的next引用,也就能夠實現迭代。這種暴露一個內部類來實現外部訪問的方式我還真是第一次具體見到。

到這裏咱們就能夠明白這整個過程了。

相關文章
相關標籤/搜索