目录
一、底层数据结构
JDK版本 | 底层实现 |
JDK<=1.7 | 数组+链表 |
JDK>=1.8 | 数组+链表+红黑树 |
红黑树
红黑树在数据量大的时候性能会比链表要好,是一个自平衡的二叉搜索树,
使得查询的时间复杂度降为O(logn)
特点:
- 每个节点只有两种颜色:红色或者黑色
- 根节点必须是黑色
- 每个叶子节点(NIL)都是黑色的空节点
- 从根节点到叶子节点,不能出现两个连续的红色节点
- 从任一节点出发,到它下边的子节点的路径包含的黑色节点数目都相
性能临界点:
Hash值产生碰撞后,链表长度>8时会由链表转换为红黑树
而当红黑树的节点<6时,会由红黑树转换为链表,这就是二者的性能临界点。
//当链表长度过长时,会有一个阈值,超过此阈值8就会转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
//当红黑树上的元素个数,减少到6个时,就退化为链表
static final int UNTREEIFY_THRESHOLD = 6;
数据类型性能分析
数据类型 | 查询速度 | 优缺点 | 时间复杂度 |
数组 | 快 | 插入和删除比较困难 | O(1) |
链表 | 慢 | 插入和删除操作比较容易 | O(N) |
红黑树 | 中 | 自平衡的二叉搜索树 | O(logn) |
二、源码分析
2.1、数据结构定义
2.1.1、数组类型为Node[ ]
//存放所有Node节点的数组
transient Node<K,V>[] table;
2.1.2、链表结点类 Node
每个Node都保存某个KV键值对元素的key、value、hash、next等值。
由于next的存在,所以每个Node对象都是一个单向链表中的组成结点。
//普通单向链表节点类
static class Node<K,V> implements Map.Entry<K,V> {
//key的hash值,put和get的时候都需要用到它来确定元素在数组中的位置
final int hash;
final K key;
V value;
//指向单链表的下一个节点
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
2.1.3、红黑树结点类 TreeNode
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; //当前节点的父节点
TreeNode<K,V> left //左孩子节点
TreeNode<K,V> right;//右孩子结点
TreeNode<K,V> prev; //指向前一个节点
boolean red; //当前节点是红色或者黑色的标识
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
2.2、 HashMap构造函数
2.2.1、默认无参构造,指定一个默认的加载因子
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
2.2.2、可指定容量的有参构造
但是需要注意当前我们指定的容量并不一定就是实际的容量
public HashMap(int initialCapacity) {
//同样使用默认加载因子
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
2.2.3、可指定容量和加载因子
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);
}
2.2.4、可传入一个已有的map
前三个方法都没有进行数组的初始化操作,即使调用了构造方法此时存放HaspMap中数组元素的table表长度依旧为0 。
在第四个构造方法中调用了inflateTable()方法完成了table的初始化操作,并将m中的元素添加到HashMap中。
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
2.3、当新添加一个KV键值对元素时:
1.通过该元素的key的hash值,计算该元素在数组中应该保存的下标位置。
2.如果该下标位置如果已经存在其它Node对象,则采用链地址法(下面会讲到)处理hash冲突,
即将新添加的KV键值对元素将以链表形式存储。
3.将新元素封装成一个新的Nod对象,插入到该下标位置的链表尾部(尾插法)。
4.当链表的长度超过8并且数组长度大于64时,为了避免查找搜索性能下降,该链表会转换成一个红黑树。
三、解决Hash冲突
3.1、开放定址法
该方法也叫做再散列法,其基本原理是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi 。
3.2、再Hash法
这种方法就是同时构造多个不同的哈希函数: Hi=RH1(key) i=1,2,…,k。当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
3.3、链地址法(Java就是采用这种方法)
其基本思想: 将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
3.4、建立公共溢出区
这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表
四、基本属性
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认初始化大小 16
static final float DEFAULT_LOAD_FACTOR = 0.75f; //负载因子0.75
static final Entry<?,?>[] EMPTY_TABLE = {}; //初始化的默认数组
transient int size; //HashMap中元素的数量
int threshold; //判断是否需要调整HashMap的容量
五、添加--put()方法
在该方法中,添加键值对时,
1.进行table是否初始化的判断,如果没有进行初始化(分配空间,Entry[]数组的长度)。
2.然后进行key是否为null的判断,如果key==null ,放置在Entry[]的0号位置。
3.调用一个Hash()方法,得到当前key的一个hash值,
====>用于确定当前key应该存放在数组的那个下标位置
4.计算在Entry[]数组的存储位置,判断该位置上是否已有元素,
如果已经有元素存在,则遍历该Entry[]数组位置上的单链表。
5.判断key是否存在,如果key已经存在,
则用新的value值,替换点旧的value值,并将旧的value值返回。
6.如果key不存在于HashMap中,程序继续向下执行。
将key-vlaue, 生成Entry实体,添加到HashMap中的Entry[]数组中,调用addEntry()方法
public V put(K key, V value) {
if (table == EMPTY_TABLE) { //是否初始化
inflateTable(threshold);
}
if (key == null) //放置在0号位置
return putForNullKey(value);
int hash = hash(key); //计算hash值
int i = indexFor(hash, table.length); //计算在Entry[]中的存储位置
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); //添加到Map中
return null;
}
六、调用addEntry()
添加到方法的具体操作:
1.在添加之前先进行容量的判断,如果当前容量达到了阈值,并且需要存储到Entry[]数组中====>先进性扩容操作,空充的容量为table长度的2倍。
2.重新计算hash值和数组存储的位置,扩容后的链表顺序与扩容前的链表顺序相反。
3.然后将新添加的Entry实体存放到当前Entry[]位置链表的头部。
特此说明:
4.在1.8之前,新插入的元素都是放在了链表的头部位置,
但是这种操作在高并发的环境下容易导致死锁,
所以1.8之后,新插入的元素都放在了链表的尾部。
/*
* hash hash值
* key 键值
* value value值
* bucketIndex Entry[]数组中的存储索引
* /
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容操作,将数据元素重新计算位置后放入newTable中,
链表的顺序与之前的顺序相反
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
七、链表和红黑树互转
public V put(K key, V value) {
//调用putVal()方法完成
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;
//判断table是否初始化,否则初始化操作
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);
//链表长度8,将链表转化为红黑树存储
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//key存在,直接覆盖
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
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;
}
7.1、链表转红黑红树
链表的长度大于8的时候,就转换为红黑树
先判断table的长度是否大于64,如果小于64,就通过扩容的方式来解决,避免红黑树结构化。
链表长度大于8有两种情况:
- table长度足够,hash冲突过多
- hash没有冲突,但是在计算table下标的时候,由于table长度太小,导致很多hash不一致的
第二种情况是可以用扩容的方式来避免的,扩容后链表长度变短,读写效率自然提高。另外,扩容相对于转换为红黑树的好处在于可以保证数据结构更简单。
由此可见并不是链表长度超过8就一定会转换成红黑树,而是先尝试扩容
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//首先tab的长度是否小于64,
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//小于64则进行扩容
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
//否则才将列表转换为红黑树
TreeNode<K,V> hd = null, tl = null;
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);
}
}
7.2、红黑树转换为链表
所需条件:
扩容 resize( ) 时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表。
static final int UNTREEIFY_THRESHOLD = 6;//退化链表的临界值
//扩容时判断是红黑树结构时会执行split方法
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
//把红黑树中的结点依次添加到 low 和 high 两颗红黑树中
//还是依靠 (e.hash & bit) == 0 的位运算来判断属于哪颗树,bit是传过来的旧数组下标
for (TreeNode<K,V> e = b, next; e != null; e = next) {
......
if ((e.hash & bit) == 0) {
++lc; //添加到 loHead
}
else {
++hc;//添加到 hiHead
}
}
if (loHead != null) {
//如果low树的元素个数小于等于6,退化成链表
if (lc <= UNTREEIFY_THRESHOLD)
//并插入到新数组 tab[index] 的位置上,index是当前红黑树所在旧数组坐标
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
//bit是旧数组长度
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
7.3、小结
1、hashMap并不是在链表元素个数大于8就一定会转换为红黑树,而是先考虑扩容,扩容达到默认限制后才转换。
2、hashMap的红黑树不一定小于6的时候才会转换为链表,而是只有在resize的时候才会根据 UNTREEIFY_THRESHOLD 进行转换。