为什么哈希映射中的不可变对象如此有效?

时间:2012-04-26 23:33:34

标签: java performance caching hashmap immutability

所以我读到了HashMap。有人指出:

  

“Immutability还允许缓存不同键的哈希码,这使得整个检索过程非常快,并建议Java Collection API提供的String和各种包装类(例如,Integer)非常好{{1} }键。“

我不太明白......为什么?

7 个答案:

答案 0 :(得分:33)

String#hashCode

private int hash;

...

public int hashCode() {
    int h = hash;
    if (h == 0 && count > 0) {
        int off = offset;
        char val[] = value;
        int len = count;

        for (int i = 0; i < len; i++) {
            h = 31*h + val[off++];
        }
        hash = h;
    }
    return h;
}

由于String的内容永远不会改变,所以该类的制作者选择在计算一次后缓存哈希。这样,时间不会浪费重新计算相同的值。

答案 1 :(得分:10)

引用链接的博客条目:

  

使用正确的equals()和hashcode()实现的最终对象将充当完美的Java HashMap密钥,并通过减少冲突来提高Java hashMap的性能。

我无法看到finalequals()如何与哈希冲突有关。这句话引起了我对该条款可信度的怀疑。它似乎是教条式Java“智慧”的集合。

  

Immutability还允许缓存不同键的哈希码,这使得整个检索过程非常快,并建议String和各种包装类,例如Java Collection API提供的Integer是非常好的HashMap键。

我看到这句话有两种可能的解释,这两种解释都是错误的:

  • HashMap缓存不可变对象的哈希码。这是不正确的。地图无法确定对象是否“不可变”。
  • 对象需要不可变性来缓存自己的哈希码。理想情况下,对象的哈希值总是只依赖于对象的非变异状态,否则该对象无法合理地用作键。所以在这种情况下,作者也没有说明一点:如果我们假设我们的对象没有改变它的状态,我们也不必每次都重新计算哈希值,即使我们的对象是可变的!

实施例

因此,如果我们真的很疯狂,并且实际上决定使用List作为HashMap 的键并且使哈希值依赖于内容,而不是列表的标识,我们可以决定在每次修改时使缓存的哈希值无效,从而将哈希计算的数量限制为对列表的修改次数。

答案 2 :(得分:6)

这很简单。由于 immutable 对象不会随时间发生变化,因此只需执行一次哈希码的计算。再次计算它将产生相同的值。因此,通常在构造函数(或懒惰)中计算哈希代码并将其存储在字段中。然后hashcode函数只返回字段的值,这确实非常快。

答案 3 :(得分:1)

基本上不变性是通过使类不可扩展而在Java中实现的,并且对象中的所有操作理想地不会改变对象的状态。如果您看到String的操作(如replace()),它不会更改您正在操作的当前对象的状态,而是为您提供一个带有替换字符串的新String对象。理想情况下,如果您将这些对象维护为键,则状态不会更改,因此哈希代码也保持不变。因此,在检索过程中缓存哈希码将会提高性能。

答案 4 :(得分:1)

将hashmap视为一个大数字编号框。数字是哈希码,方框按编号排序。

现在,如果对象无法更改,则哈希函数将始终重现相同的值。因此,对象将始终保留在其框中。

现在假设一个可变对象。将它添加到哈希后更改,所以现在它坐在错误的方框中,就像琼斯太太碰巧与多伊先生结婚,现在它也被命名为Doe,但在许多寄存器中仍然命名为琼斯。

答案 5 :(得分:1)

不可修改的类是不可修改的,这就是为什么它们被用作Map中的键的原因。

例如-

    StringBuilder key1=new StringBuilder("K1");
    StringBuilder key2=new StringBuilder("K2");
    
    Map<StringBuilder, String> map = new HashMap<>();
    map.put(key1, "Hello");
    map.put(key2, "World");
    
    key1.append("00");
    System.out.println(map); // This line prints - {K100=Hello, K2=World}

您会看到插入到映射中的密钥K1(这是可变类StringBuilder的对象)由于意外更改而丢失了。如果您将不可变的类用作Map系列成员的键,则不会发生这种情况。

答案 6 :(得分:0)

哈希表仅在对象的哈希码在存储在表中时永远不会更改时才起作用。这意味着哈希代码不能考虑对象的任何方面,这些方面可能会在表中发生变化。如果一个对象最有趣的方面是可变的,那意味着:

  • 哈希码必须忽略对象的大部分有趣方面,从而导致许多哈希冲突,或者......

  • 拥有哈希表的代码必须确保其中的对象不会暴露于存储在哈希表中时可能会更改它们的任何内容。

如果Java哈希表允许客户端提供EqualityComparer(.NET字典的方式),那么知道哈希表中对象的某些方面不会意外更改的代码可能会使用哈希代码将这些方面带入但是,在Java中实现这一目标的唯一方法是将存储在哈希码中的每个项目包装在一个包装器中。然而,这样的包装可能不是世界上最邪恶的东西,因为包装器能够以EqualityComparer不能的方式缓存哈希值,并且还可以缓存进一步的相等相关信息[例如,如果存储的内容是嵌套集合,那么计算多个哈希代码可能是值得的,并且在对元素进行任何详细检查之前确认所有哈希代码都匹配]。