1.基本原理:
2.存储结构:
jdk1.7:采用链地址法来存储数据,即数组和链表结合的方式。
数组中每个元素存储的都是一个链表,当不同的key经过hash运算如果得到相同的值,那么我们就把key放在数组的链表上
jdk1.8:当链表新增节点导致链表长度超过8的时候,就会将原有链表转化为红黑树来存储数据(红黑树查询的时间复杂度O(logn))
3.重要参数:
1.哈希桶(buckets):哈希桶来形象表示数组中每个元素的位置。
2.初始容量(initial capacity):哈希桶的初始容量,默认为16
3.树化阈值(THREEIFY_THRESHOLD):一个哈希桶的节点个数大于该值的时候(默认为8)将会被转化为红黑树存储结构
4.最小树化容量(MIN_TREEIFY_CAPACITY):哈希表的数量超过最小树化容量,才可以转化为红黑树(64)
5.加载因子(load factor):加载因子时衡量哈希桶在其扩容之前可以达到多满的一种尺度,默认0.75
6.扩容阈值(threshold):容量*加载因子
问:为何加载因子默认为0.75?
答:通过源码里的javadoc注释看到,元素在哈希表中分布的桶频率服从参数为0.5的泊松分布,具体可以参考下StackOverflow里的解答:https://stackoverflow.com/questions/10901752/what-is-the-significance-of-load-factor-in-hashmap
问:为何数组容量必须是2次幂?
答:索引计算公式为i = (n - 1) & hash,如果n为2次幂,那么n-1的低位就全是1,哈希值进行与操作时可以保证低位的值不变,从而保证分布均匀,效果等同于hash%n,但是位运算比取余运算要高效的多。
//初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树形阈值
pubic static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
//最小树化容量
static final int MIN_TREEIFY_CAPACITY = 64;
//哈希桶
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount;
//加载因子
final float loadFactor;
4.存储单元:
(1)jdk1.7 :Entry
(2)jdk1.8:内部类 Node 实现基类的内部接口 Map.Entry<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(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; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//节点相同时用到
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
5.构造方法:
1. 指定初始容量和加载因子的构造函数
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);
}
//返回大于cap的并且时2^n的扩容阈值
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;
}
2.只指定初始容量的构造函数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
3.无参数构造函数
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
6.hashMap添加方法实现:(时间复杂度:O(1),O(n),O(logN)都有可能)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
先看一下hash函数的实现
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
解析:(h = key.hashCode()) ^ (h >>> 16)
key.hashCode()获取key的hash码,然将hash码右移16位与原hash进行异或运算
将hash码的高十六位与低十六位进行异或运算,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性 。
为什么要加大低位的随机性的,因为hash码是不能直接当作下标访问hashMap的主数组的(hash码太大),还需要经过这么一步。
(n - 1) & hash
因为容量为2^n,减一之后与hash进行&操作,就只保留了低位,正好可以用来进行数组访问的下标,而如果只保留低位,还是会产生碰撞,因而经过扰动函数,混淆高位与低位,使低位参杂了高位的部分特征,能够减少碰撞。
过程如图:
顺便说一下jdk1.7的hash函数实现:
//4次位运算 + 5次异或运算
static final int hash(Object k) {
int h = 0;
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
看完了hash函数,就可以接着看putVal了
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;
}
1.如果table为null,则代表第一次插入元素,需要进行首次扩容,此时n为首次扩容后的长度
2.计算键值对在table表中的索引位置
3.判断索引位置是否有元素,如果没有元素,则直接将元素插入该位置,如果有元素,且key值相同,则覆盖该元素。
4. 如果有元素但key值不相同,则判断是否为红黑树,如果为红黑树则在红黑树插入
5.如果不为红黑树则遍历该元素对应的链表,找到key值相同的元素并覆盖,然后退出,如果没有找到该元素,则在链表尾部新建一个节点(尾插法),jdk1.7为头插法。
6.如果插入节点成功,判断链表长度是否大于树化阈值,大于则转化为红黑树
7.最后判断数组长度是否大于扩容阈值,大于则进行扩容
转自:https://juejin.im/post/5ac83fa35188255c5668afd0
7.扩容:
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; //扩容阈值变为2倍
}
else if (oldThr > 0) // 阈值大于0
newCap = oldThr;
else { // 数组为空且未指定阈值
newCap = DEFAULT_INITIAL_CAPACITY;//默认大小
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//阈值=加载因子*默认大小
}
if (newThr == 0) {//阈值为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) {
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;
}
(1)首次调用扩容方法流程图
(2)非首次调用扩容方法:
计算位置时jdk1.7:
jdk1.8 进行链表拆分:
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;
}
}
1.我们首先准备了两个链表 lo
和 hi
, 然后我们顺序遍历该存储桶上的链表的每个节点, 如果 (e.hash & oldCap) == 0
, 我们就将节点放入lo
链表, 否则, 放入hi
链表.
2.如果lo链表非空, 我们就把整个lo链表放到新table的j
位置上,如果hi链表非空, 我们就把整个hi链表放到新table的j+oldCap
位置上
原理:https://segmentfault.com/a/1190000015812438?utm_source=tag-newest
我们假设 oldCap = 16, 即 2^4,
16 - 1 = 15, 二进制表示为 0000 0000 0000 0000 0000 0000 0000 1111
可见除了低4位, 其他位置都是0(简洁起见,高位的0后面就不写了), 则 (16-1) & hash
自然就是取hash值的低4位,我们假设它为 abcd
.
以此类推, 当我们将oldCap扩大两倍后, 新的index的位置就变成了 (32-1) & hash
, 其实就是取 hash值的低5位. 那么对于同一个Node, 低5位的值无外乎下面两种情况:
0abcd
1abcd
其中, 0abcd
与原来的index值一致, 而1abcd
= 0abcd + 10000
= 0abcd + oldCap
故虽然数组大小扩大了一倍,但是同一个key
在新旧table中对应的index却存在一定联系: 要么一致,要么相差一个 oldCap
。
而新旧index是否一致就体现在hash值的第4位(我们把最低为称作第0位), 怎么拿到这一位的值呢, 只要:
hash & 0000 0000 0000 0000 0000 0000 0001 0000
上式就等效于
hash & oldCap
故得出结论:
如果(e.hash & oldCap) == 0
则该节点在新表的下标位置与旧表一致都为j
如果(e.hash & oldCap) == 1
则该节点在新表的下标位置j + oldCap
根据这个条件, 我们将原位置的链表拆分成两个链表, 然后一次性将整个链表放到新的Table对应的位置
8.查询:
对key的hashCode()做hash运算,计算index;
如果在bucket里的第一个节点里直接命中,则直接返回;
如果有冲突,则通过key.equals(k)去查找对应的Entry;
- 若为树,则在树中通过key.equals(k)查找,O(logn);
- 若为链表,则在链表中通过key.equals(k)查找,O(n)。
9.HashTable 的区别
1.HashMap 是线程不安全的,HashTable是线程安全的。
2.HashMap 允许 key 和 Vale 是 null,但是只允许一个 key 为 null,且这个元素存放在哈希表 0 角标位置。 HashTable 不允许key、value 是 null
3.HashMap 内部使用hash(Object key)扰动函数对 key 的 hashCode 进行扰动后作为 hash 值。HashTable 是直接使用 key 的 hashCode() 返回值作为 hash 值。
4.HashMap默认容量为 2^4 且容量一定是 2^n ; HashTable 默认容量是11,不一定是 2^n
5.HashTable 取哈希桶下标是直接用模运算,扩容时新容量是原来的2倍+1。HashMap 在扩容的时候是原来的两倍,且哈希桶的下标使用 &运算代替了取模。
其他问题: