HashMap JDK1.8
在分析HashMap之前,我们先看一下Map接口
java.util.Map
在Map接口中我们主要观察三个地方
首先时两个方法
Set<K> keySet();
Collection<V> values();
ketSet方法可以看出其采用的是set来维护其key值,可以推出其key值是唯一的
而values()方法的返回值可以看出其采用的Collection,可以看出其value值是不唯一的
我们所需要看的另一个地方是在Map内部定义了一个Entry接口
interface Entry<K, V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
//....略...
}
通过这个接口,可以看出Map是通过接口Entry来维护key和value关系
HashMap
重要参数:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认的初始大小
static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量,因为2的最大有效幂次方为30,1>>31就会出界
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认加载因子
static final int TREEIFY_THRESHOLD = 8;//treeify_threshold 树形化阈值;即当链表的长度大于8的时候,会将链表转为红黑树,优化查询效率。链表查询的时间复杂度为o(n) , 红黑树查询的时间复杂度为 o(log n )
static final int UNTREEIFY_THRESHOLD = 6;//untreeify_threshold 解树形化阈值;其实就是当红黑树的节点的个数小于等于6时,会将红黑树结构转为链表结构。
static final int MIN_TREEIFY_CAPACITY = 64;//min_treefy_capacity 树形化的最小容量,桶容量必须达到64且链表长度达到TREEIFY_THRESHOLD = 8才可以树形化
transient Node<K,V>[] table;//哈希桶数组,维护所有key的hash不同的Entry,而相同hash值的Entry用链表/红黑树连接
transient Set<Map.Entry<K,V>> entrySet;//所有的Entry (key value)
transient int size;//数量
transient int modCount;//修改次数,用来统计删除/修改的次数,与线程安全有关(有点版本号的意思)
int threshold;//下次扩容时的size 扩容阈值
final float loadFactor;//负载因子
数据结构
数组+单链表+红黑树
其中:
Node<K,V>[] table: 哈希桶数组,明显它是一个Node的数组 。
而Node结构如下:实现了Entry接口,维护了key、value、hash以及下一个Node的引用。 本质是就是一个映射(键值对)
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //用来定位数组索引位置
final K key;
V value;
Node<K,V> next; //链表的下一个node
Node(int hash, K key, V value, Node<K,V> next) { ... }
public final K getKey(){ ... }
public final V getValue() { ... }
public final String toString() { ... }
public final int hashCode() { ... }
public final V setValue(V newValue) { ... }
public final boolean equals(Object o) { ... }
}
HashMap的结构
HashMap就是使用哈希表来存储的 .通过hash必定会有冲突,下面看看HashMap采用的hash算法
哈希算法
static final int hash(Object key) {
int h;
//当key==0时,其返回值为0,说明HashMap是可以存储key为null的,但是只允许一个
//不为0时,则取(h= key.hashCode()) ^ (h >>> 16)
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
得出hash来之后如何确定哈希桶数组的位置呢
i = (length - 1) & hash
length为数组的长度,其实就是取模操作
总结上面的过程,得出确定hash位置的三部:
- 获得hashcode
- 高位运算 hashcode^(hashcode>>>16)
- 取模运算(寻址算法)(采用的是与运算,桶的长度始终是2的次方,减一之后其二进制会有很多个1,其结果与高位运算之后的结果相与得出的就是桶位下表,其效果与取模运算相同,但是采用的是位运算其效率更高)
根据常识我们知道,即使再好的算法,如果数组很小其hash冲突的可能性也肯大,即使再坏,其hash冲突的可能性也很小。因此hashmap 中hash的好坏与 算法和容器大小都有关。但是我们不可能在初始就赋予其很大的数组,所以HashMap采用了扩容机制
-
为什么采用高16位与低16位进行与运算
直接做&运算那么高十六位所代表的部分特征就可能被丢失 将高十六位无符号右移之后与低十六位做异或运算使得高十六位的特征与低十六位的特征进行了混合得到的新的数值中就高位与低位的信息都被保留了。用异或能够保留各部分的特征。最终目的还是为了让哈希后的结果更均匀的分布,减少哈希碰撞,提升hashmap的运行效率
扩容机制
扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。
源码:
final Node<K, V>[] resize() {
Node<K, V>[] oldTab = table;//原桶数组
int oldCap = (oldTab == null) ? 0 : oldTab.length;//原数组容量
int oldThr = threshold;//原下次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)
//原容量x2
newThr = oldThr << 1; // 新的threshold也x2
}
//下面的if else与第一次初始化时有关
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;//将原数组位置置为null
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;//如果没有冲突,即不存在h单链表或者红黑树的情况,则直接赋值给新数组
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;
}
put流程
HashMap执行的是懒加载机制,当第一次Put的时候才会创建散列表table
共分四种情况:
- slot为null
- slot不为null,但尚未链化
- slot已经链化
- slot已经转化为红黑树
链表转换红黑树
条件
- 链表长度达到8
- 散列表数组长度达到64
HashMap中的TreeNode
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
//*****省略
}
TreeNode继承自LinkedHashMap中的内部类——LinkedHashMap.Entry,而这个内部类又继承自Node,所以算是Node的孙子辈了。我们再来看看它的几个属性,parent用来指向它的父节点,left指向左孩子,right指向右孩子,prev则指向前一个节点(原链表中的前一个节点),注意,这些字段跟Entry,Node中的字段一样,是使用默认访问权限的,所以子类可以直接使用父类的属性
线程不安全
在多线程使用场景中,应该尽量避免使用线程不安全的HashMap,而使用线程安全的ConcurrentHashMap