本教程将带你学习Java中IdentityHashMap类的使用,并探讨它与普通HashMap类的区别。虽然IdentityHashMap实现了Map接口,但它违反了Map接口的契约——这可能是你踩坑的起点。更详细的文档可以参考官方JavaDoc,关于HashMap的详细内容可以参考Java HashMap指南

2. About the IdentityHashMap Class

这个类实现了Map接口,但行为上却是个"叛逆者":

  • 违反Map契约:Map接口规定键比较必须使用equals()方法,但IdentityHashMap偏不
  • 使用引用相等:在键搜索操作中直接使用==比较内存地址
  • 特殊哈希策略:使用System.identityHashCode()而非hashCode()
  • 线性探测技术:哈希表搜索采用线性探测法

这些特性让IdentityHashMap在特定场景下性能更优,但使用不当容易踩坑。

3. Using the IdentityHashMap Class

对象构造和方法签名与HashMap相同,但行为因引用平等而不同。

3.1. 创建对象

三种创建方式,简单粗暴:

// 默认构造器
IdentityHashMap<String, String> identityHashMap = new IdentityHashMap<>();

// 指定初始容量
IdentityHashMap<Book, String> identityHashMap = new IdentityHashMap<>(10);

// 从其他Map初始化
IdentityHashMap<String, String> identityHashMap = new IdentityHashMap<>(otherMap);

未指定容量时默认为21

3.2. 增删改查操作

// 添加元素
identityHashMap.put("title", "Harry Potter and the Goblet of Fire");
identityHashMap.put("author", "J. K. Rowling");
identityHashMap.put("language", "English");
identityHashMap.put("genre", "Fantasy");

// 批量添加
identityHashMap.putAll(otherMap);

// 获取值
String value = identityHashMap.get(key);

// 更新值(返回旧值)
String oldTitle = identityHashMap.put("title", "Harry Potter and the Deathly Hallows");
assertEquals("Harry Potter and the Goblet of Fire", oldTitle);

// 删除元素
identityHashMap.remove("title");

3.3. 遍历条目

两种遍历方式,注意fail-fast机制:

// 使用entrySet遍历
Set<Map.Entry<String, String>> entries = identityHashMap.entrySet();
for (Map.Entry<String, String> entry: entries) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

// 使用keySet遍历
for (String key: identityHashMap.keySet()) {
    System.out.println(key + ": " + identityHashMap.get(key));
}

遍历时修改Map会抛出ConcurrentModificationException

3.4. 其他方法

方法 说明
clear() 清空所有条目
containsKey() 检查键存在性(引用比较)
containsValue() 检查值存在性(引用比较)
keySet() 返回基于引用的键集合
size() 返回条目数量
values() 返回值集合

3.5. 支持Null键和值

IdentityHashMap<String, String> identityHashMap = new IdentityHashMap<>();
identityHashMap.put(null, "Null Key Accepted");
identityHashMap.put("Null Value Accepted", null);
assertEquals("Null Key Accepted", identityHashMap.get(null));
assertEquals(null, identityHashMap.get("Null Value Accepted"));

3.6. 并发处理

IdentityHashMap不是线程安全的,多线程场景需要同步包装:

Map<String, String> synchronizedMap = Collections.synchronizedMap(new IdentityHashMap<String, String>());

4. 引用相等性示例

IdentityHashMap的核心特性:引用相等优先于值相等

// 创建IdentityHashMap
IdentityHashMap<String, String> identityHashMap = new IdentityHashMap<>();
identityHashMap.put("title", "Harry Potter and the Goblet of Fire");
identityHashMap.put("author", "J. K. Rowling");
identityHashMap.put("language", "English");
identityHashMap.put("genre", "Fantasy");

// 创建普通HashMap
HashMap<String, String> hashMap = new HashMap<>(identityHashMap);
hashMap.put(new String("genre"), "Drama");
assertEquals(4, hashMap.size());  // 值相等,更新现有键

// IdentityHashMap行为不同
identityHashMap.put(new String("genre"), "Drama");
assertEquals(5, identityHashMap.size());  // 引用不同,视为新键

相同内容的不同String对象,在IdentityHashMap中会被视为不同键

5. 可变键支持

IdentityHashMap允许使用可变键,这是它区别于HashMap的实用特性:

class Book {
    String title;
    int year;
    // 其他方法包括equals, hashCode和toString
}

// 创建可变键对象
Book book1 = new Book("A Passage to India", 1924);
Book book2 = new Book("Invisible Man", 1953);

// HashMap中的问题
HashMap<Book, String> hashMap = new HashMap<>(10);
hashMap.put(book1, "A great work of fiction");
hashMap.put(book2, "won the US National Book Award");
book2.year = 1952;  // 修改键对象
assertEquals(null, hashMap.get(book2));  // 找不到值

// IdentityHashMap正常工作
IdentityHashMap<Book, String> identityHashMap = new IdentityHashMap<>(10);
identityHashMap.put(book1, "A great work of fiction");
identityHashMap.put(book2, "won the US National Book Award");
book2.year = 1951;  // 修改键对象
assertEquals("won the US National Book Award", identityHashMap.get(book2));  // 仍能找到

引用相等性让IdentityHashMap不受键对象修改影响

6. 值比较的坑

Java 20前IdentityHashMap有个隐藏陷阱:键用引用比较,值却用值比较

Book book = new Book("A Passage to India", 1924);
IdentityHashMap<Book, String> identityHashMap = new IdentityHashMap<>(10);
identityHashMap.put(book, "A great work of fiction");

// Java 20前:remove使用值比较
identityHashMap.remove(book, new String("A great work of fiction"));  // 会删除!
assertEquals(null, identityHashMap.get(book));

// replace同样问题
identityHashMap.replace(book, new String("A great work of fiction"), "One of the greatest books");
assertEquals("One of the greatest books", identityHashMap.get(book));

Java 20修复了这个问题,升级时要注意行为变化

Java 20+的正确行为:

// remove现在使用引用比较
identityHashMap.remove(book, new String("A great work of fiction"));
assertEquals("A great work of fiction", identityHashMap.get(book));  // 不会被删除

// replace同样使用引用比较
identityHashMap.replace(book, new String("A great work of fiction"), "One of the greatest books");
assertEquals("A great work of fiction", identityHashMap.get(book));  // 不会被替换

7. 典型使用场景

IdentityHashMap不是通用Map,适合特定场景:

  • 代理对象管理:维护一组可变对象的代理
  • 引用缓存:基于对象引用的快速缓存
  • 对象引用图:内存中维护带引用的对象图

8. 总结

我们深入探讨了IdentityHashMap的使用方式、与HashMap的核心差异,以及它的特殊应用场景。完整代码示例可在GitHub获取。记住:这个工具很强大,但用错地方就是个大坑。


原始标题:Java IdentityHashMap Class and Its Use Cases