在Java集合体系中,大多数映射实现都依靠键对象的逻辑相等性来定位数据,但IdentityHashMap走了一条完全不同的路线。它不使用equals方法,也不依赖hashCode的常规约定,而是用引用地址层面的比较来决定两个键是否为同一个。换句话说,只有两个键指向堆里的同一个对象实例时,才被认为相等。

IdentityHashMap的基本原理
IdentityHashMap位于java.util包中,从外部看它和HashMap一样实现了Map接口,但内部行为差异极大。常规HashMap在put和get时,会先调用键的hashCode,再使用equals比对桶中的元素;IdentityHashMap则使用System.identityHashCode获取对象身份哈希,并用双等号(==)比较键引用。这意味着哪怕两个对象内容一模一样,只要不是同一个实例,就会被当成不同键。
这种设计并不是为了替代HashMap,而是解决特定问题:当业务关心的是对象身份而非对象值的时候。例如在一个编辑器应用中,每个图形节点都是独立对象,即使用户把两个节点属性改得完全一致,系统仍要把它们当作不同实体处理,此时IdentityHashMap比HashMap更安全。
内部存储结构
IdentityHashMap底层使用一个Object数组table,数组以偶数索引存键、奇数索引存值,这种键值相邻排列的方式减少了节点对象的创建。数组长度始终为2的幂相关值,并且负载因子默认约为三分之二,当元素数量超过阈值时会进行扩容与重哈希。
由于使用身份哈希,不同对象也可能产生相同的identityHashCode,因此依然需要线性探测来解决冲突。当发生哈希碰撞时,IdentityHashMap会向后探测下一个可用槽位,而不是像HashMap那样挂链表或树化。下面的代码展示了最基本的用法:
import java.util.IdentityHashMap;
import java.util.Map;
public class Demo {
public static void main(String[] args) {
Map<String, Integer> map = new IdentityHashMap<>();
String a = new String("key");
String b = new String("key");
// a与b内容相同但引用不同
map.put(a, 1);
map.put(b, 2);
// 输出2,因为a和b被视为不同键
System.out.println(map.size());
System.out.println(map.get(a));
System.out.println(map.get(b));
}
}
与HashMap的核心差异对比
为了更直观地理解,我们把两者在键相等判定、哈希来源、适用场景上的区别列出来。很多人在调试时发现map.get明明放了值却取不到,往往就是因为误把IdentityHashMap当成了值相等的容器。
| 对比维度 | HashMap | IdentityHashMap |
|---|---|---|
| 键相等判定 | equals方法 | ==引用比较 |
| 哈希值来源 | key.hashCode() | System.identityHashCode(key) |
| 典型用途 | 通用键值缓存 | 对象身份映射、防止值相等混淆 |
为什么不用equals
如果IdentityHashMap也调用equals,那它就和HashMap没有本质区别,只是哈希算法不同而已。实际上,有些类重写了equals导致逻辑相等但物理不同,这种重写会带来副作用:在需要严格区分实例的场景里,重写后的equals会掩盖对象身份。IdentityHashMap刻意绕开equals,从而保证只要不是同一块内存引用,就绝不会被误判。
从性能角度看,==比较和identityHashCode都是JVM级操作,不触发任何用户代码,因此在某些高频映射且键为不可变实例的场景中,IdentityHashMap反而比HashMap少了一层方法调用开销。不过它的线性探测在冲突严重时性能会下降,所以不能盲目替换。
常见使用场景与注意事项
IdentityHashMap常见于对象序列化框架、ORM工具或图形界面库中,用来维护对象到辅助信息的映射,而不希望因为对象值相同而合并记录。比如在遍历一个复杂对象图时,用IdentityHashMap记录已访问节点,可避免由于两个不同节点值相同而造成的错误短路。
使用时要特别注意:基本类型包装类如Integer在自动装箱时可能存在缓存(如-128到127),这时候两个字面量可能指向同一实例,会让IdentityHashMap表现出看似值相等的假象。下面的例子说明了这一点:
import java.util.IdentityHashMap;
public class CacheDemo {
public static void main(String[] args) {
IdentityHashMap<Integer, String> idMap = new IdentityHashMap<>();
Integer x = 100;
Integer y = 100;
// 因Integer缓存,x与y引用相同
idMap.put(x, "a");
System.out.println(idMap.get(y)); // 输出a
Integer m = 1000;
Integer n = 1000;
// 超出缓存范围,引用不同
idMap.put(m, "b");
System.out.println(idMap.get(n)); // 输出null
}
}
序列化与线程安全
IdentityHashMap本身不是线程安全的,多线程并发读写需要外部同步或使用Collections.synchronizedMap包装。在序列化时,它也能正常写入流中,但由于依赖引用身份,反序列化后生成的是全新对象,原本的引用相等关系在跨JVM后自然失效,因此不能把身份映射持久化后期待还原对象身份。
总体来看,IdentityHashMap是Java集合里一个小众但不可替代的工具。当你明确需要按对象本身而非其内容来建立映射时,它比手动维护WeakReference或自定义标识要简洁得多。理解它只认引用地址不认值的特性,就能在合适的地方用对工具。
IdentityHashMap引用地址比较Java集合修改时间:2026-08-03 13:42:33