当前教程
选择不可变类型作为键
这是系列的结尾!

选择不可变类型作为键

 

避免使用可变键

使用可变键是一种反模式,你应该绝对避免这样做。如果你这样做,你可能会得到可怕的副作用:你最终可能会使你的映射内容无法访问。

设置一个示例来展示这一点非常容易。这里有一个Key类,它只是对String的可变包装器。请注意,equals()hashCode()方法已被你的 IDE 可以生成的代码覆盖。

//
// !!!!! This an example of an antipattern !!!!!!
// !!! do not do this in your production code !!!
//
class Key {
    private String key;

    public Key(String key) {
        this.key = key;
    }

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    @Override
    public String toString() {
        return key;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Key key = (Key) o;
        return Objects.equals(this.key, key.key);
    }

    @Override
    public int hashCode() {
        return key.hashCode();
    }
}

你可以使用这个包装器来创建一个映射,在其中放入键值对。

Key one = new Key("1");
Key two = new Key("2");

Map<Key, String> map = new HashMap<>();
map.put(one, "one");
map.put(two, "two");

System.out.println("map.get(one) = " + map.get(one));
System.out.println("map.get(two) = " + map.get(two));

到目前为止,这段代码还可以,并打印出以下内容

map.get(one) = one
map.get(two) = two

如果有人修改你的键会发生什么?好吧,这真的取决于修改。你可以尝试以下示例中的修改,看看当你尝试获取你的值时发生了什么。

在以下情况下,你正在用一个不对应于已存在键的新值修改一个现有键。

one.setKey("5");

System.out.println("map.get(one) = " + map.get(one));
System.out.println("map.get(two) = " + map.get(two));
System.out.println("map.get(new Key(1)) = " + map.get(new Key("1")));
System.out.println("map.get(new Key(2)) = " + map.get(new Key("2")));
System.out.println("map.get(new Key(5)) = " + map.get(new Key("5")));

结果如下。你无法再从键中获取值,即使你使用相同的对象。从持有原始值的键中获取值也会失败。这个键值对丢失了。

map.get(one) = null
map.get(two) = two
map.get(new Key(1)) = null
map.get(new Key(2)) = two
map.get(new Key(5)) = null

如果你用另一个现有键的值修改你的键,结果将不同。

one.setKey("2");

System.out.println("map.get(one) = " + map.get(one));
System.out.println("map.get(two) = " + map.get(two));
System.out.println("map.get(new Key(1)) = " + map.get(new Key("1")));
System.out.println("map.get(new Key(2)) = " + map.get(new Key("2")));

结果现在如下。获取与修改后的键绑定的值将返回与另一个键绑定的值。并且,与前面的示例一样,你无法再获取与修改后的键绑定的值。

map.get(one) = two
map.get(two) = two
map.get(new Key(1)) = null
map.get(new Key(2)) = two

如你所见,即使在一个非常简单的示例中,事情也可能变得非常糟糕:第一个键无法再用于访问正确的值,并且你可能会在此过程中丢失值。

简而言之:如果你真的无法避免使用可变键,请不要修改它们。但你最好的选择是使用不可修改的键。

 

深入研究 HashSet 的结构

你可能想知道为什么在本节中讨论HashSet类会很有趣?好吧,事实证明,HashSet类实际上是建立在内部的HashMap上的。因此,这两个类共享一些共同的功能。

以下是add(element)的代码,它是HashSet类的一部分

private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

你可以看到,实际上,哈希集将你的对象存储在一个哈希映射中(transient关键字无关紧要)。你的对象是这个哈希映射的键,而值只是一个占位符,一个没有意义的对象。

这里要记住的重要一点是,如果你在将对象添加到集合后修改了它们,你可能会在你的应用程序中遇到奇怪的错误,这些错误很难修复。

让我们再次使用前面的示例,使用可变的Key类。这次,你将把这个类的实例添加到一个集合中。

Key one = new Key("1");
Key two = new Key("2");

Set<Key> set = new HashSet<>();
set.add(one);
set.add(two);

System.out.println("set = " + set);

// You should never mutate an object once it has been added to a Set!
one.setKey("3");
System.out.println("set.contains(one) = " + set.contains(one));
boolean addedOne = set.add(one);
System.out.println("addedOne = " + addedOne);
System.out.println("set = " + set);

运行这段代码会产生以下结果

set = [1, 2]
set.contains(one) = false
addedOne = true
set = [3, 2, 3]

你可以看到,实际上集合的第一个元素和最后一个元素是相同的

List<Key> list = new ArrayList<>(set);
Key key0 = list.get(0);
Key key2 = list.get(2);

System.out.println("key0 = " + key0);
System.out.println("key2 = " + key2);
System.out.println("key0 == key2 ? " + (key0 == key2));

如果你运行这段最后一段代码,你将得到以下结果

key0 = 3
key2 = 3
key0 == key2 ? true

在这个示例中,你看到,在将对象添加到集合后修改对象会导致在该集合中多次出现同一个对象。简单地说,不要这样做!


最后更新: 2021 年 9 月 14 日


当前教程
选择不可变类型作为键
这是系列的结尾!