HashMap
HashMap在开发中是一个用的非常多的容器,我们都知道它是以key-value的形式来存储数据,并使用它。但对于它是怎么实现快速存取value,它的数据结构和扩容机制。我做了简单的探索,如有误,请指正。
定义
HashMap实现了Map接口,继承AbstractMap。其中Map接口定义了键映射到值的规则,而AbstractMap类提供 Map 接口的骨干实现。HashMap 的实现不是同步的,这意味着它不是线程安全的。
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
成员变量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 建立hashMap初始容量为16。2的4次方
static final int MAXIMUM_CAPACITY = 1 << 30; 最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f; 默认的加载因子。
static final int TREEIFY_THRESHOLD = 8;这是1.8后新增的变量。判断是否要将链表转换为红黑数的临界值。目的是提高查 询效率。
static final int UNTREEIFY_THRESHOLD = 6; 跟上一变量相反,将红黑树转换成链表的临界值
static final int MIN_TREEIFY_CAPACITY = 64;红黑树的最小容量。如果没有达到这个阈值,即hash表容量小于MIN_TREEIFY_CAPACITY,当map中bin的数量太多时会执行resize扩容操作
对于不了解红黑树的,可以去查找相关的博文。我自己对红黑树了解也不够透彻,简单来说,红黑树是在进行插入和删除操作时通过特定规则保持二叉查找树的平衡,从而获得较高的查找性能。
构造函数
public HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap
public HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap
public HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap
public HashMap(Map< ? extends K, ? extends V> m):构造一个映射关系与指定 Map 相同的新 HashMap
这里有两个很重要的参数:initialCapacity(初始容量)、loadFactor(加载因子),看看JDK中的解释:
HashMap 的实例有两个参数影响其性能:初始容量 和加载因子。
容量 :是哈希表中桶的数量,初始容量只是哈希表在创建时的容量,实际上就是[] table的容量大小。
加载因子 :是哈希表在其容量自动增加之前可以达到多满的一种尺度。它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下我们是无需修改的。
当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
数据结构
HashMap是数组+链表的一个结构,链表是单向链表。Java中最常用的两种结构是数组和模拟指针(引用),几乎所有的数据结构都可以利用这两种来组合实现,HashMap也是如此。如下是它数据结构:
数组中存放着Node<key,value>节点。Node是hashMap中的一个内部类:
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
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;
}
/**
省略部分代码*/
}
它包含了键key、值value、下一个节点next,以及hash值,其中hash是用来计算该节点在table中存放的位置。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
位置index=hash&(n-1) n为hashMap的大小。等同hash%n,得到的范围是0~(n-1);从上面的代码可以看到key的hash值的计算方法。key的hash值高16位不变,低16位与高16位异或作为key的最终hash值。(h >>> 16,表示无符号右移16位,高位补0,任何数跟0异或都是其本身,因此key的hash值高16位不变。)因为,table的长度都是2的幂,高位在相与时都置为了0,因此index仅与hash值的低m位有关,发生hash冲突的可能性较大。其实hashCode分布的已经很不错了,而且当发生较大碰撞时也用树形存储降低了冲突。但我们仅仅只需要异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。
这里假设length为16和15,hash为5,6,7
再假设length为14:
碰撞的几率很大,出现多个下标没有数据。而当length = 16时,length – 1 = 15 即1111,那么进行低位&运算时,值总是与原来hash值相同,而进行高位运算时,其值等于其低位值。所以说当length = 2^n时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。
再来看看其中的构造函数:
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
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);
}
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
由此可以看到,当我们在实例化HashMap时,如果指定了初始容量,都会调用
tableSizeFor方法去找到大于等于指定初始容量的最小的2的次幂(如果initialCapacity已经是2的次幂了,返回是本身。因为对cap操作前已对cap做减1操作)。注意:得到的这个capacity却被赋值给了threshold(达到这个阈值时会进行扩容),是因为在构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中去调用resize方法。
存储操作
首先看源码:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
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) // -1 for 1st
treeifyBin(tab, hash);
break;
}
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;
}
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
代码中可以看出,在put之前都会先判断table数组是否为null或 ,如果为null,则先调用resize方法来返回一个table数组。然后通过(n-1)&hash计算在table中的位置,如果在该位置的node节点为null,就根据给定的key-value new一个node节点并放在该位置上。如果不为null,则先判断key的hash值和key值是否相等,若相等则对value值进行覆盖,再判断是否是treeNode节点(红黑树)来决定调用putTreeNode()方法,都不是的话就对该位置的链表进行遍历,if (binCount >= TREEIFY_THRESHOLD - 1) 是1.8之后设立的,长度足够长时采用红黑树存储,因为链表长度较长时查询性能会大大降低,这也就是为什么还要对hash值做一系列的计算来解决hash冲突原因,使得数据分布均匀。
扩容问题:
上段代码中标红的那段是用来判断数组是否需要扩容。该临界点在当HashMap中元素的数量等于table数组长度*加载因子。但是扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。并且扩容后还需重新计算hash值来调整元素在table中的位置。
<span style="color:#ff0000">/**Initializes or doubles table size **/</span>
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 ,向左移动一位即乘于2
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
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 { // 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) {//判断是否需要迁移位置。等于0时在原位置,不等于0时需迁移。(oldIndex+oldCap)
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;
}
//双倍扩容的同时,判断是否需要扩容的临界值也需要变为双倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold ,向左移动一位即乘于2
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
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 { // 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) {//判断是否需要迁移位置。等于0时在原位置,不等于0时需迁移。(oldIndex+oldCap)
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;
}
resize()方法具有两个功能,上段被注释的红色英文
<span style="color:#ff0000">Initializes or doubles table size</span>
意思是初始化table容量或者进行双倍扩容。至于为什么是双倍,是让数组的大小保持2的n次幂,这样才能得到length-1的二进制为***011111,让hash&(length-1)的结果才可能尽最大可能的分散。扩容后,会进行计算hash值来进行重新调整。操作代码为上述加粗的代码。首先是判断old是否为空,不为空是对数组进行遍历。需要对每个位置上的存储结构进行遍历。分为三中情况:情况一是该位置上只有一个节点,计算出该结点元素的新位置直接插入。情况二是该位置的存储结构是红黑树结构。则根据红黑树的规则来处理。情况三:该位置的存储结构是个单向链表。则需计算该链表上每个节点的新位置并插入。注意:链表上的每个节点元素的位置只有两种情况:原位置,原位置+oldCap(原容量大小)。看其中的代码:
<strong> if (<span style="color:#ff0000">(e.hash & oldCap) == 0</span>){/*省略代码*/}</strong>
其中的判断条件:e.hash & oldCap==0;判断的条件中e.hash的值没有变化。
看到这里不得不佩服设计者的思维。将位运算运用得淋漓尽致。
取数据:
源码:
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;
}
在这里能够根据key快速的取到value除了和HashMap的数据结构密不可分外,HashMap在存储过程中并没有将key,value分开来存储,而是当做一个整体key-value来处理的,系统根据key的hashcode来决定Node在table数组中的存储位置,在取的过程中同样根据key的hashcode取出相对应的Entry对象。