java.util.IdentityHashMap 自 JDK 1.4 起存在,表面上实现了 Map 接口,但官方文档明确说它不是通用 Map 实现——判等方式和内部存储结构都和 HashMap 不同,只应该用在明确需要"引用相等"语义的少数场景。
与 HashMap 的核心区别:引用相等而非逻辑相等
HashMap 判断两个 key 是否相同,用的是 equals();IdentityHashMap 用的是 ==,也就是判断是否是同一个对象引用。计算哈希值时也不调用 key 自身的 hashCode(),而是统一用 System.identityHashCode(key)。
Map<String, Integer> hashMap = new HashMap<>();
Map<String, Integer> identityMap = new IdentityHashMap<>();
String a = new String("key");
String b = new String("key");
hashMap.put(a, 1);
hashMap.put(b, 2);
System.out.println(hashMap.size()); // 1,a.equals(b) 为 true,视为同一个 key
identityMap.put(a, 1);
identityMap.put(b, 2);
System.out.println(identityMap.size()); // 2,a != b,视为两个不同的 key
这个差异不仅体现在 key 上,entrySet()、keySet()、values() 这些视图集合里元素的比较,同样是引用相等语义,而不是 HashMap 视图那种基于 equals() 的语义。
内部实现:开放地址法 + 线性探测
HashMap 用的是开链法(数组 + 链表/红黑树),而 IdentityHashMap 用的是开放地址法的线性探测,这一点在 开链法与开放地址法 里也提到过。
它的存储结构是一个扁平的 Object[] 数组,key 存在偶数下标,紧跟着的奇数下标存对应的 value,而不是像 HashMap 那样为每个键值对分配一个 Node 对象:
// java.util.IdentityHashMap 源码节选(JDK 21)
private static int hash(Object x, int length) {
int h = System.identityHashCode(x);
// Multiply by -254 to use the hash LSB and to ensure index is even
return ((h << 1) - (h << 8)) & (length - 1);
}
private static int nextKeyIndex(int i, int len) {
return (i + 2 < len ? i + 2 : 0);
}
get() 的查找逻辑很直白:算出起始下标,命中就返回,遇到空槽位就说明没有这个 key,否则线性向后探测下一对 key/value:
public V get(Object 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 (V) tab[i + 1];
if (item == null)
return null;
i = nextKeyIndex(i, len);
}
}
因为线性探测依赖"空槽位"来判断查找失败,null 不能直接存进数组当 key,否则会和"这里本来就没有值"混淆。IdentityHashMap 的解法是用一个内部哨兵对象 NULL_KEY 代替真正的 null key 存进数组,取值时再转换回 null:
static final Object NULL_KEY = new Object();
private static Object maskNull(Object key) {
return (key == null ? NULL_KEY : key);
}
负载因子固定为 2/3,且不可配置
HashMap 默认负载因子是 0.75,可以通过构造函数调整;IdentityHashMap 的负载因子固定为 2/3,不提供配置入口。默认容量 32 对应"预期最大元素数"(expectedMaxSize)21,这也是无参构造函数背后的换算关系:
private static final int DEFAULT_CAPACITY = 32; // 对应 expectedMaxSize = 21,负载因子 2/3
private static final int MINIMUM_CAPACITY = 4; // 对应 expectedMaxSize = 2
典型应用场景
IdentityHashMap 的 Javadoc 里明确给出了它的设计初衷:
- 拓扑保持的对象图变换(topology-preserving object graph transformations):比如序列化、深拷贝。这类场景需要维护一张"节点表"记录已经处理过的对象引用,且要求两个逻辑相等但不是同一个对象的实例被视为不同节点——这正是
equals()语义会出错、必须用引用相等的地方。java.io.ObjectOutputStream内部处理循环引用时就是类似的思路。 - 维护代理对象表:比如调试工具需要为程序中每一个对象维护一个对应的 proxy 对象,这时 key 就是原始对象本身,只关心它是不是同一个实例,不关心它的
equals()/hashCode()是怎么实现的。 - 绕开不可信或有开销的
equals()/hashCode():如果 key 的equals()/hashCode()实现有 bug、开销大,或者对象在插入后被修改导致hashCode()发生变化(这在HashMap里会导致该 entry 再也找不到),用引用相等的IdentityHashMap可以规避这类问题。
与 HashMap 对比
| 维度 | HashMap | IdentityHashMap |
|---|---|---|
| key 判等 | equals() |
== |
| 哈希来源 | hashCode() |
System.identityHashCode() |
| 冲突解决 | 开链法(链表 / 红黑树) | 开放地址法(线性探测) |
| 存储结构 | Node<K,V>[],每个桶一条链/树 |
扁平 Object[],key/value 交替存放 |
| 负载因子 | 默认 0.75,可配置 | 固定 2/3,不可配置 |
| null key/value | 允许 | 允许(内部用 NULL_KEY 哨兵对象表示 null key) |
| 线程安全 | 否 | 否 |
| 典型场景 | 通用 map | 序列化/深拷贝循环引用检测、代理对象表 |
使用注意事项
- 不是通用
Map实现:Javadoc 原话是 “This class is not a general-purpose Map implementation”,只在明确需要引用相等语义时使用。 - 非线程安全:和
HashMap一样未做同步,多线程结构性修改需要外部同步,或用Collections.synchronizedMap()包装。 - fail-fast 迭代器:结构性修改后继续用旧迭代器遍历会抛
ConcurrentModificationException,这一点和HashMap、Hashtable(见 HashMap 与 Hashtable 的区别)一致。
更多关于 HashMap 本身在 JDK 1.7/1.8 的内部演进,参见 HashMap;开链法与开放地址法两种冲突解决思路的通用对比,参见 开链法与开放地址法。