java1.8版本的HashMap源码剖析
一、摘要
以下分析内容均是基于JDK1.8产生的,同时也和JDK1.7版本的hashmap做了一些比较。在1.7版本中,HashMap的实现是基于数组+链表的形式,而在1.8版本中则引入了红黑树,但其实好多内容都是相同的。
从上面图中可以看出,HashMap等于数组+链表+红黑树三者结合。当进来的数据被Hash后会得到一个数组的下标,从而可以找到对应的位置,当该数组元素存在元素时,则会相应的以链表的形式给出,同时我们想取出value值时也要相应对key进行equals才能找到相应的位置,当链表长度大于8时,则会转换成红黑树来表示。
二、源码分析
1、HashMap主要的成员值:
//源码英文注释均舍去
//初始化Node数组容量16
static final int DEFAULT_INITIAL_CAPACITY = << ;
//初始化最大的数组容量
static final int MAXIMUM_CAPACITY = << ;
//初始化负载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//由链表转红黑树的临界值
static final int TREEIFY_THRESHOLD = ;
//由红黑树转链表的临界值
static final int UNTREEIFY_THRESHOLD = ;
//桶可能被转化为树形结构的最小容量的临界值
static final int MIN_TREEIFY_CAPACITY = ;
//计数器
transient int modCount;
//Node数组扩容的临界值,第一次为12
int threshold;
2、HashMap主要的构造方法
HashMap中有四个构造方法,但这四个构造方法主要的目的还是在于初始化数组的容量以及负载因子(这个变量涉及到数组的扩容的问题),下面仅拿一个构造函数来讲:
public HashMap(int initialCapacity, float loadFactor) {
//判断初始化数组的容量大小
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//判断初始化的负载因子
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//初始化负载因子
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
3、HashMap中主要的方法分析
a、putVal()方法
当我们使用map.put时,该方法会去调用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)
//会对该桶进行第一次初始化,桶的数组大小为16
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;
//判断桶下标中存在的第一个元素的hash值和key值是否相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//相等的话则用e来进行记录
e = p;
else if (p instanceof TreeNode)
//hash值相等,key不相等则判断标中存在的第一个元素是否为树的节点
//是的话则将元素添加到树节点上
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//hash值相等,key不相等放到链表中
else {
for (int binCount = 0; ; ++binCount) {
//判断该链表尾部指针是不是空的
if ((e = p.next) == null) {
//在链表的尾部创建链表节点
p.next = newNode(hash, key, value, null);
//判断链表的长度是否达到转化红黑树的临界值,临界值为8
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已经存在的情况下,再来一个相同的hash值、key值时返回新来的value这个值
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;
}
b、resize()方法
在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//判断旧的table大小
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
}
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) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//上面这些均是代表对桶的大小进行一些判断并初始化
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//扩容之后对旧的桶进行重新分配,打散到其他的位置,使其均匀的分散
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> 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<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;
//将同一桶中的元素根据(e.hash & oldCap)是否为0进行分割
//为0的话则保留在原始的位置
//不为0的话则将其移动到原始位置+增加的数组大小(比如第二次扩容时,这时值就为16)这个位置上面
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;
}
c、treeifyBin()方法
在putVal()方法中,我们能够看到,当链表的长度大于TREEIFY_THRESHOLD这个临界值时,这个时候就会调用treeifyBin()方法,将链表的结构转化为红黑树结构,这也是JDK1.8版本新优化的功能点
在此方法中主要做了:
1、判断桶是否初始化、或者判断桶中的元素个数是否达到MIN_TREEIFY_CAPACITY阈值,没有的话则去进行初始化或者扩容
2、若不符合上述条件,则会对其进行树形化,首先会先去遍历桶中链表的元素,并创建相同的树节点,接着会根据桶的第一个元素而去创建树的头结点,并以此建立联系
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;
//对桶Node中的链表元素进行循环,从链表的头节点开始将链表的头元素改为树的头节点
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);
}
}
三、细节注意
1、在上述方法中,我们经常看到在进行当前元素是否相同时会去进行判断,如果仅仅是对值的hashCode进行判断,当hash值相同时,则会发生Hash碰撞,这个时候利用链表的形式去解决hash碰撞的问题,当碰撞发生了,则会将元素存放在链表的下一个节点中,同时在判断两个是否是同一个元素时,需要去判断当且仅当hashCode()和equal都相等时才能判断这两个元素是相等的,两元素相同时则会用新的value替换掉旧的value值
2、在对桶进行扩容时,当桶的实际使用大小超多了0.75*桶的容量时,这个时候要对其进行扩容,同时扩容之后原桶上的元素的位置也会从新被打散,其判断条件是通过值的hash与上原始的容量,若等于0则停留在原始的位置不动,若等于1则新的位置=原始的位置+新增了多少个数组
3、当链表的长度大于8时,这个时候就需要将链表树形化转换成红黑树
4、根据(n - 1) & hash来判断桶的数组大小最好是2的幂次方,如果length不是2的次幂,比如length为15,则length-1为14,对应的二进制为1110,在于h与操作,最后一位都为0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了
java1.8版本的HashMap源码剖析的更多相关文章
- HashMap源码剖析
HashMap源码剖析 无论是在平时的练习还是项目当中,HashMap用的是非常的广,真可谓无处不在.平时用的时候只知道HashMap是用来存储键值对的,却不知道它的底层是如何实现的. 一.HashM ...
- 转:【Java集合源码剖析】HashMap源码剖析
转载请注明出处:http://blog.csdn.net/ns_code/article/details/36034955 您好,我正在参加CSDN博文大赛,如果您喜欢我的文章,希望您能帮我投一票 ...
- 基于JDK1.8版本的hashmap源码笔记(二)
这一篇是接着上一篇写的, 上一篇的地址是:基于JDK1.8版本的hashmap源码分析(一) /** * 返回boolean类型的值,当集合中包含key的键值,就返回true,否则就返 ...
- 【Java集合源码剖析】HashMap源码剖析
转载出处:http://blog.csdn.net/ns_code/article/details/36034955 HashMap简介 HashMap是基于哈希表实现的,每一个元素是一个key-va ...
- Java HashSet和HashMap源码剖析
转自: Java HashSet和HashMap源码剖析 总体介绍 之所以把HashSet和HashMap放在一起讲解,是因为二者在Java里有着相同的实现,前者仅仅是对后者做了一层包装,也就是说Ha ...
- 【转】Java集合:HashMap源码剖析
Java集合:HashMap源码剖析 一.HashMap概述二.HashMap的数据结构三.HashMap源码分析 1.关键属性 2.构造方法 3.存储数据 4.调 ...
- Java集合---HashMap源码剖析
一.HashMap概述二.HashMap的数据结构三.HashMap源码分析 1.关键属性 2.构造方法 3.存储数据 4.调整大小 5.数据读取 ...
- [转载] Java集合---HashMap源码剖析
转载自http://www.cnblogs.com/ITtangtang/p/3948406.html 一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射 ...
- Java集合:HashMap源码剖析
一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap ...
随机推荐
- Linux显示邮件状态等信息
Linux显示邮件状态等信息 youhaidong@youhaidong-ThinkPad-Edge-E545:~$ finger -l Login: youhaidong Name: youhaid ...
- 从“跳一跳”来看微信小程序的未来
从“跳一跳”来看微信小程序的未来 相信大家这两天都被微信新推出的小程序跳一跳刷爆了朋友圈,为了方便用户在使用过程中切换小程序,微信在这次6.6.1版本中加入了下拉可快速切换小程序的功能,而“跳一跳 ...
- Centos7.2 搭建Lamp服务器以及迁移WordPress个人博客详细过程
其实自己的博客搭了有段时间了,但是由于自己不太确定是不是一定要用wd的框架,以及实验室公网服务器的不稳定,就一直荒废着. 今天偶然间看到了腾讯云对于学生的优惠活动,毕业之前每月只要8元的云服务器(就算 ...
- C#接口--C#基础
1.接口的声明 接口:描述属于任何类或者结构的一组相关功能,是一种规范.功能 组成:属性.方法.事件.索引或者这四种成员的任意组合构成 基本知识点: 1)接口默认的权限修饰符是:public,不允许加 ...
- 将nodejs代码部署到阿里云服务器
概述 最近在做一个小项目,其中用nodejs做了个数据转发的接口,之后需要将这部分代码部署到服务器上面,并使用Nginx做反向代理.期间使用搜索引擎大量查阅了其他同鞋的经验,不过写的大多很笼统,因此踩 ...
- 【小白学爬虫连载(10)】–如何用Python实现模拟登陆网站
Python如何实现模拟登陆爬取Python实现模拟登陆的方式简单来说有三种:一.采用post请求提交表单的方式实现.二.利用浏览器登陆网站记录登陆成功后的cookies,采用get的请求方式,传入c ...
- 零基础新手学习Java必须知道的市场行情
Java如今的市场不如从前,竞争很大,工资非常高,标准非常高,想要胜任一份高薪的工作不是那么容易,只有掌握最新的行情才能更好的了解Java,才能更好的在这个领取发展,让新手小白了解Java市场行情如下 ...
- Jenkins + Github持续集成构建Docker容器,维基百科&人工自能(AI)模块
本文分两部分,第一部分是手动计划任务的方式构建Github上的Docker程序,第二部分是用Github webhook Trigger一个自动构建任务. Jenkins采用2.5版本Docker采用 ...
- luogu【P2753】[USACO4.3]字母游戏Letter Game
这个题...一开始看了很久题目(并且问了机房几个大佬)才明白题意.. (原题入口) 题意 大概是一开始给你一些字母出现的次数 你之后组成的单词(最多两个单词)每个字母出现次数 必须小于或等于标准(st ...
- 【BZOJ2337】Xor和路径(高斯消元)
[BZOJ2337]Xor和路径(高斯消元) 题面 BZOJ 题解 我应该多学点套路: 对于xor之类的位运算,要想到每一位拆开算贡献 所以,对于每一位拆开来看 好了,既然是按位来算 我们就只需要计算 ...