如何在不到一分钟的时间内打破hashmap
#java #guide #jvm #hashmap

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 :(,那就正确了。但是为什么?

为什么

问题是我们在哈希图中使用了一个可变类,然后将其突变。正如我在上一篇文章中提到的那样,在哈希姆普中添加一个新的密钥/值将计算键的哈希。然后,它将它们存储在相关存储桶中的一对中:

1*UVvdHtSHcnMofPVdogGKtg.png

但是,在我们的情况下,我们更改了班级的价值。因此,当我们计算更新对象的哈希代码时,我们(很可能)最终会以不同的存储桶。因此,当hashmap检查存储桶时,它确实不包括所需的对象!

1*-ehInn6beG7s3ry6JnqOBQ.png

我们可以避免这个问题吗?

0*a_vh_4bR2vHyx220

是的,我们可以!为了避免问题,我们应该使用不变的类而不是可变的类。这意味着一旦班级创建其状态就无法改变。

让我们从一类简单的普通旧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

结论

0*J4M6sTFLBqyDRLw8

每当您使用hashmap(使用Java或任何其他语言)时,请确保使用不变的对象。否则,它可能会导致代码执行过程中的意外行为。