HashMap
的总结,有错误请留言.HashMap
是在JDK1.2中引入的一种K/V对
形式的集合类.HashMap
经过数组和单链表组合的结构形式来存储数据,数组在这做为一个外部结构,数组中的每一个节点被称作Bucket(桶)
,而桶是由在单链表构成,JDK1.8
以后为了解决长链表下,查询和插入效率低下的状况,又引入了红黑树的做为桶的实现方式,HashMap
定义的Node
内部类生成的,是普通的链表节点类.HashMap
是线程不安全的,在JDK1.8
以前多线程状况下甚至可能会出现环路(后面会讲),因此多线程状态下仍是要使用ConcurrentHashMap
的.HashMap
的参数很少,除去当作默认属性的静态常量和底层数组对象,就只有如下五个transient Node<K,V>[] table;
transient int size
transient int modCount;
int threshold;
final float loadFactor;
复制代码
table
就是整个HashMap
的底层数组,table
的初始化并不在构造函数中完成,而是在resize()
方法中完成.html
table
的初始化可能有点绕,构造函数中最多指定了阈值threshold
和负载因子loadFactor
并无容量相关,可是在resize()
方法中会根据旧容量和旧阈值判断新容量是等于默认容量,旧阈值或者两倍旧容量,最后根据新容量建立新数组loadFactor
就是所谓的负载因子,默认为0.75,是控制扩容时机的关键属性,由于扩容发生在当前元素个数超过阈值时,而阈值等于当前容量乘以负载因子.java
modCount
为修改计数,是fast-fail
机制的关键参数.在对Map
中的元素作新增/删除操做时会自增,但修改不会(putVal()方法中覆盖原值)git
HashMap
的新增过程重点主要仍是定位,如何肯定元素在数组中的位置,HashMap
采用的就是Hash算法
HashMap
会根据Key
的hash值,按照表达式(n - 1) & hash
计算出桶的下标Node
,做为链表的第一个元素,直接存放在数组中.(之前还据说过什么链表首节点为空的状况,是假的.)TreeNode
的方法插入到树中.table
是否初始化,新增后会判断该桶大小是否超过的8,超过则转化为红黑树,再判断整个数组是否须要扩容.Hash
同时也叫散列,能够把任意长度的输入经过算法,换算成固定长度的输出,不一样元素经过Hash
算法得到的下标一致能够被称之为冲突或者碰撞
,Hash
算法的要求就是使元素尽可能少的发生碰撞,从而均匀的散布在数组中
.而发生碰撞时,像HashMap
这种以一个列表下挂的方式能够被称为拉链法
.get()
方法,经过key
值查找的状况,若是本身遍历的另说.
(n - 1) & hash
计算出桶的下标(能够说是至关重要了),若获得的桶为空,直接返回nullkey.equals(k)
判断是否相等JDK1.8
以后引入的红黑树做为桶的另外一种实现方法.当链表长度大于8
时,桶的实现会转化为红黑树
.HashMap
的性能很大一部分取决于Hash
算法..经过插入和查找咱们能够知道,在数组大小不变的状况下,链表越长或者说树的高度越高都会致使操做性能下降,因此此时颇有必要经过扩容数组的方式,从新排列桶中元素,下降链表长度,减小树的高度.github
首先,触发扩容的状况是size > threshold
即元素个数大于阈值.整个扩容过程能够简单的拆分为如下几步:算法
1 << 30
也就是2^30.resize()
方法中从新散布元素的方法仍是颇有意思的(除去单元素链表和红黑树(桶的容量在1~7之间)shell
lo
和hi
(源码是loHead和hiHead,我猜是low和high,怎么缩写这么随意),lo
表示0到旧容量大小部分,hi
表示余下算是新加入的部分,并以此建立两个链表的节点e.hash & oldCap
判断元素是否分布在lo
部分,是就挂到lo
链表下面,否就挂到hi
链表下面.lo
链表挂到和旧数组相同位置的桶,而hi
则挂到下标为原下标 + 旧数组容量
的桶.e.hash & (oldCap - 1) + oldCap == e.hash & (oldCap << 1) -1
能够看出resize()
方法会调整所有的元素散列状况,所以过于频繁的resize
会下降HashMap
的性能,所以若是一开始能够大概知道所须要存放的元素个数时,尽可能直接指定容量大小.数组
JDK1.7
以前的resize()
方法在并发条件下可能会发生闭环问题,但在JDK1.8
以后不会在出现,但并不表明HashMap
能够在并发条件下使用了,小部分状况仍是会出现数据丢失等问题.安全
介绍JDK1.8
以前的闭环问题详情的文章多线程
HashMap的懒加载问题并发
HashMap
的源码,你会发现底层数组table
的建立其实并非在构造函数中完成的,而是resize()
方法中,这就是所谓的懒加载
,数组对象并不是是在一开始就建立的,而是在第一次插入操做以前完成的。static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
复制代码
扰动函数的逻辑很简单就是将hashCode
的高16位和低16位异或.
扰动函数的做用就是增长散列的随机性,使元素可以更均匀的分布在数组中,减小冲突从而捎带提升性能.
至于为何,能够看hash(*)
用到的地方,hash(*)
被用来计算元素的下标.而下标的计算公式以下
tab[i = (n - 1) & hash] // n表示数组的长度
复制代码
由于HashMap
的容量必定会是2的次幂,因此减1以后转化为二进制会变为一串0加一串1的,例如长度为4时,减去1,就会变为000…00011
(前面30个0),再结合&
能够发现他只使用了hashCode
的末尾几位,高位是所有没用.
而通过扰动函数,将高16位和低16位异或以后至关于高低位都用到了,其散列的随机性也就增长了.
(length - 1) & hash)
代替hash % length
,相对来讲位运算性能更佳,速度更快。(length - 1) & hash
的方式计算下标以后,若是不是二次幂的容量,出现碰撞的概率将会大大增长,例如咱们取17做为容量((17 -1) => 0001000
),通过&
与运算,能够想象会有一大批的元素直接挂在0号桶。hash & length
的话,也不用要求容量必定是二次幂,但各方面的性能老是会差一点的。HashTable
都没用过了,但之前还稍微看过HashTable
是线程安全的,暴力的加方法级synchronized
.而HashMap
是线程不安全的,并发状况下可能会出现数据丢失等状况.HashTable
不容许null值,而HashMap
容许null值.(包括key和value)HashCode
的使用不一样,HashTable
是直接调用hashCode
,而HashMap
会通过扰动函数.并且HashMap
中用&
代替了%
HashTable
数组默认是11,且增加为2n+1
,而HashMap
默认为16,增加为2n
,且硬性要求长度为2的次幂.HashTable
并非和HashMap
同样继承自AbstractMap
的,它继承自一个独立的父类AbstractDictionary