tl; dr:永远不要使用可变的对象作为hashmap的键!
我写了文章How does HashMap work in Java?后,一些人建议了更多主题HashMap
。因此,我决定用它制作一系列简短的文章。
在今天的文章中,我们将讨论JVM哈希图。如果您不小心,我们将展示如何打破它们。但是请记住,当我们谈论JVM世界时,大多数现代语言也适用。
在本文中,我将使用Java编写实现。然后,我将解释问题的根本原因,以及如何解决我创建的问题。
如何
考虑以下简单类。它在对象内包装一个整数值,并允许开发人员获取或设置类的值:
public class IntWrapper {
private int value;
public IntWrapper(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
@Override
public boolean equals(Object other) {
if (this == other)return true;
if (!(other instanceof IntWrapper))return false;
return value == ((IntWrapper) other).value;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
}
现在让我们使用我们的班级并将其添加到HashMap
:
Map<IntWrapper, String> map = new HashMap<>();
IntWrapper myInteger = new IntWrapper(1);
map.put(myInteger, "");
myInteger.setValue(2);
if(map.containsKey(myInteger)) {
System.out.println("Our int was found!");
} else {
System.out.println("Sorry, nobody is home :(");
}
这里打印什么?如果您的答案是Sorry, nobody is home :(
,那就正确了。但是为什么?
为什么
问题是我们在哈希图中使用了一个可变类,然后将其突变。正如我在上一篇文章中提到的那样,在哈希姆普中添加一个新的密钥/值将计算键的哈希。然后,它将它们存储在相关存储桶中的一对中:
但是,在我们的情况下,我们更改了班级的价值。因此,当我们计算更新对象的哈希代码时,我们(很可能)最终会以不同的存储桶。因此,当hashmap检查存储桶时,它确实不包括所需的对象!
我们可以避免这个问题吗?
是的,我们可以!为了避免问题,我们应该使用不变的类而不是可变的类。这意味着一旦班级创建其状态就无法改变。
让我们从一类简单的普通旧Java实现开始:
public class IntWrapper {
private final int value;
public IntWrapper(int value) {
this.value = value;
}
public int getValue() {
return value;
}
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof IntWrapper)) return false;
return value == ((IntWrapper) other).value;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
}
您在本实现中可以看到,可以通过构造函数设置类值。这可以保证它不会以后改变。通过将字段标记为最终,我们确保即使使用reflection也无法在运行时更改。因此,我们的班级确实是不变的。
我们可以做得更好吗?
该代码现在可以工作。但仍然很冗长,从Java 14开始,我们有一个新功能(在Java 19中成为正式的记录)!
什么是记录?
JDK 14介绍了记录,这是一种新型类型声明。像
enum
一样,记录是类的限制形式。它的理想是“普通数据载体”,其中包含不需要更改的数据的类别,也只有最基本的方法,例如构造函数和访问者。
那是什么意思?通过将类定义为记录,我们将在未开箱即用的情况下为我们实施以下方法:
-
构造函数将把所有输入分配给类成员
-
所有类成员的获取和与之相关的私人最终字段
-
toString()
实施 -
hashCode()
&equals()
如果您想了解有关记录及其用法的更多信息,请检查Oracles official documentation
所以让我们尝试一下:
public record IntWrapper(int value) {
}
您可以看到,我们的代码现在要简单得多,如果我们尝试重复我们的地图更改,然后才会遇到错误:
Map<IntWrapper, String> map = new HashMap<>();
IntWrapper myInteger = new IntWrapper(1);
map.put(myInteger, "I am a nice int value!");
myInteger.setValue(2); // Compilation error!
if(map.containsKey(myInteger)) {
System.out.println("Our int was found!");
} else {
System.out.println("Sorry, nobody home :(");
}
如果您使用旧版本的Java,则仍然可以使用注释@Value
使用Project Lombok.实现此功能。可以提供更多详细信息here。
结论
每当您使用hashmap(使用Java或任何其他语言)时,请确保使用不变的对象。否则,它可能会导致代码执行过程中的意外行为。