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

时间:2023-02-08 13:21:44

我在看HashMap源码的时候发现了一个没思考过的问题,在这次之前可以说是完全没有思考过,所以一开始对这个点有疑问的时候,也没有想到居然有这么个语法细节存在,弄得我百思不得其解,直到自己动手做实验改写了代码才完全明白。

HashMap里面保存的数据最底层是一个Entry型的数组,这个Entry则保留了一个键值对,还有一个指向下一个Entry的指针。所以HashMap是一种结合了数组和链表的结构。正因为如此,你有3种对数据的观测方式:keySet,values,entrySet。第一个是体现从key的值角度出发的结果。它里面包含了这个键值对表里面的所有键的值的集合,因为HashMap明确规定一个键只能对应一个值,所以不会有重复的key存在,这也就是为什么可以用集合来装key。第二个values则是从键值对的值的角度看这个映射表,因为可以有多个key对应一个值,所以可能有多个相同的values。(这个观点和函数的观点相似)第三个角度是最基本的角度,也就是从键值对的角度思考这个问题。它返回一个键值对的集合。(键值对相等当且仅当键和值都相等)。

以上是大致的理解。在此基础上,java的源码:(这里我只用了keySet说明这个问题)

     public Set<K> keySet() {
Set<K> ks = keySet;
return (ks != null ? ks : (keySet = new KeySet()));
} private final class KeySet extends AbstractSet<K> {
public Iterator<K> iterator() {
return newKeyIterator();
}
public int size() {
return size;
}
public boolean contains(Object o) {
return containsKey(o);
}
public boolean remove(Object o) {
return HashMap.this.removeEntryForKey(o) != null;
}
public void clear() {
HashMap.this.clear();
}
}

看上去简单明了,可是我发现了一个细节并且与之纠缠了一个下午(这个语法细节隐藏的很深)。

这个地方我们可以看到,当向一个HashMap调用keySet()方法的时候就是返回一个集合,其内容是所有的key的值。可是问题是这个地方到底是怎么实现的。从代码可以看到这个地方直接返回了一个叫keySet的东西。那么这个东西究竟是什么呢?按住command键可以直接去看这个变量声明的地方:

在AbstractMap.class里面:

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

也就是说,这个地方是从HashMap的父类AbstractMap里面继承过来的两个集合类型(第一个就是我说的keySet,第二个和这个完全一样的过程)。

可是问题还是没有解决,这个keySet为什么能返回当前HashMap的key的值得集合呢?我一开始只是抱着“简单看看”的想法来看这个地方,因为我的想象是可能能在哪里找到一个显而易见的同步方法,使得keySet的里面的值随着table(这也就是那个基础数组,储存了所有的键值对Entry)的值变化而变化。可是我发现:“没有”。

第一时间我觉得我可能没有找对位置,因为一般它提供的这些类的继承关系比较复杂,可能不在这个地方,可能在别的地方实现了,可是我翻来覆去找半天确实发现没有,也就是说:“没有明确的代码让keySet同步HashMap”。这下问题就变大了,事实上如果你在AbstractMap里面找只找得到如下代码:

     public Set<K> keySet() {
if (keySet == null) {
keySet = new AbstractSet<K>() {
public Iterator<K> iterator() {
return new Iterator<K>() {
private Iterator<Entry<K,V>> i = entrySet().iterator(); public boolean hasNext() {
return i.hasNext();
} public K next() {
return i.next().getKey();
} public void remove() {
i.remove();
}
};
} public int size() {
return AbstractMap.this.size();
} public boolean isEmpty() {
return AbstractMap.this.isEmpty();
} public void clear() {
AbstractMap.this.clear();
} public boolean contains(Object k) {
return AbstractMap.this.containsKey(k);
}
};
}
return keySet;
}

看上去完全不是一个同步过程,至少在我的理解中把一个容器的东西搬运到另外一个容器需要用循环把东西一个一个搬运过去,哪怕只是浅拷贝把指针的值丢过去。这一节代码怎么看都和“让keySet这个set持有table里面的key的值的集合”没有任何关系。但是确确实实是这个地方实现了同步。

看如下代码:

 public class Main {

     public static void main(String[] args) {

         testIterator t = new testIterator();
Set<Integer> set = t.keySet();
System.out.println(set); }
} class testIterator {
public Set<Integer> keySet() { final ArrayList<Integer> result = new ArrayList<Integer>();
result.add(1);
result.add(2);
result.add(3); Set<Integer> keySet = new AbstractSet<Integer>() {
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
private Iterator<Integer> i = result.iterator(); @Override
public boolean hasNext() {
return i.hasNext();
} @Override
public Integer next() {
return i.next();
} @Override
public void remove() {
i.remove();
}
};
} @Override
public int size() {
return 0;
}
}; return keySet;
}
}

这个地方的结果是:

[1, 2, 3]

为什么呢?这个地方的代码是按照HashMap的代码改写的,我再改写一下如下所示:

 public class Main {

     public static void main(String[] args) {
ArrayList<Integer> array = new ArrayList<Integer>();
array.add(1);
array.add(2);
array.add(3); mySet set = new mySet(array.iterator());
System.out.println(set);
} } class mySet extends AbstractSet<Integer> { private Iterator<Integer> iter; public mySet(Iterator<Integer> i) {
iter = i;
} @Override
public Iterator<Integer> iterator() {
return iter;
} @Override
public int size() {
return 0;
} }

也是一样的效果。换句话说,直接让一个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()方法的时候:

 public Set<K> keySet() {
Set<K> ks = keySet;
return (ks != null ? ks : (keySet = new KeySet()));
} private final class KeySet extends AbstractSet<K> {
public Iterator<K> iterator() {
return newKeyIterator();
}
public int size() {
return size;
}
public boolean contains(Object o) {
return containsKey(o);
}
public boolean remove(Object o) {
return HashMap.this.removeEntryForKey(o) != null;
}
public void clear() {
HashMap.this.clear();
}
}

可见:会返回一个名字叫keySet的Set。但是这个keySet如上面所写的是来自AbstractMap的一个引用。我前面思路错的原因是因为我一直认为需要去AbstractMap里面找它的具体实现,其实不是的。这个ks的第一次初始化就反映了问题的本质是通过引用。看它的初始化过程:返回了一个“newKeyIterator();”对象。那么这个对象是什么呢?

再往前的代码:

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

它调用了一个方法返回了一个 KeyIterator 对象。这个对象的代码如图所示:

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

它又基础自HashIterator。看上去这个过程比较复杂,其实看源代码的话可以很清楚它的意图:keySet和values和entrySet本质既然一样,就可以通过封装其相同的部分(也就是这里的HashIterator),再各自实现最重要的next方法。

这是HashIterator的源代码:

     private abstract class HashIterator<E> implements Iterator<E> {
Entry<K,V> next; // next entry to return
int expectedModCount; // For fast-fail
int index; // current slot
Entry<K,V> current; // current entry HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
} public final boolean hasNext() {
return next != null;
} final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException(); if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}

可见,对于迭代器的操作,其实都是根据底层的table来实现的,也就是直接操作键值对。在得到Entry之后再获得它的key或者value。正因为如此,迭代器的底层直接根据table进行操作,所以如果有别的容器持有了这个迭代器内部类,就可以直接实现同步中的可见性:对HashMap的改变体现在table,而传递出去的内部类可以访问table。

而这之所以可以实现的更底层一步的地方是迭代器的具体实现。一方面它是一个内部类可以直接访问HashMap的table,另外一个方面是它用了类似指针的next引用,也就可以实现迭代。这种暴露一个内部类来实现外部访问的方式我还真是第一次具体见到。

到这里我们就可以明白这整个过程了。