一、摘要

  以下分析内容均是基于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源码剖析的更多相关文章

  1. HashMap源码剖析

    HashMap源码剖析 无论是在平时的练习还是项目当中,HashMap用的是非常的广,真可谓无处不在.平时用的时候只知道HashMap是用来存储键值对的,却不知道它的底层是如何实现的. 一.HashM ...

  2. 转:【Java集合源码剖析】HashMap源码剖析

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/36034955   您好,我正在参加CSDN博文大赛,如果您喜欢我的文章,希望您能帮我投一票 ...

  3. 基于JDK1.8版本的hashmap源码笔记(二)

    这一篇是接着上一篇写的, 上一篇的地址是:基于JDK1.8版本的hashmap源码分析(一)     /**     * 返回boolean类型的值,当集合中包含key的键值,就返回true,否则就返 ...

  4. 【Java集合源码剖析】HashMap源码剖析

    转载出处:http://blog.csdn.net/ns_code/article/details/36034955 HashMap简介 HashMap是基于哈希表实现的,每一个元素是一个key-va ...

  5. Java HashSet和HashMap源码剖析

    转自: Java HashSet和HashMap源码剖析 总体介绍 之所以把HashSet和HashMap放在一起讲解,是因为二者在Java里有着相同的实现,前者仅仅是对后者做了一层包装,也就是说Ha ...

  6. 【转】Java集合:HashMap源码剖析

    Java集合:HashMap源码剖析   一.HashMap概述二.HashMap的数据结构三.HashMap源码分析     1.关键属性     2.构造方法     3.存储数据     4.调 ...

  7. Java集合---HashMap源码剖析

    一.HashMap概述二.HashMap的数据结构三.HashMap源码分析     1.关键属性     2.构造方法     3.存储数据     4.调整大小 5.数据读取           ...

  8. [转载] Java集合---HashMap源码剖析

    转载自http://www.cnblogs.com/ITtangtang/p/3948406.html 一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射 ...

  9. Java集合:HashMap源码剖析

    一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap  ...

随机推荐

  1. RHEL简单管理SELINUX

    Security Enhanced Linux(SELinux)是一个额外的系统安全层,主要目的是防止已遭泄露的系统服务访问用户数据. 对于一个服务来说,要关注SELinux的三个方面,一是文件SEL ...

  2. 8.C++-类的关键字

    在之前学习的C++章节里,可以发现结构体越来越不像C语言里的结构体了 比如,里面可以定义函数,可以定义private/public,结构体名还可以指向父类. 但是C++需要兼容C,所以C++中便提供了 ...

  3. 【原】Spring Boot 配置swagger2没有文档解决方案

    @Bean public Docket customImplementation(){ return new Docket(DocumentationType.SWAGGER_2) .select() ...

  4. C#技术点--修改系统时间

    C#的System.DateTime类提供了对日期时间的封装,用它进行时间的转换和处理很方便,但是我没有在其中找到任何可以用来修改系统时间的成员.用过VC.VB等的朋友可能知道,我们可以调用Win32 ...

  5. C#接口--C#基础

    1.接口的声明 接口:描述属于任何类或者结构的一组相关功能,是一种规范.功能 组成:属性.方法.事件.索引或者这四种成员的任意组合构成 基本知识点: 1)接口默认的权限修饰符是:public,不允许加 ...

  6. Spring有什么缺点?

    Spring有什么缺点如下: 1.jsp中要写很多代码: 2.控制器过于灵活,缺少一个公用控制器: 3.不支持分布式部署.

  7. 【Luogu1876】开灯(数论)

    [Luogu1876]开灯(数论) 题面 题目描述 首先所有的灯都是关的(注意是关!),编号为1的人走过来,把是一的倍数的灯全部打开,编号为二的的把是二的倍数的灯全部关上,编号为3的人又把是三的倍数的 ...

  8. linux升级python3.6相关命令

    sudo apt-get install python3.6 sudo update-alternatives --install /usr/bin/python python /usr/bin/py ...

  9. RPC vs RESTful

    在微服务中,使用什么协议来构建服务体系,一直是个热门话题. 争论的焦点集中在两个候选技术: (binary) RPC or Restful. 以Apache Thrift为代表的二进制RPC,支持多种 ...

  10. 13.C++-静态成员变量、静态成员函数

    首先回顾下成员变量 能通过对象名能够访问public成员变量 每个对象的成员变量都是专属的 成员变量不能在对象之间共享 再来讲讲类的静态成员变量 介绍 静态成员变量属于整个类所有 静态成员变量的生命期 ...