博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
HashMap源码分析
阅读量:4550 次
发布时间:2019-06-08

本文共 6225 字,大约阅读时间需要 20 分钟。

一、要点

1. 如何减少哈希碰撞

  1. 将哈希桶长度设置为2的倍数,这样在计算下标时(n-1)& hash 的(n-1)二进制最后一位也会参与运算,

  2. 当Map中元素增加时,势必会造成碰撞的增加,这时候通过扩容来,来减少碰撞

2. 何时初始化HashMap

  在put值时,初始化hashMap

3. 哈希桶的寻址方法

  计算下标的算法 (n-1)& hash

4. 链表何时转红黑树

  当哈希桶中链表长度大于7时,则链表转红黑树,因为红黑树的查找效率更高

5. 扩容时扩大几倍

  扩大两倍,同时阈值也同样扩大两倍

6. 为什么hashMap容量都是2的倍数

  1. 计算下标的算法是 (n-1)& hash

  2. indexFor代码,正好解释了为什么HashMap的数组长度要取2的整次幂。因为这样(数组长度-1)正好相当于一个“低位掩码”。“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问,

  3. 以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111。和某散列值做“与”操作如下,结果就是截取了最低的四位值。

  

7. 讲一下Put的过程

  1. 哈希桶为空的话,调用扩容函数初始化哈希桶,默认长度16

  2. (n-1)& hash计算下标,不发生hash碰撞的话,直接赋值

  3. 发生hash碰撞,如果链头的key值就相同,直接替换,如果是红黑树就进入红黑树对比赋值

  4. 否则遍历链表,比较赋值(比较方式hash+equals)

  5. 最后判断是否需要扩容

8. 讲一下resize()过程

  1. 判断是否需要初始化哈希桶的容量值、阈值

  2. 遍历老数组,用hash值重新计算下标位置,要么将原来的链表放入低位、要么将要来的链表放入高位

9. 能否让HashMap同步

  Map m = Collections.synchronizeMap(hashMap);

10. HashMap的长度为什么设置为2的n次方

  1. 在寻址过中,一般用取余的方式来,这样的效率不高,当容量为2次方时,按位运算&上length-1时,效果和取余相同

  2. 方便扩容时移位操作,效率高,同时扩容后还是2的次方

二、源码

hash()方法

1. 扰动函数就是为了解决hash碰撞的

key的hash值 异或 hash值的低16位

static final int hash(Object key) {    int h;    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

put()方法

public V put(K key, V value) {    return putVal(hash(key), key, value, false, true);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                   boolean evict) {    Node
[] tab; Node
p; int n, i; // 判断数组是否为空 if ((tab = table) == null || (n = tab.length) == 0) // 数组为空则初始化数组, 并获取长度 n = (tab = resize()).length; // 判断数组中值是否为空,index是 哈希值 & 哈希桶长度-1, 代替模运算 if ((p = tab[i = (n - 1) & hash]) == null) // 数组中对应下标放入值 tab[i] = newNode(hash, key, value, null); else { // 数组中对应下标不为空,哈希碰撞 Node
e; K k; // 判断数组取到的第一个节点的key值是否和要存入的相同 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 则把原来的值替换掉 e = p; else if (p instanceof TreeNode) // 如果p是红黑树, 则进入红黑树存值的流程 e = ((TreeNode
)p).putTreeVal(this, tab, hash, key, value); else { // 否则p是链表, 且第一个节点key值与要存入的不相同, 则对单项链表进行遍历 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; } //如果e不是null,说明有需要覆盖的节点 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; // 遍历下一个节点 p = e; } } // 判断是否找到了与待插入元素的hash值与key值都相同的元素 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } // 记录修改次数 ++modCount; //更新size,并判断是否需要扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null;}

resize()扩容

final Node
[] resize() { Node
[] oldTab = table; // 获取旧的数组的长度,和阈值 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; // 旧的容量大于0的情况 if (oldCap > 0) { // 如果数组长度等于最大容量值,则不扩容直接返回 if (oldCap >= MAXIMUM_CAPACITY) { // 设置阈值为2的31次方-1 threshold = Integer.MAX_VALUE; // 不扩容了,直接返回旧的数组 return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 如果容量变为2倍后小于最大容量,且大于等于默认容量16时 // 阈值也扩大一倍 newThr = oldThr << 1; // double threshold } //如果当前表是空的,但是有阈值。代表是初始化时指定了容量、阈值的情况 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults // 当数组还未被初始化时,设置默认容量和阈值 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { // 如果新的阈值是0,对应的是 当前表是空的,但是有阈值的情况 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; // 扩容重新创建一个大小为2倍的数组 @SuppressWarnings({"rawtypes","unchecked"}) Node
[] newTab = (Node
[])new Node[newCap]; // 更新哈希桶引用 table = newTab; if (oldTab != null) { // 循环将老数组中的放入新数组 for (int j = 0; j < oldCap; ++j) { // 当前节点e Node
e; if ((e = oldTab[j]) != null) { // 将原哈希桶置空,以便GC oldTab[j] = null; // 如果链表中只有一个元素 if (e.next == null) // 直接将这个元素放入新的哈希桶 // 注意这里重新计算了下标,这里相当于取模运算 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode
)e).split(this, newTab, j, oldCap); // 如果发生了Hash碰撞, 节点小于8,要遍历节点,依次放入新的节点 else { // preserve order //因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量 //低位链表的头结点、尾节点 Node
loHead = null, loTail = null; // 高位链表的头结点、尾结点 Node
hiHead = null, hiTail = null; Node
next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } // 遍历整个链表 } while ((e = next) != null); // 如果低位链表不为空,则将链表放入低位 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 如果高位链表不为空,则将链表放入高位 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab;}

三、疑问点

1. 扩容时,hiTail = e表示什么意思

转载于:https://www.cnblogs.com/milicool/p/11435244.html

你可能感兴趣的文章
ActiveMQ 消息队列服务
查看>>
《程序是给自己看的还是给别人看的》
查看>>
(12) PHP 随笔---Smarty模板引擎 单模板多缓存、局部不缓存 20--21
查看>>
【转】Math.Atan2 方法
查看>>
C++设计模式之工厂方法模式
查看>>
poj3984_bfs+回溯路径
查看>>
MyEclipse使用技巧
查看>>
[译]径向镜片反畸变滤波
查看>>
畅通工程-最小生成树+并查集
查看>>
top命令输出解释以及load average 详解及排查思路
查看>>
Ajax的封装
查看>>
Java传入参数个数不确定可用(Type ... values)
查看>>
POJ 2081
查看>>
记录下zend studio 的xdebug 在调试安装
查看>>
ES6阅读笔记
查看>>
数字基带信号分类
查看>>
移动HTML5前端性能优化指南(转)
查看>>
Jq 遍历each()方法
查看>>
Android源码分析:Telephony部分–phone进程
查看>>
关于 redis.properties配置文件及rule
查看>>