博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
HashMap实现原理
阅读量:6216 次
发布时间:2019-06-21

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

面试高频问题,这里提供下参考。

img_4d9269ae62a027cc9d365ef726875c7b.png
HashMap结构图

目录

  • 唠叨
  • 解析思路
  • get方法
  • put方法
  • resize方法

唠叨

认真阅读了下HashMap的实现方式,也参考了网上别人的一些解析,个人觉得还是有些东西想说。网上有的文章名字为HashMap源码解析,实际上就是给它里面的一些方法加上一些注释而已,有不少都是这样的。

我自己看源码的时候,发现不是别人不想解析,而是它的实现真的需要亲自研读,多理顺几遍才知道怎么回事。

我在这里解析的文字描述也较多,不管谁的解析,自己也都要看一下JDK源码的具体实现,我们仅提供参考而已。

解析思路

源码不太方便看,先说明一下我的阅读思路。

  1. 把常用的几个方法拷贝到文本编辑器里面。
  2. HashMap中不同的时候会有不同的流程,梳理方法中的逻辑流程。
    就像采用极端法,采用特殊的数据,然后查看方法执行语句。未执行的语句暂时不考虑。
  3. 注释源码...

我觉得HashMap的实现方式不够好,关键的几个方法里面包含的情况太多了,阅读起来是有难度的,而写程序的目的之一不就是让其他开发者阅读吗?一个方法内部做了太多的事情,违反了代码整洁的规则,一个函数做要尽量少的事情。

解析

之前稍微介绍了一些HashMap的特性,。这里接着深入。

get方法

先挑最简单的说...

public V get(Object key) {        Node
e; return (e = getNode(hash(key), key)) == null ? null : e.value;}
  1. 先从数组下标,找到对应的Node
  2. 如果Node里的第一个节点命中,直接返回
  3. 如果有冲突,则通过key.equals(k)去查找对应的entry
  • 若为树,则在树中通过key.equals(k)查找,O(logn);
  • 若为链表,则在链表中通过key.equals(k)查找,O(n)。
// hash值为hash(key),keyfinal Node
getNode(int hash, Object key) { Node
[] tab; Node
first, e; int n; K k; //table不为空,并且tab[(n-1) & hash] != null的时候。 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //判断取出Node的hash值是否相等。key值相等,那么直接返回。 //想一想什么情况下,if语句不成立? if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; //也就是取出的第一个Node的hash值与key计算的hash不等。 if ((e = first.next) != null) { if (first instanceof TreeNode) //从树中取节点。 return ((TreeNode
)first).getTreeNode(hash, key); do { //判断hash值与key值是否相等,一直判断到相等或到节点末端为止。 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }

put方法

这个中间涉及的逻辑多一些,方法需要分不同的步骤看。

思路:

  • 对key的hashCode()做hash,然后再计算index;
  • 如果没碰撞直接放到bucket里;
  • 如果碰撞了,以链表的形式存在buckets后;
    • 如果节点已经存在就替换old value(保证key的唯一性)
    • 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
  • 如果Node的容量满了(超过load factor*current capacity),就要resize。
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; //如果table为空,就重新创建table if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 如果tab[(n-1) & hash]为空的话,就在tab[(n-1) & hash]位置存储节点。 // newNode = new Node<>(hash, key, value, next); if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //当tab[(n-1)&hash]位置已经存在Node的时候。 Node
e; K k; //如果已经存在的Node与即将要存的key值一样 // e为存在的Node if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode
)p).putTreeVal(this, tab, hash, key, value); else { //不满足以上的情况,一直把Node往后插入。 //如果插入的节点数量多于TREEIFY_THRESHOLD-1个,变为树形节点 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 treeifyBin(tab, hash); break; } //如果再遍历的时候,发现key值相同的时候,就跳出循环。e = p.next, // 这时已经记录e的Node值了 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 存在对应的Node时 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } // HashMap内部修改的初始 ++modCount; //如果存储的节点数,大于临界值,重新分配大小 if (++size > threshold) resize(); //抽象方法,当节点执行插入操作的时候如何处理 afterNodeInsertion(evict); return null; }

一般不发生碰撞的时候,相对简单,数据量较小的情况下。

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; // 留意 i = (n-1)&hash,所以取的时候也这样取 // newNode = new Node<>(hash, key, value, next); if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null;}

发生碰撞后,有个红黑树的处理,因为红黑树相对知识点较多,下次单独详细解释。这里可以参考以下,。我解释下关于碰撞冲的循环。

  • 查看是否存在相同的key,存在相同的key跳出循环,覆盖key的value
  • 如果不存在相同的key,在链表末尾插入新的Node
    • 如果链表节点过长,转换为树。
for (int binCount = 0; ; ++binCount) {                    // p.next为null的时候,走到了链表的末端,然后新建一个节点,如果链表的长度太长,转换为树存储。                    if ((e = p.next) == null) {                        p.next = newNode(hash, key, value, null);                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                            treeifyBin(tab, hash);                        break;                    }                                  // 如果链表中存在于要put的key值相同的时候,存储key值,也就是e ,(e = p.next)。                    if (e.hash == hash &&                        ((k = e.key) == key || (key != null && key.equals(k))))                        break;                    p = e;                }

红黑树的部分,我们下次单独解析

resize方法

这个涉及的内容,有不少线需要捋一捋。首先看申明时候会resize()。它们都在调用put的时候执行的。

  • table == null的时候
if ((tab = table) == null || (n = tab.length) == 0)            n = (tab = resize()).length;
  • 键值映射的的数目大于临界值的时候。
if (++size > threshold)            resize();
resize具体方法
final Node
[] resize() { Node
[] oldTab = table; // 之前的容量可能为0或者为之前的大小 // threshold可能为null或者为2的n次方 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; //新的容量,新的临界目前都为0 int newCap, newThr = 0; // 第二次resize的时候。 if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } //第一次resize()的时候,初始化的操作。 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); } //第一次resize(),会进入 if (newThr == 0) { //负载因子 * 初始容量 float ft = (float)newCap * loadFactor; //保证临界值不超过最大值。 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //真正初始化的操作,新建newCap个数组,临界值初始化。 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node
[] newTab = (Node
[])new Node[newCap]; table = newTab; // 非 第一次reizie()时 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node
e; if ((e = oldTab[j]) != null) { // 重新计算了一次hash 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); // 如果e.next != null 存在hash的Node 链子 else { // preserve order 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; } // 原索引+ oldCap 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; }

如果是第一次resize,我们抽出来会执行到的语句。

  • 初始化容量
  • 初始化threshold,也就是初始化临界值,决定了table的键值对数目到什么时候会再次resize()
final Node
[] resize() { //第一次的时候table为null Node
[] oldTab = table; // oldCap 为 0,threshod为null int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0;// 不会走 if (oldCap > 0) { ....// 从这里执行 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node
[] newTab = (Node
[])new Node[newCap]; table = newTab; return newTab;}

第二次及后续的resize执行流程

final Node
[] resize() { Node
[] oldTab = table; // oldCap为当前table的长度, oldThr为上次的table临界值 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { // 这个if语句保证容量不超过hashmap的容量上限值。 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 如果扩容之后,不超过容量上限, // 那么表的大小加倍。临界值加倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node
[] newTab = (Node
[])new Node[newCap]; table = newTab; //第二次扩容的时候,对上次的table如何处理。 if (oldTab != null) { // 遍历之前的table,重新hash排序 for (int j = 0; j < oldCap; ++j) { Node
e; //只对存在的索引操作 if ((e = oldTab[j]) != null) { //销毁当前索引的内容 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); //链表的操作 else { // preserve order Node
loHead = null, loTail = null; Node
hiHead = null, hiTail = null; Node
next; do { next = e.next; // oldCap 为 16也就是 10000, //oldCap为16的倍数,这里是hash值为低数字的时候 if ((e.hash & oldCap) == 0) { //第一次 if (loTail == null) loHead = e; //计算新的next 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; }

resize中对有碰撞的链表的操作写的很有意思,再叙述一下。在重新分配索引的时候,有重新组建链表的操作。

举个比较夸张的例子,读者就明白了。

  • e.hash < 2,那么 e.hash&oldCap就等于0,索引为小于之前hash表大小以内的索引。也就是当初的索引不变。
  • e.hash > 2的时候,e.hash&old不等于0,那么它的索引就为当前表的索引再加上新扩容的大小。
img_6c8ebed5375d72c6f34b8e665fa3d6e1.png
案例图

这个图说的是,当hashmap的表大小为2扩充到4的时候,原本挂载在1位置的链表,重新分配之后的样子。

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; }

最后

篇幅有限,我这里仅仅介绍了get方法,put方法,resize方法的具体原理,文章就已经非常长了,不利于阅读。

下次再补充一下HashMap的hash方法原理,其余的相关注意事项。

参考

转载地址:http://wslja.baihongyu.com/

你可能感兴趣的文章
CIDR的特殊性
查看>>
《我的歌声里》程序员版
查看>>
秀一秀我的微软MVP(最有价值专家)的大礼包和水晶奖杯!
查看>>
各个web服务器的性能对比测试
查看>>
基于VMware vSphere 5.0的服务器虚拟化实践(7)
查看>>
MDT 2013 从入门到精通之SQL Configure And Verify
查看>>
vim常见使用命令总结完整分享(一)
查看>>
【Python之旅】第五篇(三):Python Socket多线程并发
查看>>
cacti监控添加thold插件
查看>>
[Ruby] 异常捕获
查看>>
HP 服务器 iLO 远程控制软件 介绍
查看>>
[JavaScript] 环境与内存
查看>>
最全面的常用正则表达式大全
查看>>
不与你商量的远程强制关机
查看>>
Android系统匿名共享内存(Anonymous Shared Memory)C++调用接口分析(1)
查看>>
虚幻4,BP写了一个简单的三线跑酷工程
查看>>
警告okyep之辈,我要让你们抱憾终生
查看>>
逆天的H3C Comware V7 RBAC
查看>>
SQL Server 备份场景示例
查看>>
论通过测试与失败测试
查看>>