我在看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引用,也就可以实现迭代。这种暴露一个内部类来实现外部访问的方式我还真是第一次具体见到。

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

实在没想到系列——HashMap实现底层细节之keySet,values,entrySet的一个底层实现细节的更多相关文章

  1. 【java】TreeMap/HashMap的循环迭代中 keySet和entrySet和forEach方式 + map的几种迭代方式

    参考链接:https://www.cnblogs.com/crazyacking/p/5573528.html ================================== java紫色代表迭 ...

  2. Java基础系列--HashMap(JDK1.8)

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/10022092.html Java基础系列-HashMap 1.8 概述 HashMap是 ...

  3. 刨死你系列——HashMap(jdk1.8)

    本文的源码是基于JDK1.8版本,在学习HashMap之前,先了解数组和链表的知识. 数组:数组具有遍历快,增删慢的特点.数组在堆中是一块连续的存储空间,遍历时数组的首地址是知道的(首地址=首地址+元 ...

  4. 没想到吧!这个可可爱爱的游戏居然是用 ECharts 实现的!

    摘要:echarts 是一个很强大的图表库,除了我们常见的图表功能,还可以自定义图形,这个功能让我们可以很简单地在画布上绘制一些非常规的图形,基于此,我们来玩一些花哨的:做一个 Flappy Bird ...

  5. 在做关于NIO TCP编程小案例时遇到无法监听write的问题,没想到只是我的if语句的位置放错了位置,哎,看了半天没看出来

    在做关于NIO TCP编程小案例时遇到无法监听write的问题,没想到只是我的if语句的位置放错了位置,哎,看了半天没看出来 贴下课堂笔记: 在Java中使用NIO进行网络TCP套接字编程主要以下几个 ...

  6. centos clamav杀毒软件安装配置及查杀,没想到linux下病毒比windows还多!

    centos clamav杀毒软件安装配置及查杀,没想到linux下病毒比windows还多! 一.手动安装 1.下载(官网)    cd /soft     wget http://www.clam ...

  7. 【原创】这道Java基础题真的有坑!我也没想到还有续集。

    前情回顾 自从我上次发了<这道Java基础题真的有坑!我求求你,认真思考后再回答.>这篇文章后.我通过这样的一个行文结构: 解析了小马哥出的这道题,让大家明白了这题的坑在哪里,这题背后隐藏 ...

  8. 头条编程题 万万没想到之抓捕孔连顺 JavaScript

    [编程题] 万万没想到之抓捕孔连顺 时间限制:1秒 空间限制:131072K 我叫王大锤,是一名特工.我刚刚接到任务:在字节跳动大街进行埋伏,抓捕恐怖分子孔连顺.和我一起行动的还有另外两名特工,我提议 ...

  9. 杀死众筹的N种方法:没想到山寨大军也参与了

    ​ ​ 众筹作为当下创业者筹集资金,将创意变为现实的最重要手段之一,正面临着越来越多的困难,甚至衍生出杀死众筹的N种方法.甚至这些方法还分为了两类,就众筹本身看,杀死它们的主要方法是:创业者卷钱跑路. ...

随机推荐

  1. git删除远程仓库的文件或目录

    git rm -r --cached a/2.txt //删除a目录下的2.txt文件   删除a目录git rm -r --cached a git commit -m "删除a目录下的2 ...

  2. 在实例中说明java的类变量,成员变量和局部变量

    java中一般有三种变量:类变量,成员变量和局部变量.类变量 1.下面先看类变量,看下面这个例子 public class Demo6{ public String name; public int ...

  3. Debian 8.2 下安装MySQL5.7.9 Generic Binaries

    安装过程参考了Installing MySQL on Unix/Linux Using Generic Binaries 首先检查是否安装libaio shell> apt-cache sear ...

  4. PAT 1030. 完美数列(25)

    给定一个正整数数列,和正整数p,设这个数列中的最大值是M,最小值是m,如果M <= m * p,则称这个数列是完美数列. 现在给定参数p和一些正整数,请你从中选择尽可能多的数构成一个完美数列. ...

  5. java多线程系类:基础篇:04synchronized关键字

    概要 本章,会对synchronized关键字进行介绍.涉及到的内容包括:1. synchronized原理2. synchronized基本规则3. synchronized方法 和 synchro ...

  6. iTextSharp带中文转换出来的PDF文档显示乱码

    刚才有写一个小练习<Html代码保存为Pdf文件>http://www.cnblogs.com/insus/p/4323224.html.马上有网友说,当截取块有中文时,保存的pdf文件将 ...

  7. logstash搭建日志追踪系统

    前言 开始博客之前,首先说下10月份没写博客的原因 = =. 10月份赶上国庆,回了趟老家休息了下,回来后自己工作内容发生了点改变,开始搞一些小架构的东西以及研究一些新鲜东西,当时我听到这个消息真的是 ...

  8. Laravel 下结合阿里云邮件推送服务

    最近在学习laravel做项目开发,遇到注册用户推送邮件的问题,之前用java做的时候是自己代码写的,也就是用ECS推送邮件,但是现在转php的laravel了就打算用php的邮件发送功能来推送邮件, ...

  9. 从炉石传说的一个自杀OTK说起

    OTK就是one turn kill,不过这次我们要谈的OTK是自杀,对就是自己把自己给OTK了. 其实程序没有任何错误,只是恰巧碰上了这么个死循环. ps:文章最后有代码git地址 发动条件及效果: ...

  10. 发布园友设计的新款博客皮肤BlueSky

    园友#a为大家设计了一款“简单.纯粹,一点淡雅,一点宁静”的博客皮肤——BlueSky,欢迎您的享用!感谢#a的精心设计! 如果您有兴趣为大家设计博客皮肤,请将您设计的html/css/images文 ...