实在没想到系列——HashMap实现底层细节之keySet,values,entrySet的一个底层实现细节

Posted 救赎之道就在其中

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实在没想到系列——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,它会认为自己是它。这个地方的细节待续。

 

以上是关于实在没想到系列——HashMap实现底层细节之keySet,values,entrySet的一个底层实现细节的主要内容,如果未能解决你的问题,请参考以下文章

Java集合之HashMap

高薪程序员&面试题精讲系列43之HashMap扩容机制的底层实现原理,HashMap扩容后是如何进行rehash操作的?

朗坤研发 | HashMap底层实现原理

java集合系列之HashMap源码

ListSet集合系列之剖析HashSet存储原理(HashMap底层)

java集合之HashMap