1.什么是HashMap
HashMap是Map集合框架下的一个实现子类,名字上加了一个Hash表明它是一个散列且无序的key-value集合,对HashMap的操作都是对key的操作,如put()、get()等方法,它要求key唯一(可以为null)且一个key对应一个value,value可以重复且多个value可以为null,同时它也是应用特别广泛的一种Map类。
HashMap类中主要定义了以下几个全局变量:size、loadFactor、threshold、DEFAULT_LOAD_FACTOR和DEFAULT_INITIAL_CAPACITY。
■ size:记录了Map中的键值对个数。
■ loadFactor:装载因子,用来衡量HashMap满的程度,默认值为0.75f(DEFAULT_LOAD_FACTOR=0.75f)。
■ threshold:临界值,当size超出临界值时,HashMap就会扩容,threshold=loadFactor*容量(capacity)。容量如果不指定,默认容量是16(static final int DEFAULT_INITIAL_CAPACITY = 1 << 4)。
■ size与capacity:size表示容器中实际的kv个数,而capacity表示最大的理论个数。
public static void main(String[] args) throws Exception {
HashMap<Integer,Integer> hashMap = new HashMap<>();
hashMap.put(1,1);
Method capacity = hashMap.getClass().getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity:" + capacity.invoke(hashMap)); //16
Method size = hashMap.getClass().getDeclaredMethod("size");
size.setAccessible(true);
System.out.println("size:" + size.invoke(hashMap)); //1
}
默认情况下HashMap的初始容量是16,之所以这样设计是由于可以使用位与替代取模来提升hash的效率。刚说过,当超过临界值时,hashMap会进行扩容,扩容值是原值的2倍,如16->32->64…
HashMap中还提供一个重载构造函数,可以指定初始容量,但指定之后的capacity是大于该值的最小的2次幂的值。
2.HashMap的实现原理
在Java1.8中,HashMap采用的是数组+链表+红黑树实现的,如图:
那问题来了:底层存储的到底是什么?这样的存储方式有什么优点?
(1)由源码可知,Node<K,V>[](哈希桶数组)是HashMap的结构主干,Node<K,V>是基本的组成单位。
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) { //构造函数
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
......
}
Node是HashMap的一个内部类,实现了Map.Entry<k,v>接口,本质是一个映射(键值对),上图中的每个黑点就是一个Node对象。
(2)HashMap就是使用哈希表《哈希表-百度百科》来存储的。哈希表为解决冲突,可以采用开放地址和链地址法等来解决问题,Java中的HashMap采用的就是链地址法,简单来说就是数据和链表的结合。
HashMap<String,String> map = new HashMap<>(); map.put("lee","小小");
执行put操作时,会调用lee这个key的hashCode()方法得到hashCode值,然后通过Hash算法的后两步运算(高位运算和取模运算)来定位该键值对的存储位置,当有两个以上的key定位到相同位置时,表示发生了hash碰撞。当然,Hash算法计算结果越分散均匀,发生碰撞的概率就越小,map的存取效率就越高。
此时又出现了一个新的问题:Hash算法的好坏决定着碰撞率的高低,同时数据的大小也决定着碰撞的概率,那怎样权衡空间成本与时间成本的关系呢?解决的方法就是Hash算法和扩容机制。推荐一篇博文《全网把Map中的hash()分析的最透彻的文章,别无二家》,在这篇博客中你会具体了解到什么是哈希,什么是碰撞,如何解决以及源码分析。
让我们再想一个问题,当较多数据在一个节点上怎么办?Java8为了解决这个问题,当链表长度超过默认值8时,就转为红黑树,利用红黑树快速的CRUD特点,提高HashMap的性能。
3.关于红黑树
红黑树是一种特殊的二叉查找树,满足二叉查找树的特征:任意一个节点所包含的键值,大于等于左孩子的键值,小于等于右孩子的键值。同时,也具有红黑树自身的一些特性:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点是黑色。注:这里叶子节点,是指为空的叶子节点 。
(4)如果一个节点是红色,则它的子节点必须是黑色的(反之不一定)。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
关于上面的特性,需要注意:
■ 特性(3)中的叶子节点为空节点,即NIL或null的节点。
■ 特性(5)确保没有一条路径会比其他路径长出两倍,所以红黑树是接近平衡的二叉树。
红黑树对平衡进行修正主要通过三种方式:改变节点颜色、左旋和右旋。
变色
之所以会变色,是违背了特性(4),上图当插入节点D时,由于节点B和D都是红色,根据特性(4)的描述,它会先将B节点变为黑色,同理将Y节点变为红色,然后将C变为黑色,然后根据特性(5)将A节点变为黑色。
左旋
逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代。如图,身为右孩子的Y取代了X的位置,而X变成了自己的左孩子,同时Y的左孩子变为X的右孩子。此为左旋转。
右旋
顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子取代。如图,父节点X通过右旋转变为左孩子Y的右孩子,同时Y的右孩子变为X的左孩子。详见程序员小灰灰-什么是红黑树和红黑树-原理与算法详解。
4.HashMap的PUT操作
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* 和 Java7 稍微有点不一样的地方就是,Java7 是先扩容后插入新值的,Java8 先插值再扩容
* 参数onlyIfAbsent如果是true,则只有在不存在该key时才会进行put操作
* 参数evict为false时,表处于创建状态,此参数我们不关心
*/
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) //第一次put时,会出发resize()操作,第一次的resize操作和后
n = (tab = resize()).length; //续扩容不同,因为这次是从null扩容到16或自定义的初始容量
if ((p = tab[i = (n - 1) & hash]) == null) //找到具体数组下标,如果位置没有值,则直接初始化Node,数据放到此位置
tab[i] = newNode(hash, key, value, null);
else { //数组该位置有值
Node<K,V> e; K k;
if (p.hash == hash && // 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点
((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) {
// 插入到链表的最后面(Java7 是插入到链表的最前面)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 9 个
// 会触发下面的 treeifyBin,也就是将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果在该链表中找到了"相等"的 key(== 或 equals)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 node
break;
p = e;
}
}
// e!=null 说明存在旧值的key与要插入的key"相等"
// 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
/**
* 数组扩容,每次扩容后容量为原来的两倍,并进行数据迁移
*/
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; // double threshold
}
// 对应使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的时候
else if (oldThr > 0)
newCap = oldThr;
else { // 对应使用 new HashMap() 初始化后,第一次 put 的时候
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];
// 如果是初始化数组,到这里就结束了,返回 newTab 即可
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);
// 这块是处理链表的情况,
// 需要将此链表拆成两个链表,放到新的数组中,并且保留原来的先后顺序
// loHead、loTail 对应一条链表,hiHead、hiTail 对应另一条链表
else { // preserve order
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;
// 第二条链表的新的位置是 j + oldCap
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
5.HashMap的GET操作
/**
* 计算 key 的 hash 值,根据 hash 值找到对应数组下标: hash & (length-1)
* 判断数组该位置处的元素是否刚好就是我们要找的,如果不是,走第三步
* 判断该元素类型是否是 TreeNode,如果是,用红黑树的方法取数据,如果不是,走第四步
* 遍历链表,直到找到相等(==或equals)的 key
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 判断第一个节点是不是就是需要的
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 判断是否是红黑树
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 链表遍历
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}