前几天在一个网站上回答问题,虽然是平时经常遇到的,但有很多点都被人模糊掉或者用一些专业名词盖掉。笔者觉得这个问题很不错,是一种学习思路,于是回答后并将自己的答案记录下来,如下:
1. if ((tab = table) == null || (n = tab.length) == 0)
2. n = (tab = resize()).length;
3. if ((p = tab[i = (n - 1) & hash]) == null)
4. tab[i] = newNode(hash, key, value, null);
5. else {
6. Node e; K k;
7. if (p.hash == hash &&
8. ((k = p.key) == key || (key != null && key.equals(k))))
9. e = p;
10. else if (p instanceof TreeNode)
11. e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
12. else {
13. for (int binCount = 0; ; ++binCount) {
14. if ((e = p.next) == null) {
15. p.next = newNode(hash, key, value, null);
16. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
17. treeifyBin(tab, hash);
18. break;
19. }
20. if (e.hash == hash &&
21. ((k = e.key) == key || (key != null && key.equals(k))))
22. break;
23. p = e;
24. }
25. }
解读这一段源码:
0. java1.8中,map的结构是由Node[] + 链表组成的,和之前的主要区别是,链表部分,当超过8个元素会转成TreeNode对象,由TreeBin封装,也就是链表转换为树形结构便于存储和查找。不是线程安全类,但是如果多线程操作会报出ConCurrentxxxException。
下面按行数来说:
1. 如果当前Node[] 为null 或者 Node数组元素个数为0 执行下面一行代码(省略大括号)
2. resize() 方法返回一个扩容后的Node[],扩容的机制为所需容量的两倍,是原有的1.5倍,这个计算是由负载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; 来决定的(具体的可以看一下resize()方法,如果有困难我们再讨论一次)。 Node数组元素个数n 赋值为扩容后的长度。
4. 「 (n - 1) & hash] 」定位数组下标位置的经典算法,你可以自己写一个main方法来做几个实验。n=16的话,计算结果肯定在0~15之间。p为计算后取到的一个Node节点 判断是否为null (if else 省略大括号)
5. 如果上述判断为true,那么当前节点赋值为一个新建节点 newNode(x,x,x,x); 这个不用说吧。也是一个经典数据结构。
6. 如果4 中判断结果为false,证明当前下标为i的位置存在节点,那么进行下列操作。
7、8、9. 如果当前node节点的hash 和 节点中key属性值 双等或值等(也就是进行新旧值替换),则将当前节点赋值给e。
10、11. 如果当前节点为TreeNode节点,则将当前操作元素添加到TreeNode[]中(图中,单独的hash,key,value都是当前操作元素的值)。
12. 如果上述都不成立,直接执行下列代码(到这步的意思是,不会是覆盖操作,当前节点也不是treeNode,只能是将当前节点拼接在列表末端)。
13-23. 这是一个看似无限循环的算法,这有几个退出点,按顺序来,第一个break,代表找到了列表末端,把当前元素添加到末端即可。第二个break,是找到新旧值覆盖的元素,进行替换。
14行为常规列表遍历。
15行为如果找到最后一个元素了,直接新建元素,将最后一个元素与新元素逻辑关联。
16行 判断当前个数,是否大于或者等于7,如果是,则将列表转化为TreeBin包装类,其实列表转树的界限是8,但是数组下标是从0开始,所以是TREEIFY_THRESHOLD - 1。
17行就是转化方法了。
补充 1:jdk8之后,hashmap中,数据结构是Node[]+(链表|树),Node[]肯定是不会改变的了,那什么时候用链表,什么时候是树呢?他们有个临界点,8,这个数字我在回答里也说过,你在源码里也能找到。知道这个8了,我们看下怎么转化的,首先,数组中的一个元素,他是从链表就够开始构造的,也就是说,你连续put相同的(lenght-1)&hash的值,当个数小于8的时候,用的都是链表,也是就是node.next=new Node()形式,当个数大于等8的时候,将链表转化为TreeNode形式(在内部TreeNode托管给TreebBin,TreeNode是红黑树结构。),p instenceof NodeTree 就是用来判断,当前数组元素结构,是链表还是树,如果返回时true,那么,当前元素下的结构已经是树,就用树的方式将当前操作的对象加入到树中,反之还是链表,将当前操作的对象,加入到链表末尾,加入完成之后,判断是否需要转化为树结构,判断条件是什么呢,就是那个8。
补充 2: jdk8之后,初始化HashMap之后,他的容量size并不是16,初始化后的对象 是{},当有写操作进行时,才会inittable 给他一个初始化容量1<<4,具体的请看源码,如果不懂,欢迎留言一起讨论。
补充 3: 有人说到底是看jdk7好还是8好,我个人意见,以8为主,7也要看,看他为什么这么优化,这么优化的好处是什么,优化的作者是如何想的,我想我们能把这种思想转化为我们自己的,jdk这种教科书也算是成功的。