为啥哈希图中的不可变对象如此有效?

Posted

技术标签:

【中文标题】为啥哈希图中的不可变对象如此有效?【英文标题】:Why are immutable objects in hashmaps so effective?为什么哈希图中的不可变对象如此有效? 【发布时间】:2012-05-07 17:49:49 【问题描述】:

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

“不变性还允许缓存不同键的哈希码,这使得整个检索过程非常快,并表明 Java 集合 API 提供的 String 和各种包装类(例如,Integer)是非常好的HashMap 键。”

我不太明白……为什么?

【问题讨论】:

因为在存储在 HashMap 中时更改哈希码将导致无法访问该 HashMap 中的该键,并且您也可以缓存可变对象的哈希码,我发现这句话具有误导性并且在某种程度上是错误的 我强烈建议您找到更好的信息来源,例如docs.oracle.com/javase/6/docs/technotes/guides/collections/… 上的 Javadoc 和 Java Collections Framework 文档。该博客包含许多事实、语法和拼写错误。您在此处引用的这句话完全是文盲,HashMap 缓存哈希码的明显含义是不正确的。 @EJP:我也是这么想的。另一个明显的含义是,缓存哈希码需要不变性,但事实并非如此。 您的文章链接实际上只是一个充满垃圾广告的垃圾邮件网站。我同意其他 cmets 的观点,即这篇文章的技术价值值得怀疑。 【参考方案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 系列成员的键,则不会发生这种情况。

【讨论】:

【参考方案2】:

只有当对象的哈希码在存储在表中时永远不会改变时,哈希表才会起作用。这意味着哈希码不能考虑对象在表中时可能更改的任何方面。如果对象最有趣的方面是可变的,则意味着:

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

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

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

【讨论】:

【参考方案3】:

引用链接的博客条目:

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

我看不出finalequals() 与哈希冲突有什么关系。这句话引起了我对文章可信度的怀疑。它似乎是教条式 Java“智慧”的集合。

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

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

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

示例

所以,如果我们真的很疯狂,并且真的决定使用 List 作为 HashMap 的键 并且 使哈希值取决于内容,而不是列表的身份,我们可以决定在每次修改时使缓存的哈希值无效,从而将哈希计算的次数限制为对列表的修改次数。

【讨论】:

您可以为HashMap 使用可变键,只要您确保它们永远不会被修改。但是,可变对象无法可靠地缓存其hash 值,因此每次访问地图都会浪费时间。 @Jeffrey:如果数据结构永远不会被修改,我们可以很容易地设计它,这样我们就不需要多次计算哈希。检查我的编辑。 如果您设计的数据结构永远不会被修改,那么您就是在设计一个不可变的数据结构。如果每次修改可变对象时都必须重新计算它的哈希值,那么开销可能会比每次调用 hashCode 时都重新计算它更糟糕。 @Jeffrey:每个理智的实现都会按需计算hashCode,并设置一个标志来表示已经计算了哈希码。下次修改结构时,会重置标志,以便hashCode 知道下次必须重新计算。我不知道这是否是 Java 的方式,但许多其他语言都以这种方式工作。它确实不会引入开销,并且仍然不需要每次都重新计算哈希值。这种缓存需要不变性的假设是完全错误的。 对不起,一定是看错了你写的东西。我可以发誓你说的是在每次修改时重新计算哈希。你的想法很合理,+1【参考方案4】:

将 hashmap 想象成一个由编号框组成的大数组。数字是哈希码,盒子按数字排序。

现在如果对象不能改变,散列函数将总是重现相同的值。因此,该对象将始终留在它的盒子里。

现在假设一个可变对象。它在添加到散列后被更改,所以现在它放在错误的盒子里,就像琼斯夫人碰巧嫁给了 Doe 先生,现在也被命名为 Doe,但在许多寄存器中仍然命名为 Jones。

【讨论】:

【参考方案5】:

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

【讨论】:

【参考方案6】:

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

【讨论】:

【参考方案7】:

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 的内容永远不会改变,类的创建者选择在计算一次哈希后缓存它。这样,就不会浪费时间重新计算相同的值。

【讨论】:

仅作记录:如果字符串是可变的,并且您在HashMap 中使用它,那么在您修改字符串后,哈希表将损坏并且或多或少无法使用。哈希表不知道根据新的哈希码将条目移动到其新位置。 哈希字段是否应该有transient修饰符? @Andreas 不。我引用了 JDK 的 String 类,它没有 transient 修饰符。

以上是关于为啥哈希图中的不可变对象如此有效?的主要内容,如果未能解决你的问题,请参考以下文章

Java 的不可变类 (IMMUTABLE CLASS) 和 可变类 (MUTABLE CLASS)

第7章

python可变和不可变类型

JavaScript 中的不可变对象(Immutable Objects)

如何理解“字符串是一组由16位组成的不可变的有序序列”

第7章