JDK源码解析-HashMap
Posted lilice
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK源码解析-HashMap相关的知识,希望对你有一定的参考价值。
在经过漫长的秋招的漫长的打击之后,认识到了自己的许许多多的问题,最大的问题就是很多东西只会用,或者只能说出很表面的原理,于是乎,自己开始了漫长的啃代码之路,这一篇,从HashMap说起,本合集源码均基于JDK1.8,不比较其它版本。
一、参数解读
serialVersionUID用来保证序列化与反序列化时版本的兼容性。
1、DEFAULT_INITIAL_CAPACITY定义了HashMap初始时的容量为16
2、MAXIMUM_CAPACITY定义了HashMap的最大容量为2的30次方
3、DEFAULT_LOAD_FACTOR定义了扩容时的加载因子,也就是当容量已经占用了0.75后自动扩容
4、TREEIFY_THRESHOLD(树化阈值)定义了当数据量大于等于8时使用红黑树而不使用链表,因为在数据量较小的情况下,红黑树要维持自身平衡,对比链表并没有优势
5、UNTREEIFY_THRESHOLD(还原阈值)定义了当数据量小于等于6时使用链表,在这里有一个小知识点,就是中间值7没有操作,这是为了防止红黑树和链表之间频繁转换降低性能(ps:之前小米面试官问我当数据量从6到7到8和8到7到6会发生什么我没答出来,刻骨铭心的痛)
6、MIN_TREEIFY_CAPACITY(最小树型化阈值)当哈希表中的容量大于该值时,才允许树型化链表,否则,若桶内元素太多,则直接扩容,而不是树形化,为了避免进扩容和树形化的冲突,这个值不能小于4倍的树化阈值
7、table是在创建时初始化的表,长度总是2的幂次方,在某些情况下允许长度为0,每一个node本质上都是一个单向链表
8、entrySet功能为保存缓存
9、size是当前hashmap的大小
10、modCount记录了结构性修改的次数,这个参数在官方源码中的作用是这样说的:“此字段使哈希映射的集合视图上的迭代器快速失效”,说白了,因为hashmap是线程不安全的,使用迭代器迭代时,一旦modCount被修改了,就证明数据被修改了,迭代器就会抛出异常,这其实就是所谓的Fail-Fast 机制
11、threshold此参数是下一次扩容时的大小
12、loadFactor也是加载因子,前面那个是默认的加载因子,这个参数可以在构造hashmap时自定义
单向链表没什么好说的,看一下就行。
三、红黑树的实现
红黑树的实现代码太太太长了,看了一下,不贴上来了,告辞。
四、Hash的计算方式
这里将key本身的hashCode与hashCode无符号右移16位进行异或运算生成一个新的hash值
至于为什么要这样做,源码中是这样说的
计算key.hashCode()并将哈希的高位扩展到低位。因为该表使用二的幂掩码,所以仅在当前掩码之上的位中变化的哈希集将总是冲突。(已知的例子有在小表格中保存连续整数的浮动键组。)所以我们应用了一个向下传播高位比特影响的变换。比特扩展的速度、效用和质量之间有一个折衷。因为许多常见的哈希集已经合理分布(因此不要从扩展中受益),并且因为我们使用树来处理容器中的大冲突集,所以我们只是以最便宜的方式异或一些移位的位,以减少系统损失,并结合最高位的影响,否则由于表的边界,最高位将永远不会用于索引计算。
大概就是说可以避免hash冲突,因为链表的查询是十分耗时的,我们希望hashmap中的元素尽可能的分布均匀,尽可能少的产生哈希冲突,在这里我查询了其它版本JDK中的这个方法,1.7和1.8都是使用无符号右移和异或降低冲突,这个方法称为扰动函数,防止不同的hashCode的高位不同但是低位相同导致的hash冲突,就是把高位的特征和低位的特征结合起来,从而降低hash冲突的概率。
这里可以深究一下,打个tag
想起来一个问题,如果已知要存储100个数据,hashmap初始容量应该设置为多少,答案为256,看到这里还不知道为什么的去面壁思过。
五、put方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //如果table为空或者长度为0,则通过resize获取 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) //当前位置为空,直接new一个节点并赋值 tab[i] = newNode(hash, key, value, null); else { //当前节点不为空 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) //如果这个节点的TreeNode类型,则在红黑树中查找 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //否则则向单链表中添加数据 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //大于8则树化 treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) //判断是否需要扩容 resize(); afterNodeInsertion(evict); return null; }
put方法执行流程
1、获取Node数组table,如果为空或者长度为0,调用resize方法获取对象得到长度
2、判断数组中指定下标下的节点是否为null,若为null,则new一个单向链表赋值
3、有数据则判断key有没有重复,重复则覆盖
4、不重复则判断是什么类型的节点,树型节点则添加,链表型节点需要判断长度是否超过阈值
5、判断数组需不需要扩容
未完待续
以上是关于JDK源码解析-HashMap的主要内容,如果未能解决你的问题,请参考以下文章