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


 

我在看HashMap源碼的時候發現了一個沒思考過的問題,在這次之前可以說是完全沒有思考過,所以一開始對這個點有疑問的時候,也沒有想到居然有這么個語法細節存在,弄得我百思不得其解,直到自己動手做實驗改寫了代碼才完全明白。

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

 

以上是大致的理解。在此基礎上,java的源碼:(這里我只用了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     }

 

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

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

在AbstractMap.class里面:

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里面找只找得到如下代碼:

 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引用,也就可以實現迭代。這種暴露一個內部類來實現外部訪問的方式我還真是第一次具體見到。

到這里我們就可以明白這整個過程了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM