PS:本博客内容来源是根据Guide哥归纳的关于HashMap和ConcurrentHashMap的原理知识学习总结而来,他的JavaGuide是学习Java知识很棒的知识树,推荐大家去学习 传送门:JavaGuide 本博客是笔者仅用来记录学习的,如果能够帮助到大家也是非常开心的(❁´◡`❁)
HashMap
HashMap主要用来存放键值对,它是基于哈希表的Map接口实现的,常用的Java集合之一。
数据结构
JDK1.8之前HashMap底层是数组和链表结合在一起使用也就是链表散列。JDK1.8HashMap底层是数组+链表/红黑树的结构
hash方法
HashMap是通过key的hashcode()方法经过扰动函数处理过后得到hash值,然后通过(n-1)& hash判断当前元素存放的位置(n指的是数组的长度),如果当前元素存在元素的化,就判断该元素与要存入的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突;拉链法实现:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。既然讲到解决hash冲突,那就再简单回忆一下四种解决hash 的冲突吧
开发定址法:
线性探测:按顺序决定值时,如果某数据的值已经存在,则在原来的值的基础上往后加一个单位,直至不发生哈希冲突。再平方探测:按顺序决定值时,如果某数据的值已经存在,则在原来的值的基础上先加1的平方个单位,若仍然存在则减1的平方个单位。随后是2的平法,3的平方等,直至不发生哈希冲突。伪随机探测:按顺序决定值时,如果某数据已经存在,通过随机函数随机生成一个数,在原来值的基础上加上随机数,直至不发生哈希冲突。
链式地址法(HashMap的哈希冲突解决方法):对于相同的值,使用链表进行连接。使用数组存储每一个链表。建立公共溢出区:建立公共溢出区存储所有哈希冲突的数据。再哈希法:对于冲突的哈希值再次进行哈希处理,直至没有哈希冲突。
再回来看看扰动函数是什么,所谓扰动函数就是HashMap中的hash方法。使用hash方法也就是扰动函数是为了防止一些实现比较差的hashCode()方法,实则就是使用扰动函数以减少hash冲突。JDK1.7d的HashMap的hash方法源码:
static int hash(int h
) {
h
^= (h
>>> 20) ^ (h
>>> 12);
return h
^ (h
>>> 7) ^ (h
>>> 4);
}
JDK1.8较于上面的版本更为简洁,但原理不变。
static final int hash(Object key
) {
int h
;
return (key
== null
) ? 0 : (h
= key
.hashCode()) ^ (h
>>> 16);
}
构造函数
HashMap中有四个构造方法:
public HashMap() {
this.loadFactor
= DEFAULT_LOAD_FACTOR
;
}
public HashMap(Map
<? extends K, ? extends V> m
) {
this.loadFactor
= DEFAULT_LOAD_FACTOR
;
putMapEntries(m
, false);
}
public HashMap(int initialCapacity
) {
this(initialCapacity
, DEFAULT_LOAD_FACTOR
);
}
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
);
}
putMapEntries方法:
final void putMapEntries(Map
<? extends K, ? extends V> m
, boolean evict
) {
int s
= m
.size();
if (s
> 0) {
if (table
== null
) {
float ft
= ((float)s
/ loadFactor
) + 1.0F;
int t
= ((ft
< (float)MAXIMUM_CAPACITY
) ?
(int)ft
: MAXIMUM_CAPACITY
);
if (t
> threshold
)
threshold
= tableSizeFor(t
);
}
else if (s
> threshold
)
resize();
for (Map
.Entry
<? extends K, ? extends V> e
: m
.entrySet()) {
K key
= e
.getKey();
V value
= e
.getValue();
putVal(hash(key
), key
, value
, false, evict
);
}
}
}
类的属性
HashMap的初始容量是16,loadFacotr默认是0.75,越靠近1所能放的数据就越多,反则越少,这个参数越大会导致查找元素效率低,太小数组的利用率低。扩容条件是判断当前数量是否达到 默认容量*负载因子的值
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable
, Serializable
{
private static final long serialVersionUID
= 362498820763181265L
;
static final int DEFAULT_INITIAL_CAPACITY
= 1 << 4;
static final int MAXIMUM_CAPACITY
= 1 << 30;
static final float DEFAULT_LOAD_FACTOR
= 0.75f;
static final int TREEIFY_THRESHOLD
= 8;
static final int UNTREEIFY_THRESHOLD
= 6;
static final int MIN_TREEIFY_CAPACITY
= 64;
transient Node
<k,v>[] table
;
transient Set
<map
.entry
<k,v>> entrySet
;
transient int size
;
transient int modCount
;
int threshold
;
final float loadFactor
;
}
添加元素
JDK1.8HashMap 的put方法
如果定位到的数组位置没有元素就直接插入如果定位到的数组位置有元素就要和要插入的key比较,如果key相同就直接覆盖,然后就return;如果key不相同,就判断p是否是一个树节点,如果是就调用e=((TreeNode<K,V>)p).putTreeVal(this,tab,hash,key,value)将元素添加进入。如果不是就遍历链表插入(使用尾插法)
public V
put(K key
, V value
) {
return putVal(hash(key
), key
, value
, false, true);
}
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)
treeifyBin(tab
, hash
);
break;
}
if (e
.hash
== hash
&&
((k
= e
.key
) == key
|| (key
!= null
&& key
.equals(k
))))
break;
p
= e
;
}
}
if (e
!= null
) {
V oldValue
= e
.value
;
if (!onlyIfAbsent
|| oldValue
== null
)
e
.value
= value
;
afterNodeAccess(e
);
return oldValue
;
}
}
++modCount
;
if (++size
> threshold
)
resize();
afterNodeInsertion(evict
);
return null
;
}
JDK1.7HashMap的put方法
如果定位到的数据位置没有元素就直接插入如果定位到的数组位置有元素,遍历以这个元素为头节点的链表,依次和插入的key比较,如果key相同就直接覆盖,不同就采用头插法插入元素。
public V
put(K key
, V value
)
if (table
== EMPTY_TABLE
) {
inflateTable(threshold
);
}
if (key
== null
)
return putForNullKey(value
);
int hash
= hash(key
);
int i
= indexFor(hash
, table
.length
);
for (Entry
<K,V> e
= table
[i
]; e
!= null
; e
= e
.next
) {
Object k
;
if (e
.hash
== hash
&& ((k
= e
.key
) == key
|| key
.equals(k
))) {
V oldValue
= e
.value
;
e
.value
= value
;
e
.recordAccess(this);
return oldValue
;
}
}
modCount
++;
addEntry(hash
, key
, value
, i
);
return null
;
}
扩容
resize方法
扩容会判断是否超过最大扩容空间,如果没有超过则扩容为原来的两倍,遍历旧的Node<K,V>中的元素放入新的Node<K,V>中,否则不在扩容; 扩容操作会伴随者依次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的,在编程中尽量避免resize。阿里开发手册中是推荐根据实际情况需要自定义初始容量的
final Node
<K,V>[] resize() {
Node
<K,V>[] oldTab
= table
;
int oldCap
= (oldTab
== null
) ? 0 : oldTab
.length
;
int oldThr
= threshold
;
int newCap
, newThr
= 0;
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;
}
else if (oldThr
> 0)
newCap
= oldThr
;
else {
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 {
Node
<K,V> loHead
= null
, loTail
= null
;
Node
<K,V> hiHead
= null
, hiTail
= null
;
Node
<K,V> next
;
do {
next
= e
.next
;
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
;
}