
面试题
- HashMap与HashTable的关系
- 是否线程安全
- 元素是否有序
- 怎样解决哈希冲突
- 键应该如何选择
- 扩容时机和过程
- 容量为什么必须是2的幂
- 负载因子的作用
- 什么时候会用红黑树代替链表
- key的hashCode计算方式
不想看源码解析的同学,可以直接去最下方查看答案
源码解析(JDK 1.8)
Map是一种常用的数据结构,以键值对(key-value)的形式进行存储。在Java中Map的常用实现有HashMap、HashTable、TreeMap、LinkedHashMap和ConcurrentHashMap等。本篇文章只介绍最常使用的HashMap,其他实现的分析将在其他文章中介绍。
HashMap虽然经常被称为集合类,但是并没有像List或者Set一样实现Collection接口。而只是实现了Map接口。
在JDK 1.8版本中,HashMap的底层有两种方式实现,默认情况下是基于数组与单向链表链表实现的,当超过一定阈值时,会将链表转化为红黑树,来减少时间消耗。

底层结构
/**
* HashMap默认的初始容量,如果构造时没有指定容量,将会使用 1 << 4 = 16
* 作为容量。容量必须是2的幂数,原因见面试题第7题解答
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // = 16
/**
*
* 默认的最大容量,必须是2的幂
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认的负载因子负载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 当某个槽中node的数量大于这个阈值时,这个槽中的链表会被转化为红黑树
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 当某个槽中node的数量小于这个阈值时,这个槽中的红黑树会被转化为链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 与TREEIFY_THRESHOLD配合使用。当Map中槽的数量大于该阈值,且TREEIFY_THRESHOLD
* 阈值同样满足时,才会进行链表到红黑树的转化。否则会用resize替代树化
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 底层数组。length必须是2的幂数,length即为map的capacity(不是size)
*/
transient Node<K,V>[] table;
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* map中键值对(key-value)的数量
*/
transient int size;
/**
* 相当于map的版本号。每次对map进行结构性改造(例如,rehash)的时候,会更新版本号
* 用来在遍历时实现fail-fast机制
*/
transient int modCount;
/**
* 下一次扩容时,要增加到的容量 threshold = oldCapacity * loadFactor
*/
int threshold;
/**
* 复杂因子
*/
final float loadFactor;
Node结点
/**
* Map默认情况下使用链表解决哈希冲突,Node为实现链表结点的数据结构
*/
static class Node<K,V> implements Map.Entry<K,V> {
// Node经过计算后对应的hash值,通过key计算求出,用单独字段保存,
// 防止每次都要计算,节省时间
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;
}
}
Map默认情况下使用链表解决哈希冲突,Node为实现链表结点的数据结构。红黑树使用TreeNode实现,在文章最后介绍。
工具方法
/**
* 扰动函数,重新计算key的hash值。
*
* 由于Map底层数组 N 的长度为2的幂数。计算key存放的位置时,计算公式为hash ^ (N.length - 1),
* 如果直接使用key的hash值,那么对于一些key来说如果只在高位有区分,由于高位
* 不参与计算算,那么这些key都会发生碰撞,还会产生周期性重复。
*
* 例如,当数组长度为16时,N.length - 1为15,二进制为 1111。
* key_1 -> 5 二进制为 101, key_2 -> 21 二进制为 10101。
* key_1 ^ 15 = 101, key_2 ^ 15 = 101
*
* 所以HashMap计算key的hash值,采用了扰动函数,将key自身的hashCode的高16位与低16位
* 异或,这样保证高位和低位都会对元素的位置产生影响
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 返回大于输入参数cap,且最近的2的整数次幂的数
*/
static final int tableSizeFor(int cap) {
// 防止cap本身就为2的幂数
int n = cap - 1;
// 假设n不为0,那么n的某位必为1,假设n = 0000100000,
// 那么 n>>>1 = 0000010000,n|n>>>1 则为 0000110000,
// 可见这一步的目的是让n中每位1的后一位也为1
n |= n >>> 1;
// 因为int只有32位,后面几步的步骤执行完之后,就
// 相当使最高位1之后的所有位数字都变为1,这样n就一定为
// 2的某次幂减1
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
// 假如n为0,那么就返回1,
// 假如n不为0,那么n+1就一定是2的某次幂
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
// 返回map的capacity
final int capacity() {
// 如果table不为null,说明map已经被初始化,并且已经添加过元素,capacity为table.length
// 如果table为null,说明没有添加过元素
// 如果threshold > 0,说明构造map时,指定了容量,threshold就代表了容量
// 如果threshold = 0,说明构造map时,未指定容量,就返回默认容量
return (table != null) ? table.length : (threshold > 0) ? threshold : DEFAULT_INITIAL_CAPACITY;
}
final Node<K, V>[] resize() {
Node<K, V>[] oldTab = table;
// 如果table为null,说明table还未初始化,capacity为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 当table为null时,threshold为map所需的容量
// 当table不为null时,threshold为需要扩容时的阈值
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 之前已经添加过元素,无须初始化
if (oldCap >= MAXIMUM_CAPACITY) {
// 如果oldCap大于等于最大容量,无法进行扩容
threshold = Integer.MAX_VALUE;
return oldTab;
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
// 因为oldCap一定是2的幂数,所以oldCap左移一位相当于扩容到之前的2
// 倍,并且newCap也一定是2的幂数。并且newCap小于最大容量时,新的阈值
// 也为之前的2倍
newThr = oldThr << 1;
} else if (oldThr > 0)
// 如果oldCap等于0,说明table并未初始化oldThr大于0,说明构造时指定了容量,
// 新的容量即为指定的容量
newCap = oldThr;
else {
// table未初始化,且未指定容量,那么就使用默认容量
newCap = DEFAULT_INITIAL_CAPACITY;
// 新的阈值为capacity * loadFactor
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
// 如果newThr为0,可能为两种情况:
// 1. map之前添加过元素,且旧的容量扩容2倍后会超出最大容量
// 2. map未添加过元素,且构造时指定了容量
// 计算新的阈值
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) {
// 如果oldTab不为null,说明之前有元素,需要重新放置元素
// 遍历之前的哈希槽
for (int j = 0; j < oldCap; ++j) {
Node<K, V> e;
// 如果哈希槽头结点为null,说明该槽之前没有元素
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 如果之前的槽中只有一个元素,那么直接计算在newTab中应该放置
// 的位置,并放入新位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 如果哈希槽对应的是红黑树,以树的方式进行处理
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
else {
// 遍历链表,将之前链表的中的结点分为两个部分,分别用low和head链表保存
Node<K, V> loHead = null, loTail = null;
Node<K, V> hiHead = null, hiTail = null;
Node<K, V> next;
// 遍历链表
do {
next = e.next;
// 将每个结点e的hash值与oldCap相与,等于0的结点放置在low链表中
// 不等于0的结点放置在high链表中
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;
}
构造方法
/**
* 根据给定的容量和负载因子构造一个空Map
*/
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;
// initialCapacity并不是真实的容量,真实的容量必须是
// 2的幂数,所以需要通过tableSizeFor方法计算大于
// initialCapacity的最小2的幂数
this.threshold = tableSizeFor(initialCapacity);
}
/**
* 根据给定的容量和默认的负载因子构造一个空Map
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 根据默认容量和默认的负载因子构造一个空Map
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* 创建一个HashMap,并将map m中的键值对添加到新的map中
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
插入/替换
/**
* 添加新的key-value到map中,如果key之前存在,旧的value会被覆盖
* <p>
* 如果key之前存在,会返回oldValue;
* 如果key之前不存在,则返回null
* <p>
* 需要注意,由于HashMap允许null值的存在,所以如果key之前的value
* 为null也会返回null,所以不能通过返回值为null,判断key之前是否存在
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* @param hash key经过扰动函数计算过的hash值
* @param key key
* @param value value
* @param onlyIfAbsent true-只有key之前不存在的时候,才会添加,否则不做任何改动
* false-如果key之前存在,旧的value值会被覆盖
* @param evict false-table处于创建模式(即通过构造方法调用)
*/
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)
// 如果table为null,或者tab.length为0,则说明
// map刚刚进行初始化,还未添加过元素,需要调用
// resize进行初始化
n = (tab = resize()).length;
// (n - 1) & hash 计算哈希值(相当于 hash % n ),计算
// 结果为key对应的槽的位置。
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果槽为空,这说明并未发生哈希碰撞,创建一个Node,
// 并将对应槽的头结点置为Node
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))))
// 如果槽的头结点的key与给定的key相等,直接结束。后面的代码会
// 进行值的替换
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) {
// 如果遍历完整个链表,没有发现相等的key,说明key之前不存在
// 则新创建一个Node,并插入到链表尾
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))))
// key之前存在
break;
p = e;
}
}
if (e != null) {
// e != null 说明key之前存在,则将key对应的value值覆盖
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
// 查看是否可以覆盖
e.value = value;
// 给LinkedHashMap提供的钩子方法,在HashMap中只是空实现
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
// 如果size超过了阈值,就进行再哈希
resize();
// 给LinkedHashMap提供的钩子方法,在HashMap中只是空实现
afterNodeInsertion(evict);
return null;
}
/**
* 将给定m中的所有元素添加到map中,如果m中的key之前存在,
* 之前的value会被覆盖
*/
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
/**
* @param evict false-table处于创建模式(即通过构造方法调用)
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) {
// 如果table为null,代表table刚被构造,还未添加键值对
// table为null的时候,threshold保存的是map capacity的大小
// 假设m的大小s为threshold,因为threshold = capacity * loadFactor,
// 所以 threshold / loadFactor 就求出来所需的capacity大小,
// +1 防止因向下取整导致的容量不足
float ft = ((float) s / loadFactor) + 1.0F;
// 对ft向下取整,并与HashMap允许最大容量进行比较,即为所需的容量
int t = ((ft < (float) MAXIMUM_CAPACITY) ?
(int) ft : MAXIMUM_CAPACITY);
if (t > threshold)
// table为null的时候,threshold保存的是map capacity的大小
// 而不是扩容时机的阈值,所以这里t大于threshold时,说明
// 之前计算的capacity容量不够大,需要重新计算容量
threshold = tableSizeFor(t);
} else if (s > threshold)
// 如果map中已经存在键值对了,且m的size大于阈值,则进行再哈希
resize();
// 遍历添加key-value到map中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
删除
/**
* 删除对应的key,并返回之前的值,如果key不存在,则返回null
*/
public V remove(Object key) {
Node<K, V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* @param hash 经过扰动函数计算的key的hash值
* @param key the key
* @param matchValue true-只有当key与value都相等时才删除,false-只要key相等就删除
* @param value 如果matchValue为true,那么value与这个值进行比较,matchValue为false值,该值
* 可以忽略
* @param movable false-删除时不移动其他node
*/
final Node<K, V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K, V>[] tab;
Node<K, V> p;
int n, index;
// 如果hash对应的槽存在元素才需要判断,否则说明key不存在直接返回null
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K, V> node = null, e;
K k;
V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 如果结点p的key与给定的key相等
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
// 如果是红黑树,就按红黑树处理
node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
else {
// 遍历链表,寻找与给定的key相等的结点
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// node不等于null,说明key之前存在。然后判断是否需要满足matchValue的条件
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
else if (node == p)
// 如果node是哈希槽头结点,直接把槽结点指向next
tab[index] = node.next;
else
// 删除node结点
p.next = node.next;
++modCount;
--size;
// 提供给LinkedHashMap的钩子方法,在HashMap中为空实现
afterNodeRemoval(node);
return node;
}
}
return null;
}
/**
* 删除map中的所有键值对
*/
public void clear() {
Node<K, V>[] tab;
modCount++;
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
查找
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;
// 如果hash对应的槽不为空才需要判断,否则说明key不存在直接返回null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 先检查头结点,如果头结点相等,直接返回
if (first.hash == hash &&
((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);
// 遍历链表,查找key相等的结点
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
public boolean containsValue(Object value) {
Node<K, V>[] tab;
V v;
if ((tab = table) != null && size > 0) {
// 遍历table所有槽
for (int i = 0; i < tab.length; ++i) {
// 遍历每个槽中的每个结点
for (Node<K, V> e = tab[i]; e != null; e = e.next) {
// 如果value相等,则直接返回,因为TreeNode继承了Node
// 所以不需要单独处理
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}
JDK 1.8 新增方法
@Override
public V getOrDefault(Object key, V defaultValue) {
Node<K, V> e;
// 返回key对应值,并用defaultValue替换null
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}
@Override
public V putIfAbsent(K key, V value) {
// 1. 如果key之前不存在,就将key-value添加到map中
// 2. 如果key之前对应的值为null,就用value替换null
// 3. 如果key之前对应的值不为null,则什么也不做
return putVal(hash(key), key, value, true, true);
}
@Override
public boolean remove(Object key, Object value) {
// 如果key之前存在,且值为value,则删除
// 否则不做任何操作
return removeNode(hash(key), key, value, true, true) != null;
}
/**
* 如果key之前存在,且key之前对应的值为oldValue,则用newValue替换
*/
@Override
public boolean replace(K key, V oldValue, V newValue) {
Node<K, V> e;
V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}
/**
* 如果key之前存在,且key的值不为null。则用value替换
*/
@Override
public V replace(K key, V value) {
Node<K, V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
@Override
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
if (mappingFunction == null)
throw new NullPointerException();
// 通过扰动函数计算key的hash值
int hash = hash(key);
Node<K, V>[] tab;
Node<K, V> first;
int n, i;
int binCount = 0;
TreeNode<K, V> t = null;
Node<K, V> old = null;
if (size > threshold || (tab = table) == null || (n = tab.length) == 0)
// 如果table还没有初始化,则先将table初始化
n = (tab = resize()).length;
if ((first = tab[i = (n - 1) & hash]) != null) {
// 如果key对应的槽不为空
if (first instanceof TreeNode)
// 按红黑树处理
old = (t = (TreeNode<K, V>) first).getTreeNode(hash, key);
else {
Node<K, V> e = first;
K k;
// 遍历链表,找到key相等结点并返回
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
V oldValue;
// 如果key之前存在,且值不为null,则直接返回oldValue
if (old != null && (oldValue = old.value) != null) {
afterNodeAccess(old);
return oldValue;
}
}
// 对key执行计算,得到value
V v = mappingFunction.apply(key);
if (v == null) {
return null;
} else if (old != null) {
// key之前存在,但是oldValue为null,则替换新值
old.value = v;
afterNodeAccess(old);
return v;
} else if (t != null)
// key之前存在且oldValue不为null,且对应槽已经转化
// 为红黑树,则添加TreeNode
t.putTreeVal(this, tab, hash, key, v);
else {
// 按正常链表处理,添加一个新结点
tab[i] = newNode(hash, key, v, first);
// 是否需要转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
return v;
}
public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
Node<K, V> e;
V oldValue;
int hash = hash(key);
if ((e = getNode(hash, key)) != null && (oldValue = e.value) != null) {
// 如果key之前存在,且对应值不为null
// 对key和oldValue进行计算,得到newValue
V v = remappingFunction.apply(key, oldValue);
if (v != null) {
// 如果newValue不为null,则直接替换值
e.value = v;
afterNodeAccess(e);
return v;
} else
// 如果newValue为null,则删除key对应结点
removeNode(hash, key, null, false, true);
}
return null;
}
@Override
public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K, V>[] tab;
Node<K, V> first;
int n, i;
int binCount = 0;
TreeNode<K, V> t = null;
Node<K, V> old = null;
if (size > threshold || (tab = table) == null || (n = tab.length) == 0)
// 如果table还没有初始化,则先将table初始化
n = (tab = resize()).length;
if ((first = tab[i = (n - 1) & hash]) != null) {
// 如果key对应的槽不为空
if (first instanceof TreeNode)
// 按红黑树处理
old = (t = (TreeNode<K, V>) first).getTreeNode(hash, key);
else {
Node<K, V> e = first;
K k;
// 遍历链表,找到key相等结点并返回
do {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
}
V oldValue = (old == null) ? null : old.value;
// 对key和oldValue进行计算,得到newValue
V v = remappingFunction.apply(key, oldValue);
if (old != null) {
// 如果key之前存在
if (v != null) {
// newValue不为null,则直接替换新值
old.value = v;
afterNodeAccess(old);
} else
// 如果newValue为null,则删除旧结点
removeNode(hash, key, null, false, true);
} else if (v != null) {
// 如果key之前不存在,且newValue不为null
if (t != null)
// 如果槽中结点已经转化为红黑树,则添加一个TreeNode
t.putTreeVal(this, tab, hash, key, v);
else {
// 按正常链表处理,添加一个Node到链表结尾
tab[i] = newNode(hash, key, v, first);
// 是否需要转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
}
return v;
}
@Override
public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
if (value == null)
throw new NullPointerException();
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K, V>[] tab;
Node<K, V> first;
int n, i;
int binCount = 0;
TreeNode<K, V> t = null;
Node<K, V> old = null;
if (size > threshold || (tab = table) == null || (n = tab.length) == 0)
// 如果table还没有初始化,则先将table初始化
n = (tab = resize()).length;
if ((first = tab[i = (n - 1) & hash]) != null) {
// 如果key对应的槽不为空
if (first instanceof TreeNode)
// 按红黑树处理
old = (t = (TreeNode<K, V>) first).getTreeNode(hash, key);
else {
Node<K, V> e = first;
K k;
// 遍历链表,找到key相等结点并返回
do {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
}
if (old != null) {
// 如果key之前存在
V v;
if (old.value != null)
// 如果key之前的value不为null,则通过oldValue和给定的value计算newValue
v = remappingFunction.apply(old.value, value);
else
// key之前的value为null,则newValue就是给定的value
v = value;
if (v != null) {
// 如果newValue不为null,则直接替换value
old.value = v;
afterNodeAccess(old);
} else
// 否则删除key对应结点
removeNode(hash, key, null, false, true);
return v;
}
// 这里value不可能为null,因为第一步判断过value为null时直接抛出异常
// 代码运行到这里说明key之前不存在
if (value != null) {
if (t != null)
// 如果槽对应的是红黑树,则添加一个TreeNode
t.putTreeVal(this, tab, hash, key, value);
else {
// 按链表处理,添加一个Node到map中
tab[i] = newNode(hash, key, value, first);
// 是否需要转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
}
return value;
}
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
Node<K, V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
// 遍历前先保存版本号
int mc = modCount;
// 遍历table所有槽
for (int i = 0; i < tab.length; ++i) {
// 遍历槽中链表
for (Node<K, V> e = tab[i]; e != null; e = e.next)
action.accept(e.key, e.value);
}
// 如果版本号和之前不一致,说明遍历过程中,对map进行了结构性修改
// 抛出异常
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
@Override
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Node<K, V>[] tab;
if (function == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
// 遍历前先保存版本号
int mc = modCount;
// 遍历table所有槽
for (int i = 0; i < tab.length; ++i) {
// 遍历槽中链表
for (Node<K, V> e = tab[i]; e != null; e = e.next) {
// 替换新值
e.value = function.apply(e.key, e.value);
}
}
// 如果版本号和之前不一致,说明遍历过程中,对map进行了结构性修改
// 抛出异常
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
迭代器
abstract class HashIterator {
// 下一个要遍历的Node
Node<K, V> next;
// 当前遍历的Node
Node<K, V> current;
// 期待版本号,实现fail-fast机制使用
int expectedModCount;
// current Node对应的槽的下标
int index;
HashIterator() {
expectedModCount = modCount;
Node<K, V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) {
// 移动index到第一个非空的槽
do {
} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
final Node<K, V> nextNode() {
Node<K, V>[] t;
Node<K, V> e = next;
// 如果遍历过程中,hashMap发生过结构性更改,则快速失败,抛出异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
// 返回链表中下一个结点,如果当前槽的结点已经遍历完,就移动到下
// 一个有结点的槽
if ((next = (current = e).next) == null && (t = table) != null) {
do {
} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
public final void remove() {
Node<K, V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
红黑树
待补充
面试题解答
-
HashMap与HashTable的关系
HashMap是非线程安全的。HashTable在get,put等方法前添加了synchronized关键字,所以是线程安全的。但是,由于在JDK1.5之后增加ConcurrentHashMap,所以HashTable不再推荐使用
-
是否线程安全
非线程安全。HashMap中的modCount字段类似于版本号的概念,在操作过程中,如果有其他线程进行结构性的操作(如put等),会修改modCount值。触发fail-fast机制,抛出ConcurrentModificationException
-
元素是否有序
不是有序的。因为遍历时,要遍历table每个槽中的每个结点。因为resize等操作,结点所在的槽可能发生变化,所以不是有序的。
可以使用LinkedHashMap或者TreeMap实现有序的目的
-
怎样解决哈希冲突
HashMap采用拉链法解决哈希冲突,slot计算方式为 hash % length。当hash落在同一个槽中时,会使用链表解决冲突。同时HashMap为了防止链表过长导致的性能问题,在满足条件时,将单向链表转化为红黑树,减少查询时间
-
键应该如何选择
尽量选择Integer或者String这样的不可变类,或者重写类的hashCode和equals方法。否则,当使用可变类对象作为key放入map中时,如果对对象进行了修改,再使用该对象作为key进行操作时,就无法找到之前的key-value对了。
-
扩容时机和过程
扩容的时机:
- 第一次向map中添加元素时,需要对table进行初始化,要调用resize
- 当map中key-value键值对的数量大于threshold时,调用resize过程进行扩容
扩容的过程:
- 首先,将之前的容量扩充2倍(会与最大容量等比较进行修剪)
- 然后,根据新的容量计算新的阈值,并创建新的数组
- 遍历之前每个槽 j 中的结点,将每个槽中的节点通过 hash & oldCap分出高位和低位的结点。将属于低位的结点放置在原槽 j 中,将属于高位的结点放置在 oldCap+j 的槽中。
-
容量为什么必须是2的幂
拉链法计算key所在槽的位置时,通常采用 hash % length的方法(length是数组长度)。
但对于计算机来说,取余操作开销很大。所以Java设计师们使用了位运算。
当length为2的幂数时,hash ^ (length - 1) = hash % length
那么,既然hash要与length-1做异或操作,为什么数组大小不采用 2的幂数-1,而是用2的幂数呢。
因为,这样再扩容时,只需要将原来的length向左移动1位就可以了。不需要多余的操作。
同时,因为2的幂数的二进制中只有1位为1,在resize过程中,只需要通过hash & oldCap是否为1,便可以将原链表中的结点分为高位和低位,解决hash冲突问题。
-
负载因子的作用
threshold的计算方式为 oldCapacity * loadFactor,loadFactor影响了扩容阈值,从而对HashMap的空间、时间性能造成影响
- 当loadFactor较小时,HashMap会偏向于扩容,容量变大后,key-value分布的就会比较稀疏,发生哈希冲突的机会会变小。因此查询性能会更优,但会耗费更多空间。同时,频繁扩容调用resize方法,会影响插入性能
- 当loadFactor较大时,特别是大于1时,HashMap需要存储更多的key-value对才能扩容,分布比较集中,容易发生哈希冲突。一个槽中的链表可能很长,影响查询时的性能,但空间耗费较少。
HashMap默认采用0.75作为负载因子是基于空间、时间两个方面性能的考虑
-
什么时候会用红黑树代替链表
在put时,当槽中链表的长度超过TREEIFY_THRESHOLD时,会调用treeifyBin方法。 treeifyBin会比较table长度(即map的capacity)与MIN_TREEIFY_CAPACITY。 当table长度大于等于MIN_TREEIFY_CAPACITY时会转化成红黑树,否则只会resize
-
key的hashCode计算方式
key.hashCode() ^ (key.hashCode() >>> 16)
通过hash扰动函数,重新计算key的hash值。 由于Map底层数组 N 的长度为2的幂数。如果直接使用key的hash值,那么对于一些key来说如果只在高位有区分,那么这些key都会发生碰撞,还会产生周期性重复。
所以HashMap计算key的hash值,采用了扰动函数,将key自身的hashCode的高16位与低16位向异或,这样保证高位和低位都会对元素的位置产生影响
假设N.length = 16

HashMap源码注解之静态工具方法hash 推荐阅读这篇文章,介绍的比较详细