JDK1.8 HashMap学习
1:源码分析
1.1:构造方法
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < )
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
} /**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
} /**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
HashMap的源码中,含有两个参数的构造函数,其参数分别是initialCapacity和loadFactor,这两个参数和HashMap中原有的threshold的关系是什么尼?代码中有一行 this.threshold = tableSizeFor(initialCapacity);要将initialCapacity的值通过tableSizeFor的方法来返回一个值赋给HashMap的threshold,那这个方法有什么用处尼?
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - ;
n |= n >>> ;
n |= n >>> ;
n |= n >>> ;
n |= n >>> ;
n |= n >>> ;
return (n < ) ? : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + ;
}
这个方法按照注释会返回一个大于输入的cap的2的幂数,详细介绍我们参考https://www.cnblogs.com/loading4/p/6239441.html。
这个位运算十分高效的,写出JDK的人真的太厉害了。
1.2:put方法
分析put方法之前,我们要分析一下hash方法的,
static final int hash(Object key) {
int h;
return (key == null) ? : (h = key.hashCode()) ^ (h >>> );
}
当key是null时,返回0,当不等于null时,将key的hashCode的值异或上其本身右移16位,这个操作有什么意义尼?简单来说,是为了对key的hashCode进行扰动计算,防止不同hashCode的高位不同但低位相同导致的hash冲突。简单点说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对最终得到的结果产生影响。这是因为直接使用hashCode&length-1,得到数据只与hashCode的低位有关,为了避免出现两个key的hashcode的低位相同,高位不停而索引到相同的数组下标,我们将hashcode的高位数据也通过右移向异或的方式,将其不同的也影响到索引的位置,所以这样操作。
下面讲解put方法中使用的putVal的方法作用。以下图的注释讲解。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//定义一个节点Node的数组tab,和节点p,
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断table是不是null,或者长度为0,即再第一次put数据的时候,tab时空数组
if ((tab = table) == null || (n = tab.length) == )
//如果为空则,进行扩容,其中resize()方法是用来扩容使用的,这里先跳过。n的值是扩容后的数据长度,这里表明,构造方法只是初始化参数,而枕真正的创建桶指针,实在第一从put方法时候。
n = (tab = resize()).length;
//(n - 1) & hash这里是通过hash与上length-1取出hash的低位当作数组的下标
//判断该数组是不是null,这里p指向的是用过待输入节点hasd索引的位置的节点
if ((p = tab[i = (n - ) & hash]) == null)
//若为null,则newNode后,放在上面找到的位置。
tab[i] = newNode(hash, key, value, null);
//若该位置不为null,进入else
else {
Node<K,V> e; K k;
//由于该位置不为null,所以判断该位置的节点的hash值与待输入节点的hash比较,若hash相等,且
//该位置节点的key也等于带输入节点的key,这里用两个方法判断之间是或的关系,==或者equals方法
//对象相等或者内容相等,二者成立一个即可。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//若该位置节点和带输入节点相等,则用e指向该位置节点。
e = p;
//如果该位置节点与输入节点不相等,则判断该节点是不是TreeNode。
else if (p instanceof TreeNode)
//若为树节点,则通过putTreeVal方法将该节点加入到red black tree中,
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//若不是树节点,则就是链表节点,按照链表进行操作
else {
//
for (int binCount = ; ; ++binCount) {
//e指向p的next节点,若e为null,
if ((e = p.next) == null) {
//则p.next指向输入节点即可,输入节点接在链表的尾部。
p.next = newNode(hash, key, value, null);
//如果binCount大于等于8,即超过jdk1.8规定的链表长度,
if (binCount >= TREEIFY_THRESHOLD - ) // -1 for 1st
//则进行将红黑树的转换,将链表转为红黑树
treeifyBin(tab, hash);
//然后跳出循环。
break;
}
//这一步操作与上面的代码功能类似,用来判断链表中是不是有与输入节点一样的节点存在,
//若有,直接break
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//前面有e = p.next,结合下面的操作实现向链表的后面移动的操作步骤,e始终指向下一个节点
p = e;
}
}
//若e不等于null,意味着输入节点没有放在尾部,而是找到了相等的节点,进行替换,
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//将替换节点的return oldValue
return oldValue;
}
}
//迭代器中实现fall fast的功能
//This field is used to make iterators on Collection-views of the HashMap fail-fast.
++modCount;
//加入后的带大小如果大于负载,则扩容,
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
上面代码注释,说明了putValue方法,何时扩容?如何查询index?如何判断是链表还是红黑树?如果返回oldValue的机制,相信见注释。
扩容分为初始化空位,使用initialCpacity或者大于设置值的的the power of two!,和size超过了有效负载后的进行扩容的机制。下面详细介绍扩容方法的原理!同样以注释的方式进行讲解。在HashMap中,构造方法只是对参数进行初始化,而真正的创建一个满足初始化要求的桶数组,实在第一次put方法使用的时候,HashMap中所有的构造方法都只是对参数进行初始化,并没有真正的创建出来桶数组,而桶数组在第一次put方法的时候创建出来。这一点与ArrayList相反,ArraList带有参数的构造方法,会在构造方法的时候就创建一个输入参数大小的内置数组出来,而不带有参数的ArrayList的构造方法的内置数组与HashMap一致,实在第一次add数据的时候才会创建默认是10。
用一个流程图来总结put方法;
2:扩容流程
final Node<K,V>[] resize() {
//用oldTab指向table
Node<K,V>[] oldTab = table;
//如果当前table为null,则oldCap至0,若不为null,则将table.length赋给oldCap
//则oldCap始终为当前数组(table)的长度
int oldCap = (oldTab == null) ? : oldTab.length;
//保存原来的有效负载数,
int oldThr = threshold;
//创建新的Capacity和threshold为0
int newCap, newThr = ;
//如果原有容量大于0,第一次扩容是,原有的oldCap就是0.
if (oldCap > ) {
//如果已经超过了最大值,
if (oldCap >= MAXIMUM_CAPACITY) {
//将有效负载至为整形最大值,和恐怖
threshold = Integer.MAX_VALUE;
//并且返回原始的table,因为超过了最大值,所以不扩容了,直接返回原始的数组。
return oldTab;
}
//如果,将原来的oldCap扩到二倍赋给newCap,且小于最大容量,且oldCap是大于等于原始容量16的。
else if ((newCap = oldCap << ) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//则将oldThr也扩大二倍后赋给newThr,这里得到的newCapu和newThr都是原来的二倍。
newThr = oldThr << ; // double threshold
}
//假如,oldCap小于等于0,其实上面可以保证第一次扩容时,oldCap是等于0的。且oldThr大于0
else if (oldThr > ) // initial capacity was placed in threshold
//这将oldThr赋给newCap。
newCap = oldThr;
//如果再oldCap为0,且oldThr也不大于0情况下,使用默认是来初始化newCap和newThr,分别为16,16*0.75 = 12
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果newThr==0,则通过newCap和loadFactor来计算得到newThr
if (newThr == ) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//将newThr赋给threshold,同时使用newcCap来创建新的newTab
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//table指向newTab
table = newTab;
//如果oldTab不等于null,这下面进行的就是最复炸的操作,
//扩容后,将原数组,链表,红黑树的节点数据进行转移到新的数组中,
//进行重新的找到位置即可!
if (oldTab != null) {
//以oldCap为上限来循环
for (int j = ; j < oldCap; ++j) {
Node<K,V> e;
//遍历原数组,如果原数组位置不为null,则
if ((e = oldTab[j]) != null) {
//将该位置至null
oldTab[j] = null;
//在上面的if中,e = oldTab[j],即e已经指向了该数组的节点了
//oldTab[j] = null;这操作只是将数组指向null,并不会改变e已经指向了原来的节点
//这意味着这里只有一个节点,
if (e.next == null)
//加入e的next指向null,则将通过e的hash与上新的length-1索引的新的坐标
//将e放在新的数组位置。
newTab[e.hash & (newCap - )] = e;
else if (e instanceof TreeNode)
//如果该节点是treeNode,这进行红黑树的操作,暂时不会,,,,,,
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//如果不是红黑树,而是链表的话。
//
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//判断扩容后,原始链表需不需要移动改变位置,如果==0.则不需要改变,否则改变
//而改变位置为原下标+oldCap构成新的数组下标
if ((e.hash & oldCap) == ) {
//形成不需要改变位置的链表
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中
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
//将需要改变位置的链表,按照原位置+olodCap放在newTab中
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//返回扩容后的newTab
return newTab;
}
难点主要集中在扩容后,如何将原节点转移到新的数组中问题,扩容的判断和操作是很简单的。
主要分为链表和红黑树的转移问题。红黑树由于我不是很懂,暂时跳过。如上面注释的那样,现在原数组的节点链表中,分成两个链表:
需要index变化的链表,不需要index变化的链表。我们分析发现index变化的链表其变化后的index都是原始index+oldCap固定在,所以可以分成两个链表后,在统一在新的index位置直接将两个链表的head节点插入数组即可!
3:多线程问题
在JDK1.7中由于,是在扩容时,将链表会顺序反过来放在newTab中,所以多线程有形参循环链表的问题,而JDK1.8中都是在尾部放入新的节点,同时也是一次性的将一条链表移动到newTab而不是,循环的插入过程,所以不会再有多线程中的循环链表的问题出现。
与Hashtable比较:后者支持多线程,操作方法都被同步了;HashMap允许一个key为null,多个value为null,而Hashtable不允许有null值。
JDK1.8 HashMap学习的更多相关文章
- JDK1.8 HashMap源码分析
一.HashMap概述 在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里.但是当位于一个桶中的元素较多,即hash值相等的元素较多时 ...
- JDK1.8 HashMap$TreeNode.balanceInsertion 红黑树平衡插入
红黑树介绍 1.节点是红色或黑色. 2.根节点是黑色. 3.每个叶子节点都是黑色的空节点(NIL节点). 4 每个红色节点的两个子节点都是黑色.(从每个叶子到根的所有路径上不能有两个连续的红色节点) ...
- JDK1.8 HashMap$TreeNode.rotateLeft 红黑树左旋
红黑树介绍 1.节点是红色或黑色. 2.根节点是黑色. 3.每个叶子节点都是黑色的空节点(NIL节点). 4 每个红色节点的两个子节点都是黑色.(从每个叶子到根的所有路径上不能有两个连续的红色节点) ...
- jdk1.8 HashMap底层数据结构:深入解析为什么jdk1.8 HashMap的容量一定要是2的n次幂
前言 1.本文根据jdk1.8源码来分析HashMap的容量取值问题: 2.本文有做 jdk1.8 HashMap.resize()扩容方法的源码解析:见下文“一.3.扩容:同样需要保证扩容后的容量是 ...
- 为什么jdk1.8 HashMap的容量一定要是2的n次幂
一.jdk1.8中,对“HashMap的容量一定要是2的n次幂”做了严格控制 1.默认初始容量: [Java] 纯文本查看 复制代码 ? 1 2 3 4 /** * The default init ...
- HashMap 学习笔记
先摆上JDK1.8中HashMap的类注释:我翻译了一下 /** * Hash table based implementation of the <tt>Map</tt> i ...
- JDK1.7 HashMap 源码分析
概述 HashMap是Java里基本的存储Key.Value的一个数据类型,了解它的内部实现,可以帮我们编写出更高效的Java代码. 本文主要分析JDK1.7中HashMap实现,JDK1.8中的Ha ...
- JDK1.8 HashMap中put源码分析
一.存储结构 在JDK1.8之前,HashMap采用桶+链表实现,本质就是采用数组+单向链表组合型的数据结构.它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置.Hash ...
- HashMap 学习心得
1.构造 HashMap 底层数据结构线性数组,HashMap有一个静态内部类Entry,Entry有四个属性,key,value,next,hash Entry就是HashMap键值对实现的一个基础 ...
随机推荐
- 吴裕雄 Bootstrap 前端框架开发——Bootstrap 图片
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...
- gym 101911
A. Coffee Break 题意:每天有m小时,你喝咖啡需要花一小时,你想在n个时刻都喝过一次咖啡,老板规定连续喝咖啡的间隔必须是d以上,求最少需要多少天才能喝够n次咖啡,并输出每个时刻第几天喝. ...
- Linux学习《第五章用户文件权限管理》之补充学习
- PHP时间格式
date 用法: date(格式,[时间]); 如果没有时间参数,则使用当前时间.格式是一个字符串,其中以下字符有特殊意义: Y - 年,四位数字; 如: "1999" y - 年 ...
- JuJu团队11月27号工作汇报
JuJu团队11月27号工作汇报 JuJu Scrum 团队成员 今日工作 剩余任务 困难 于达 将真实数据处理后按矩阵读入, 以供训练使用 提供generator的接口 对julia语言还不够 ...
- POJ 2305:Basic remains 进制转换
Basic remains Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 5221 Accepted: 2203 Des ...
- JD-Store购物网站复盘——20170312
一.商店技术架构 1.主题 2.涉及技术点: 3.核心业务功能 4.角色 5.用户故事 二.实现步骤 专案基础设施 上传图片模块 购物车 订单 支付&寄信 专案源码 三.第三方服务应用 支付 ...
- 浅谈arduino的bootloader
在arduino的板子上,作为核心的avr单片机往往都会烧录一个bootloader,这个叫做bootloader的东东其实是arduino研发团队针对arduino板子开发的一小段代码,借助于这段代 ...
- Django——整体结构/MVT设计模式
MVT设计模式 Models 封装数据库,对数据进行增删改查; Views 进行业务逻辑的处理 Templates 进行前端展示 前端展示看到的是模板 --> 发起 ...
- 七十五、SAP中数据库的使用SQL
一.在SAP中可以使用两张数据库,一直是NativeSQL和OPEN SQL. Native SQL(本地SQL)特点: 1.每种关系型数据库都有其对应的 SQL,是数据库相关的. 2.不同的 SA ...