目录
JDK1.8 HashMap的底层数据结构是数组+链表+红黑树
一.HashMap底层数据结构原理
JDK1.7 HashMap的底层数据结构是数组+链表
1.7中链表插⼊使⽤的是头插法
1.7中哈希算法⽐较复杂,存在各种右移与异或运算
JDK1.8 HashMap的底层数据结构是数组+链表+红黑树
1.8中链表插⼊使⽤的是尾插法,因为1.8中插⼊key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使⽤尾插法
1.8中进⾏了简化,因为复杂的哈希算法的⽬的 就是提⾼散列性,来提供HashMap的整体效率,⽽1.8中新增了红⿊树,所以可以适当的简化哈希 算法,节省CPU资源
二.HashMap的扩容机制
JDK1.7版本扩容步骤
1.
先⽣成新数组
2.
遍历⽼数组中的每个位置上的链表上的每个元素
3.
取每个元素的key,并基于新数组⻓度,计算出每个元素在新数组中的下标
4.
将元素添加到新数组中去
5.
所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
JDK1.8版本扩容步骤
1.
先⽣成新数组
2.
遍历⽼数组中的每个位置上的链表或红⿊树
3.
如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
4.
如果是红⿊树,则先遍历红⿊树,先计算出红⿊树中每个元素对应在新数组中的下标位置
a.
统计每个下标位置的元素个数
b.
如果该位置下的元素个数超过了8,则⽣成⼀个新的红⿊树,并将根节点的添加到新数组的对应
位置
三.HashMap的Put⽅法
HashMap的Put⽅法的⼤体流程:
1.
根据Key通过哈希算法与与运算得出数组下标
2.
如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是Node对象)并放⼊该位置
3.
如果数组下标位置元素不为空,则要分情况讨论
a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成 Entry对象,并使⽤头插法添加到当前位置的链表中
b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node
ⅰ. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去 在这个过程中会判断红⿊树中是否存在当前key,如果存在则更新value
ⅱ. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过 尾插法插⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表 过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链 表Node插⼊到链表中,插⼊到链表后,会看当前链表的节点个数,如果⼤于等于 8,那么则会将该链表转成红⿊树
ⅲ. 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容, 如果需要就扩容,如果不需要就结束PUT⽅法
四.ConcurrentHashMap的扩容机制
1.7版本ConcurrentHashMap的扩容机制
1.
1.7版本的ConcurrentHashMap是基于Segment分段实现的
2.
每个Segment相对于⼀个⼩型的HashMap
3.
每个Segment内部会进⾏扩容,和HashMap的扩容逻辑类似
4.
先⽣成新的数组,然后转移元素到新数组中
5.
扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
1.8版本ConcurrentHashMap的扩容机制
1.
1.8版本的ConcurrentHashMap不再基于Segment实现
2.
当某个线程进⾏put时,如果发现ConcurrentHashMap正在进⾏扩容那么该线程⼀起进⾏扩容
3.
如果某个线程put时,发现没有正在进⾏扩容,则将key-value添加到ConcurrentHashMap中,然
后判断是否超过阈值,超过了则进⾏扩容
4.
ConcurrentHashMap是⽀持多个线程同时扩容的
5.
扩容之前也先⽣成⼀个新的数组
6.
在转移元素时,先将原数组分组,将每组分给不同的线程来进⾏元素的转移,每个线程负责⼀组或多组的元素转移⼯作
五.Hashtable 与 HashMap 的区别
1、两者父类不同
HashMap
是继承自
AbstractMap
类,而
Hashtable
是继承自
Dictionary
类。不过它们都实现了同时
实现了
map
、
Cloneable
(可复制)、
Serializable
(可序列化)这三个接口。
2、对外提供的接口不同
Hashtable
比
HashMap
多提供了
elments()
和
contains()
两个方法。
elments()
方法继承自
Hashtable
的父类
Dictionnary
。
elements()
方法用于返回此
Hashtable
中的
value
的枚举。
contains()
方法判断该
Hashtable
是否包含传入的
value
。它的作用与
containsValue()
一致。事实
上,
contansValue()
就只是调用了一下
contains()
方法。
3、对null的支持不同
Hashtable
:
key
和
value
都不能为
null
。
HashMap
:
key
可以为
null
,但是这样的
key
只能有一个,因为必须保证
key
的唯一性;可以有多个
key
值对应的
value
为
null
。
4、安全性不同
<1>HashMap是线程不安全的,在多线程并发的环境下,可能会产生死锁等问题,因此需要开发人员自 己处理多线程的安全问题。
<2>Hashtable是线程安全的,它的每个方法上都有
synchronized
关键字,因此可直接用于多线程中。
<3>虽然HashMap
是线程不安全的,但是它的效率远远高于
Hashtable
,这样设计是合理的,因为大部分的使用场景都是单线程。
<4.1>当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。
<4.2>ConcurrentHashMap虽然也是线程安全的,但是它的效率比
Hashtable
要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
5、初始容量大小和每次扩充容量大小不同
创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。
创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小。
6、计算hash值的方法不同
Hashmap计算hash值的源码:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
注:key值的hash值的计算方法为:先调用hashCode方法计算出来一个hash值,再将hash与右移16位后相异或,从而得到新的hash值。
Hashtable计算hash值的源码:
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
注:Hashtable通过计算key的hashCode()**来得到hash值就为最终hash值。