Java之HashMap源码分析(第二篇:添加元素)
(注意:本文基于JDK1.8)(文章版本:v1.0)前言在第一篇文章中,我着手分析了HashMap对象的创建,创建HashMap对象是为了使用它提供的功能,这篇一起学习HashMap添加元素的功能是如何实现的?假设你有一个Person对象需要保存到HashMap容器中,此时你需要先准备一个Key对象,存储的Person对象则作为Value对象,以后你只需要使用Key对象就可以从HashMap对象中
(注意:本文基于JDK1.8)(文章版本:v1.0)
前言
在第一篇文章中,我着手分析了HashMap对象的创建,创建HashMap对象是为了使用它提供的功能,这篇一起学习HashMap添加元素的功能是如何实现的?
假设你有一个Person对象需要保存到HashMap容器中,此时你需要先准备一个Key对象,存储的Person对象则作为Value对象,以后你只需要使用Key对象就可以从HashMap对象中查找到存储的Person对象,那么这个Person对象是如何添加到HashMap中的呢?我们继续深入到HashMap的代码中一探究竟
先罗列一下HashMap添加元素的方法有哪些?
上图2个红圈为HashMap添加元素的方法,我们经常使用的是以put开头的方法,所以本篇文章主要分析put开头的方法,compute开头的方法将在单独的文章中进行分析,简单描述下每个put方法的特点(功能)
1、put()方法:添加单个元素的方法,每当传入的Key对象在HashMap对象中不存在,会新增一个Key-Value到HashMap中,如果Key对象已经存在于HashMap中,则会覆盖Key对象对应的Value值,每当新增一个Key-Value时,put()方法的返回值为null,若覆盖了原有的key-value中的Value值,则put()方法返回的是旧的Value对象(注意:可以利用返回值做业务逻辑的判断,比如==null代表新添加元素,不为null则为覆盖元素)
2、putAll()方法:添加多个元素的方法,将另一个Map中的所有元素都添加到当前HashMap对象中,这个方法没有返回值
3、putIfAbsent():添加单个元素的方法,不过它有要求。当Key对象不存在HashMap中时,会添加Key-Value到HashMap对象中,如果Key对象已经存在且对应的Value对象为null,也会将新的Value对象覆盖掉旧的Value值null。如果Key对象已经存在于HashMap中,且Value不是null,则不会进行覆盖Value的操作,只会返回旧的Value对象(注意:根据putIfAbsent()方法的特点,在需求中使用即可)
接下来分析put开头的所有方法,先从put()方法开始分析,put()方法用于添加一个元素……
put()方法分析
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
用于添加单个元素的方法,传入的第一个参数是参数类型为K的Key对象,传入的第二个参数为参数类型是V的Value对象(备注:类型参数与HashMap对象创建时,指定的泛型有关),看下方法体的执行逻辑
1、首先二次计算出一个新的hashCode值
将传入的Key对象再传入到一个静态方法hash()中(见本文下方),静态方法hash()将二次计算出一个新的hashCode值,这个新的值会传入到putVal()方法中
2、调用pulVal()方法添加元素
putVal()方法需传入5个参数,第一个参数表示hashCode值(此处传入的正是静态方法hash()的返回值),第二个参数为Key对象本身,第三个为Value对象本身,第四个参数为一个标志位,第五个参数也为一个标志位,后面详细分析pulVal()方法的实现,这里暂时不表
3、向调用者返回添加元素的结果
putVal()方法的返回值,表示添加元素的结果,return 语句会将结果返回给调用者,当返回值为null,说明是新增元素,返回值不为null,说明是覆盖元素(修改元素)
接下来我们先认真学习静态方法hash(),它的方法体中对传入的Key对象做了什么?为何要二次计算出一个新的hashCode值?
静态方法hash()分析
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
用于计算出一个新的hashCode值的静态方法,传入的参数为Key对象,hash()方法使用static修饰,还有final修饰,表示此静态方法不允许被隐藏,它是如何计算出一个新的hashCode值的呢?(备注:Java中的每个类都是Object的子类,所以每个对象都可以调用hashCode()方法)
1、定义局部变量h,用于保存对象的hashCode值
首先创建一个称为h的临时局部变量,局部变量h用来存储传入的Key对象的hashCode()方法的返回值,这个hashCode()方法的返回值,平时都称为对象的哈希值(对的hashCode值)
2、分别处理Key对象为null与非null的业务逻辑(HashMap支持key为null)
在静态方法hash()的内部对于Key对象为null、或者Key对象为非null的两种情况分开处理,看下是如何处理的?
a、当Key对象为null时,静态方法hash()直接为调用者返回一个0……
b、当Key为非null时,做了3件事,最后返回一个整型值作为新的hashCode值,这个返回的整型值会作为pulVal()方法中接受的第一个参数(注意:静态方法hash()返回的整型值不是哈希地址,只有实际指向数组中存储位置的值才是哈希地址(下标值、桶值),此整型值是根据Key对象的hashCode()方法的返回值进行二次计算后一个整型值,我称它为新的hashCode值,或者新的哈希值),做了哪3件事呢?
第一:调用Key对象的hashCode()方法获得返回的hashCode值,这个hashCode()方法是每个对象都有的,它默认实现在Object类下,它会返回一个整型值,然后将返回的整型值赋值给局部变量h进行存储
第二:基于局部变量h存储的整型值(对象的hashCode值)进行无符号右移16位操作,再次得到一个无符号右移后的临时新值(>>>代表无符号右移,无论正数还是负数,高位通通补0)
第三:局部变量h存储的原值与上一步生成的临时值进行一个异或运算(异或:相同为0,不同为1)
3、向调用者返回值(两种情况)
第一种:当key对象为null时,返回0
第二种:将经过异或计算后的值返回(原值与右移16位的值进行异或运算)
此处为何要构造一个对象的hashCode()返回值进行右移16位后的临时值呢?这个操作会将原有对象的hashCode()方法的值,所有高位值全部右移到低位(左侧移到右侧,高位在左,低位在右),然后再将局部变量h与h右移16位后的临时值执行一个异或运算。(对象的hashCode()方法返回值与右移16位的值进行异或运算,得到一个新的hashCode值)
看来构造这个右移16位的临时值的目的是为了进行一个异或运算,那么为什么最后要对原对象的hashCode()值再和1个右移16位的值进行异或计算呢?
答:因为异或运算后的值(二次计算后的hashCode值)可以帮助HashMap对象减少插入元素时,发生Hash碰撞(哈希冲突)的概率,有人做过实验,如果不使用静态方法hash()与使用静态方法hash()的之间会有30%的碰撞概率差,使用静态方法hash()处理后,哈希碰撞可以下降整整30%的概率,牛逼!(文章结尾见一个图表,是别人画的,不扰动前的值低位总是0)
注意:静态方法的hash()被称为扰动函数或者二次扰动函数!
未加扰动函数hash(),去计算哈希地址的过程如下:(若看不明白,回头再来看)
原HashCode | 1954974080 | 111 0100 1000 0110 1000 1001 1000 0000 (缺点:低位总是0) |
2^4-1=15(length-1)数组容量-1的值 | 15 | 000 0000 0000 0000 0000 0000 0000 1111 |
&(位与)运算 | 0 | 000 0000 0000 0000 0000 0000 0000 0000(造成桶值总是0,冲突多) |
加上“扰动函数hash()”后,计算哈希地址的过程如下:(若看不明白,回头再来看)
原HashCode | 1954974080 | 111 0100 1000 0110 1000 1001 1000 0000 |
(>>>16)原HashCode值无符号右移16位后的值 | 29830 | 000 0000 0000 0000 0111 0100 1000 0110(无符号右移,高位统统补0) |
^(异或)运算:原HashCode值与右移16位后的值进行异或运算 | 1955003654 | 111 0100 1000 0110 1111 1101 0000 0110 |
2^4-1=15(length-1) 数组容量-1 | 15 | 000 0000 0000 0000 0000 0000 0000 1111 |
&(位与)运算:异或后的值与数组容量-1的值进行位于运算,计算出哈希地址(桶值 ) | 6 | 000 0000 0000 0000 0000 0000 0000 0110(桶值为6) |
分析完用于减少哈希冲突的静态方法hash(),抓紧分析一下putVal()方法,putVal()方法实际实现了插入元素,话不多说,我们继续学习putVal()方法
putVal()方法分析
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
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)
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
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;
}
putVal()方法用于实际插入元素,它接受5个参数,第一个int值表示Key对象对应的hashCode值,第二个是Key对象,第三个是Value对象,第四个是boolean标志位,最后一个也是个boolean标志位
第一个参数hash,传入的是静态方法hash()计算后返回的一个值,这个新值是根据Key对象的hashCode()返回值进行二次扰动后(一次右移16位、一次异或)的整型值(也称为二次扰动后的哈希值),并不是桶的地址(哈希地址)
第二个参数为传入的Key对象(通过Key对象可以查找到我们存储的元素对象)
第三个参数value为传入的Value对象(我们需要保存的元素对象)
第四个参数onlyIfAbsent,是个标志位,false代表若key对象存在,则仍然覆盖掉旧的Value对象
第五个参数evict也是一个标志位,一般它的传入值为true,这个参数只在afterNodeInsertion(evict)方法中使用了(这个方法是个空实现,如果为false,代表处于创建模式,以后LinkedHashMap会用得到……)
putVal()方法比较长,我将按照顺序逐步分析它的方法体,将每一部分代码拆分出来进行分析,这样阅读起来相对容易一些
一、准备环境,创建需要用到的局部变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
1、定义局部变量tab,用于临时保存HashMap对象持有的底层数组对象table
2、定义局部变量p,用于存储在当前桶(数组下标)中取出来的Node对象
3、定义局部变量n,用于存储HashMap底层数组的长度
4、定义局部变量i,用于存储桶地址(数组下标,也称哈希地址)
二、为新创建的HashMap对象,初始化持有的数组对象
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
目的:第一次使用HashMap对象添加元素时,为HashMap对象持有的底层数组对象进行初始化
1、为局部变量tab赋值
HashMap对象持有的实例变量table会赋值给局部变量tab,此时tab指向HashMap对象持有的数组对象
2、接着检查tab的值是否已经初始化,这里使用tab==null进行判断
3、如果tab不为null的情况,继续检查数组对象的长度是否为0
先提取数组对象的长度,由局部变量n负责保存,接着比较数组的长度
4、没有创建数组对象或者数组长度为0时,执行扩容操作
若tab为null或者tab底层数组长度为0时,会立刻执行resize()方法进行数组扩容,resize()方法返回的新数组对象会赋值给tab存储上
5、接着又立刻取出新的数组对象tab的长度length,再赋值给局部变量n
注意:作者此处将初始化数组与扩容数组的代码放到了一起……好厉害……
三、无冲突时,直接在桶中插入结点对象
目的:没有哈希冲突,直接插入元素
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
1、计算哈希地址,也称桶地址,并保存在局部变量i中
使用n-1的值(数组长度减去1为奇数值) 在与传入的局部变量hash表示Key对象的哈希值进行一个按位与运算,计算结果称为哈希地址(也称桶的下标),再将该值由局部变量i存储
2、从数组对象tab中取出保存的Node对象,并保存在局部变量p中
从数组tab[i]处取出对应的Node对象,并赋值给局部变量p保存
3、检查桶地址(数组下标)处是否持有元素对象
从桶中取出来的Node对象是否为null,代表是否存在元素,若 p == null为true,则说明底层数组的桶中没有持有Node对象,即没有存储任何元素,此时该下标处可直接插入元素
4、创建表示结点的Node对象
调用newNode()方法创建Node对象,newNode()方法传入4个参数,它会创建并返回一个Node对象,该Node对象持有传入的hash参数(Key对象二次扰动后的hashCode值)、持有传入的key对象、以及传入的value对象、传入的next对象,next用于指向下一个Node对象,此处会传为null,因为此处是第一次创建结点对象(next用于构建单链表),以下是newNode()方法的方法体
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
5、将创建好的Node对象保存到哈希地址中(数组下标中)
将newNode()方法返回的Node对象赋值给底层数组对象对应的桶地址(局部变量i存储着这个桶地址),这就完成了一个插入元素的操作,可见数组对象实际存储的是Node对象,而Node对象又持有着Key对象与Value对象
tab[i] = newNode(hash, key, value, null);
四、出现哈希冲突,插入新的元素或者覆盖旧的元素
目的:插入元素时,若数组对象对应的桶地址中(数组下标)已经存储着元素,此时局部变量p != 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)
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
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;
}
}
先重温一下桶的概念,在哈希表(HashMap)中,数组持有元素的位置(数组下标),称为桶。
举个例子:有一个数组容量为10,代表有10个桶,下标值从0-9,都是桶地址,也称数组下标。
1、定义局部变量,用于保存数据
局部变量e,用于保存桶地址中存在的Node对象
局部变量k,用于保存桶地址中某一个Node对象持有的key对象
局部变量p,用于保存当前桶地址中获取到的元素对象(Node对象)
备注:注意进行哈希冲突判断时,最先给局部变量p进行赋值,只有p != null时,代码才会走到这里!
2、对三种不同的哈希冲突情况分别进行处理,依次判断是哪种冲突情况
第一种情况:检查是否为同一个Key对象
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
局部变量p指向的是当前从数组下标中获取到的Node,比较是否为同一个Key对象,用了如下方式
先比较Key对象的hashCode值是否相同:将Node对象持有的hash字段值与传入hash字段值进行比较,如果相同则继续比较
然后比较Key对象是否为同一个:先用==判断key对象是否为同一个,p.key == key,如果key对象不是同一个,继续进行比较,继续比较的前提是key对象不是null(HashMap支持Key为null),再使用Key对象的equals()方法进行判断,若Key对象的equals()方法返回true,也可以证明key对象相等
上面两个条件满足的情况下,说明找到相同的Key对象,接着将局部变量p指向的Node对象赋值给局部变量e,此时e保存着已经存在的Node对象
(注意:key使用自定义对象,一定要保证两个Key对象的equals()方法相等时,hashCode()方法的返回值也一定相同,如果重写了equals()方法,则一定要重写hashCode()方法,否则Key对象将不能在HashMap中正常工作,跟这里判断Key对象是否为同一个有关)
(注意:key对象不一定非要重写equals方法与hashCode方法才能作为HashMap的key对象,Object自带的方法就可以作为HashMap中的key对象的要求,这是因为HashMap中key是否相同,先使用==进行了判断是否为同一个对象,而Object自带的equals方法与hashCode方法,一定会保证同一个对象equals方法判断相等(证明是同一个对象)时,hashCode值一定相等!!重写equals()方法时,往往是因为需求,比如,有两个对象,它们只要id一样,就代表同一个人,这样的需求出现时,就得重写equals()方法
第二种情况:桶地址保存的结构为红黑树
else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
代码执行到这里,说明Key对象肯定不相同
首先检查元素对象是否为红黑树结点对象,即TreeNode对象,如果是则证明当前桶地址中保存的结构是红黑树结构
接着先将局部变量p强制转换为TreeNode对象
然后将相关的参数,HashMap对象、数组对象tab、hash、key对象、value对象,共计5个参数一起传入到表示红黑树结构的TreeNode对象的putTreeVal()方法中
最后putTreeVal()方法的返回值会赋值给局部变量e,局部变量e表示已经存在的结点对象
备注1:位于TreeNode类中的putTreeVal()方法内部产生两种逻辑,第一是插入的key对象不存在,则新增一个结点到红黑树中,此时putTreeVal()方法会返回null并赋值给局部变量e,第二是插入的key存在,则也会找到该Tree结点
备注2:代码中有一个类型转换,就是将p这个静态类型强行做了一个向下转型为TreeNode,因为TreeNode继承了Node,所以这里转型是可以的,目的就是为了调用putTreeVal()方法,putTreeVal()方法太复杂了,我要单独总结,红黑树是我的噩梦
第三种情况:桶地址保存的是单链表
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 treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } }
代码执行到这里,说明桶地址中的Key对象既不相同、也不是红黑树结构,这里的情况属于单链表结构(单个元素的单链表也是可以的,所以还包括单个元素的情况)
定义for循环用于遍历单链表中每个元素,看下for循环中的代码逻辑
首先定义局部变量binCount,用于记录遍历了几个元素,为后面进行单链表转红黑树结构做准备。
接着从桶地址取出来的第一个当前Node对象p,再检查Node持有的next结点,并赋值给结点e
然后判断e是否为==null,如果条件成立,说明已经到达单链表的最后一个结点(假设单链表就1个结点,那也算到达最后一个结点)
单链表最后一个结点的情况下,先调用newNode()方法,方法返回Node结点对象,然后赋值给当前结点对象p持有的实例变量next,此时等同于将新的结点对象插入到单链表(尾插法)
检查是否需要将单链表转红黑树结构:先判断binCount的数量,若大于等于TREEIFY_THRESHOLD - 1,TREEIFY_THRESHOLD的值为8,这里为什么要减去1呢?假设遍历到了单链表的最后一个结点时binCount值为6(0-6,已经7个),那么加上这次已经添加的一个结点,刚好单链表的长度已经是8了,借此判断单链表的长度大于等于8时,尝试调用treeifyBin()方法将单链表结构转为红黑树结构,为什么说是尝试转换呢?treeifyBin()方法结束后
由于已经达到单链表的最后一个结点,所以会通过调用break结束for循环(遍历单链表)
若遍历到的结点不是单链表中的最后一个结点,会进行Key对象的判断,仍是先取出结点对象持有的hash值与传入的hash对比,若值相等,再继续用==判断、==如果还是不同,则再用equals判断key对象是否相同,只要有一个条件证明key对象相同,则说明持有的相同Key的结点对象已经存在单链表中,此时会调用break去终止for循环
p = e,这行代码很孤独,只有在单链表中没有找到相同Key对象时,由于for循环遍历单链表的需要,将更新局部变量p指向桶地址中单链表中的下一个结点
出现Key对象相同,执行覆盖Value对象的行为
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
3种冲突的情况都可能造成局部变量e不为null,局部变量e保存着与Key对象相同的Node对象,说明找到了Key对象已经存在的结点对象,此时执行覆盖Value
首先取出现有Node对象中持有的value对象
接着做两个判断
检查onlyIfAbsent标志位、以及检查另外一个取出来的value对象是否指向null,有一个为真,则会覆盖Value的操作
然后调用afterNodeAccess(e)方法,该方法是留给子类去实现的,HashMap中是一个空实现(模版方法设计模式)
最后向调用者返回旧的Value对象(覆盖行为,将造成后续代码不会继续执行)
备注:put()方法调动时,传入的是onlyIfAbsent == false,表示Key对象即使存在也覆盖Value对象
五、添加新的元素,检查是否需要扩容
只有新增一个元素后,才会执行到这里,覆盖value是不会走到这里的!
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
1、防止多线程下使用添加元素
先是modCount增加1,防止并发下使用HashMap一边遍历,一边进行增、删操作的行为(fail-fast机制)
2、先更新HashMap对象持有的元素总数
size+1,代表HashMap中的元素数量增加1个
3、检查是否需要扩充数组对象的容量
判断新的元素总数是否大于扩容阈值threshold(注意:不包括等于),若大于该阈值,会调用resize()方法进行扩容
4、为LinkedHashMap做嫁衣
接着调用一个afterNodeInsertion()方法,它是一个空实现,留给子类去实现(LinkedHashMap)
5、向调用者返回结果
最后返回null,说明是新添加一个元素
注意:JDK1.8的HashMap是先增加新的元素,然后再去做检查数组容量是否需要扩容
单链表转红黑树的treeifyBin()方法(该方法在四个地方被调用,红黑树复杂,将单独总结)
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
treeifyBin()方法用于将单链表结构转为红黑树结构,但方法体中不会一定将单链表结构转为红黑树结构
两种情况如下
1、没有转红黑树,只进行数组容量的扩容,因为当前数组容量小于MIN_TREEIFY_CAPACITY(值是64)
2、确定转红黑树,当单链表的长度大于等于8时,并且当前数组容量大于等于64时,才会真的将一个超长的单链表转为红黑树结构(看来作者知道这里开销太大)
重要知识点回顾:计算桶地址(数组下标)
if ((p = tab[i = (n - 1) & hash]) == null)
(n-1) & hash 用于计算桶的下标(桶地址,哈希地址),这里巧妙使用了按位与运算代替了取模运算,为什么这么做呢?这是因为按位与运算比取模运算的执行效率高,按位与运算直接操作的二进制位,而取模运算还要将10进制转换为二进制再进行计算,当然没有直接的按位与的运算效率高!
hash变量(hashCode值)的值范围是一个32位的int值(-2147483648~2147483647[-2^31~2^31-1] ,最高位代表符号位,0为正、1为负),显然HashMap对象持有的底层数组容量是有限的,不可能放下这么大值的下标,而且底层数组的默认容量是16,所以通过一个办法,把这个hash值转为有效范围(不超过数组容量)的下标
假设数组容量是16、 此时用 hash % 16,那么可能得到的值的范围是0-15,那正好用取模运算就可以计算一个满足数组容量的下标值,比如 31 % 16,余数是15……,这个有缺点吗?我这个屌丝觉得也挺好的呀,但是对于大神来说,还是觉得效率不高!大神会想什么办法?
答:HashMap对象持有的数组对象的容量永远是2的n次方,只有此时才能使用按位与运算代替上面说的取模运算,获取桶地址(hash & length - 1)
hash &(16 - 1) == hash % 16,这两个表达式最后的计算结果一摸一样,但是按位与运算的效率比取模运算的效率高
总结
1、HashMap对象在第一次添加元素时,会对持有的数组对象进行初始化工作,此时才创建数组对象
2、单链表转红黑树结构时,必须2个条件同时满足才会进行转换,第1个条件为扩容阈值必须大于等于8(单链表长度大于等于8),第二个条件为数组对象的容量必须大于等于64,若第2个条件不满足,则单链表转红黑树的treeifyBin()方法会先调用扩充数组容量的resize()方法,充分说明单链表转红黑树的代价是非常巨大的!
3、putAll()方法添加多个元素时,是一个一个的将元素添加到HashMap对象持有的底层数组对象中的……,可见添加效率真的一般
4、红黑树复杂,需单开文章总结…………我哭!
5、HashMap是先添加新的元素,再去检查是否需要扩充数组容量
6、HashMap支持添加的Key对象为null
7、如果是覆盖旧的元素,HashMap不会再去检查是否需要扩充数组容量
8、遵守HashMap的规则,Key对象的hashCode()方法、equals()方法必须按照要求开发,才能作为HashMap中的Key对象
9、建议添加元素的过程学无数遍!!!作者无敌了!
更多推荐
所有评论(0)