简介
IdentityHashMap是一个散列表,储存的元素为键值对(key-value),允许空值和空键,非线程安全,不能确保内部元素的顺序。与其他Map不同,IdentityHashMap对键使用引用相等的比较(==)而不是equals,所以它可以存放多个内容相同的键,只要这些键的引用不相同。
以下分析基于corretto-1.8.0_282版本。
继承关系
- 实现Serializable接口,被序列化。
- 实现Cloneable接口,可以通过.clone()方法克隆出一个实例。
- 实现Map接口,提供了Map的相关操作。
属性
DEFAULT_CAPACITY
/**
* 默认初始容量,必须是2的幂
* 负载系数定义为2/3
*/
private static final int DEFAULT_CAPACITY = 32;
MINIMUM_CAPACITY
/**
* 最小容量,当调用带参构造实例化时,若计算得到的初始容量小于
* 此值,则使用最下容量作为初始容量,必须为2的幂
*/
private static final int MINIMUM_CAPACITY = 4;
MAXIMUM_CAPACITY
/**
* 最大容量,必须是小于等于2的29次方的一个2的幂
*/
private static final int MAXIMUM_CAPACITY = 1 << 29;
table
/**
* 桶数组,大小必须是2的幂
* 键和值都放在数组中,例如键映射到索引i处,则值
* 会放在索引i+1处
*/
transient Object[] table;
size
/**
* 键值对个数
*/
int size;
modCount
/**
* 修改次数,用作迭代时的快速失败检查
*/
transient int modCount;
NULL_KEY
/**
* key为null会被替换为此值
*/
static final Object NULL_KEY = new Object();
构造方法
IdentityHashMap()
/**
* 使用默认容量(32)实例化IdentityHashMap
*/
public IdentityHashMap() {
init(DEFAULT_CAPACITY);
}
/**
* 根据传入容量初始化数组
* 因为键和值都放在数组中,所以数组大小是容量的两倍
*/
private void init(int initCapacity) {
table = new Object[2 * initCapacity];
}
IdentityHashMap(int expectedMaxSize)
/**
* 使用给定的最大大小实例化IdentityHashMap,会根据此参数计算出容量
* 当储存的元素个数超过expectedMaxSize时,可能会进行扩容
*/
public IdentityHashMap(int expectedMaxSize) {
if (expectedMaxSize < 0)
throw new IllegalArgumentException("expectedMaxSize is negative: "
+ expectedMaxSize);
init(capacity(expectedMaxSize));
}
/**
* 返回给定预期最大大小的适当容量,必须为2的幂
* 返回的cap需要满足expectedMaxSize<(2 * cap / 3)
*/
private static int capacity(int expectedMaxSize) {
// 若expectedMaxSize大于(MAXIMUM_CAPACITY / 3) 返回MAXIMUM_CAPACITY
// 若expectedMaxSize小于(2 * MINIMUM_CAPACITY / 3) 返回MINIMUM_CAPACITY
// 否则返回小于等于expectedMaxSize的3倍的2的幂
return
(expectedMaxSize > MAXIMUM_CAPACITY / 3) ? MAXIMUM_CAPACITY :
(expectedMaxSize <= 2 * MINIMUM_CAPACITY / 3) ? MINIMUM_CAPACITY :
Integer.highestOneBit(expectedMaxSize + (expectedMaxSize << 1));
}
IdentityHashMap(Map<? extends K, ? extends V> m)
/**
* 根据给定的Map实例化IdentityHashMap,容量由给定Map计算
*/
public IdentityHashMap(Map<? extends K, ? extends V> m) {
// Allow for a bit of growth
this((int) ((1 + m.size()) * 1.1));
putAll(m);
}
方法
put(K key, V value)
/**
* 将键值对插入Map,若key之前存在,则更新其值
* 比较的是key的引用,使用==而不是equals
*/
public V put(K key, V value) {
final Object k = maskNull(key);
retryAfterResize: for (;;) {
final Object[] tab = table;
final int len = tab.length;
// 计算key映射到数组的索引
int i = hash(k, len);
// 线性探测解决冲突
// 遍历数组,步长为2,直到找到与key引用相等的位置或一个空位置
for (Object item; (item = tab[i]) != null;
i = nextKeyIndex(i, len)) {
// 若此位置不为null且与给定的key引用相等,则更新其值
// 引用不相等,则移动到后一个key,继续比较
if (item == k) {
@SuppressWarnings("unchecked")
V oldValue = (V) tab[i + 1];
// 更新值
tab[i + 1] = value;
// 返回旧值
return oldValue;
}
}
// 执行到这里说明数组中没有与key引用相等的位置,执行插入操作
final int s = size + 1;
// 如果插入后元素个数超过了容量的2/3(负载系数),则先执行扩容操作
if (s + (s << 1) > len && resize(len))
// 扩容后需要重新计算key映射后的索引
continue retryAfterResize;
// 真正开始插入
modCount++;
// 索引i处储存key,i为找到的空位置,可能不是key映射的位置
tab[i] = k;
// 索引i+1处储存值
tab[i + 1] = value;
// 更新元素个数
size = s;
return null;
}
}
/**
* 当key为null时,将其替换为NULL_KEY
*/
private static Object maskNull(Object key) {
return (key == null ? NULL_KEY : key);
}
/**
* 计算hash映射到桶数组的索引
*/
private static int hash(Object x, int length) {
// 获取对象的原始hashCode,此hashCode不受重写的hashCode()方法影响
// 此hashCode每个对象唯一
int h = System.identityHashCode(x);
// 计算索引
// 先将hashCode乘以-254,然后取对length的模
// 不清楚为什么取了-254这个值
// (h << 1) - (h << 8) = (h * 2) - (h * 2**8) = 2h - 256h = -254h
// -254是偶数,乘完后结果也一定是个偶数,可以保证映射到数组的偶数位
// 数组的偶数位保存key,奇数位保存value
return ((h << 1) - (h << 8)) & (length - 1);
}
/**
* 找到下一个key的位置,超过最大索引重置为0
*/
private static int nextKeyIndex(int i, int len) {
return (i + 2 < len ? i + 2 : 0);
}
putAll(Map<? extends K, ? extends V> m)
/**
* 将给定Map中的所有元素添加或更新到IdentityHashMap中
*/
public void putAll(Map<? extends K, ? extends V> m) {
// 若给定的Map为空,则不执行任何操作
int n = m.size();
if (n == 0)
return;
// 若给定Map大小超过size,则根据给定Map的大小做一次扩容操作
if (n > size)
resize(capacity(n));
// 遍历给定的Map,将其所有元素插入IdentityHashMap
for (Entry<? extends K, ? extends V> e : m.entrySet())
put(e.getKey(), e.getValue());
}
remove(Object key)
/**
* 删除指定键的键值对
*/
public V remove(Object key) {
// 计算key映射到数组的索引
Object k = maskNull(key);
Object[] tab = table;
int len = tab.length;
int i = hash(k, len);
// 线性探测
while (true) {
Object item = tab[i];
// 找到引用相同的位置,删除此处的键值对
if (item == k) {
modCount++;
size--;
@SuppressWarnings("unchecked")
V oldValue = (V) tab[i + 1];
// 解除引用
tab[i + 1] = null;
tab[i] = null;
closeDeletion(i);
return oldValue;
}
// key不存在
if (item == null)
return null;
// 遍历下一个key
i = nextKeyIndex(i, len);
}
}
/**
* 删除一个键值对后,需要将后面映射相同的键值对向前移动,使之保持连续
*/
private void closeDeletion(int d) {
Object[] tab = table;
int len = tab.length;
// 遍历删除位置之后的键
Object item;
for (int i = nextKeyIndex(d, len); (item = tab[i]) != null;
i = nextKeyIndex(i, len) ) {
// 因为数组是循环使用的,所以判断比较多
// (i < r && (r <= d || d <= i))
// i < r 说明此hash在数组尾和数组头部都有分布,此时i已经遍历到数组头部
// r <= d 说明被删除的索引d在数组尾,对应下面这种情况,r <= d <= len - 2
// --------------
// i r d
//
// d <= i 说明被删除的索引d在数组头,对应下面这种情况,0 <= d <= i
// --------------
// d i r
//
// (r <= d && d <= i)说明此hash不存在跨越,对应下面这种情况,r <= d <= i
// --------------
// r d i
// 符合上述条件说明i位置的键与d是相同的映射,需要将索引i处的键值对移动到索引d处
int r = hash(item, len);
if ((i < r && (r <= d || d <= i)) || (r <= d && d <= i)) {
// 索引i处的键值移动到索引d处
tab[d] = item;
tab[d + 1] = tab[i + 1];
tab[i] = null;
tab[i + 1] = null;
d = i;
}
}
}
clear()
/**
* 清除所有元素
*/
public void clear() {
modCount++;
Object[] tab = table;
// 遍历整个数组,将所有位置为null
for (int i = 0; i < tab.length; i++)
tab[i] = null;
// 元素个数置为0
size = 0;
}
get(Object key)
/**
* 返回key对应的值,若key不存在,返回null
*/
@SuppressWarnings("unchecked")
public V get(Object key) {
// 若key为null,替换为NULL_KEY
Object k = maskNull(key);
Object[] tab = table;
int len = tab.length;
// 计算key映射到数组的索引
int i = hash(k, len);
// 线性探测
while (true) {
Object item = tab[i];
if (item == k)
// 找到与key引用相等的键,返回对应的值
return (V) tab[i + 1];
if (item == null)
// 找不到返回null
return null;
i = nextKeyIndex(i, len);
}
}
containsKey(Object key)
/**
* 查询是否包含给定的key
*/
public boolean containsKey(Object key) {
// 计算key映射到数组的索引
Object k = maskNull(key);
Object[] tab = table;
int len = tab.length;
int i = hash(k, len);
// 线性探测
while (true) {
Object item = tab[i];
if (item == k)
return true;
if (item == null)
return false;
i = nextKeyIndex(i, len);
}
}
containsValue(Object value)
/**
* 查询是否包含给定的value
*/
public boolean containsValue(Object value) {
Object[] tab = table;
// 遍历数组储存值的位置
for (int i = 1; i < tab.length; i += 2)
// 若找到了相同的value,且对应的key不为null,则返回true
if (tab[i] == value && tab[i - 1] != null)
return true;
return false;
}
forEach(BiConsumer<? super K, ? super V> action)
/**
* 遍历所有元素
*/
@SuppressWarnings("unchecked")
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
int expectedModCount = modCount;
Object[] t = table;
// 遍历数组中所有的key
for (int index = 0; index < t.length; index += 2) {
Object k = t[index];
// 若key不为null,则将此键值对传入action处理
if (k != null) {
action.accept((K) unmaskNull(k), (V) t[index + 1]);
}
// 快速失败检查
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
}
/**
* 将NULL_KEY转换回null
*/
static final Object unmaskNull(Object key) {
return (key == NULL_KEY ? null : key);
}
replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
/**
* 遍历所有元素,使用给定的function计算新的值进行替换
*/
@SuppressWarnings("unchecked")
@Override
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Objects.requireNonNull(function);
int expectedModCount = modCount;
Object[] t = table;
// 遍历数组中所有的key
for (int index = 0; index < t.length; index += 2) {
Object k = t[index];
// 若key不为null,则将此键值对传入function处理,使用返回值更新value
if (k != null) {
t[index + 1] = function.apply((K) unmaskNull(k), (V) t[index + 1]);
}
// 快速失败检查
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
}
总结
- IdentityHashMap键和值都存放在数组中,键存放在偶数位,值存放在奇数位,所以桶数组的长度是容量的2倍。
- IdentityHashMap使用线性探测解决哈希冲突。
- IdentityHashMap允许空键,通过将其替换为NULL_KEY来进行存放。