【问题标题】:Consistency of hashCode() on a Java stringJava 字符串上 hashCode() 的一致性
【发布时间】:2010-10-21 13:39:32
【问题描述】:

Java 字符串的 hashCode 值计算为 (String.hashCode()):

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

在任何情况下(例如 JVM 版本、供应商等),以下表达式的计算结果是否为 false?

boolean expression = "This is a Java string".hashCode() == 586653468

更新 #1: 如果您声称答案是“是的,存在这种情况” - 那么请给出一个具体示例,说明何时“这是一个 Java 字符串”.hashCode() ! = 586653468。尽量做到具体/具体。

更新#2:我们都知道依赖 hashCode() 的实现细节通常是不好的。但是,我专门讨论的是 String.hashCode() - 所以请让答案集中在 String.hashCode() 上。 Object.hashCode() 与这个问题的上下文完全无关。

【问题讨论】:

  • 你真的需要这个功能吗?为什么需要精确值?
  • @Brian:我正在尝试理解 String.hashCode() 的合同。
  • @Knorv 没有必要准确了解它是如何工作的——更重要的是了解合同及其不可告人的含义。
  • @mP:感谢您的意见,但我想这由我来决定。
  • 为什么他们给第一个角色最大的权力?当您想优化速度以保留额外的计算时,您将存储前一个的功率,而前一个将是从最后一个字符到第一个字符。这意味着还会有缓存未命中。使用以下算法不是更有效吗: s[0] + s[1]*31 + s[2]*31^2 + ... + s[n-1]*31^[n-1 ] ?

标签: java string hashcode


【解决方案1】:

您不应依赖等于特定值的哈希码。只是它将在同一执行中返回一致的结果。 API 文档说明如下:

hashCode的一般合约是:

  • 只要在 Java 应用程序执行期间对同一个对象多次调用,hashCode 方法必须始终返回相同的整数,前提是没有修改对象上的 equals 比较中使用的信息。该整数不需要在应用程序的一次执行与同一应用程序的另一次执行之间保持一致。

编辑 由于 String.hashCode() 的 javadoc 指定了如何计算字符串的哈希码,因此任何违反此规定的行为都将违反公共 API 规范。

【讨论】:

  • 您的回答有效,但未解决所提出的具体问题。
  • 这是 general 哈希码合约 - 但是 String 的 specific 合约提供了算法的详细信息,并有效地覆盖了这个通用合约 IMO。
【解决方案2】:

另一个需要担心的 (!) 问题是 Java 早期/晚期版本之间的实现可能发生变化。我不相信实现细节是一成不变的,因此升级到未来 Java 版本可能会导致问题。

底线是,我不会依赖 hashCode() 的实现。

也许您可以通过使用此机制突出显示您实际尝试解决的问题,这将突出显示更合适的方法。

【讨论】:

  • 感谢您的回答。你能举一个具体的例子说明什么时候“这是一个Java字符串”.hashCode() != 586653468?
  • 没有。对不起。我的观点是,你测试的所有东西都可以按照你想要的方式工作。但这仍然不能保证。因此,如果您正在从事一个(比如说)您可以控制 VM 等的短期项目,那么上述内容可能对您有用。但你不能在更广阔的世界里依赖它。
  • “升级到未来的 Java 版本可能会导致问题”。升级到未来的 Java 版本可能会完全删除 hashCode 方法。或者让它总是为字符串返回 0。这对你来说是不兼容的变化。问题是 Sun^HOracle^HJCP 是否会认为这是一个重大变化,因此值得避免。由于算法在合同中,人们希望他们会这样做。
  • @SteveJessop 很好,因为switch 字符串上的语句编译为依赖特定固定哈希码的代码,更改String 的哈希码算法肯定会破坏现有代码......
【解决方案3】:

如上所述,一般而言,您不应依赖保持相同的类的哈希码。请注意,即使 同一应用程序同一 VM 上的后续运行也可能产生不同的哈希值。 AFAIK Sun JVM 的哈希函数在每次运行时都会计算相同的哈希值,但这并不能保证。

请注意,这不是理论上的。 JDK1.2 中 java.lang.String was changed 的哈希函数(旧哈希在 URL 或文件名等分层字符串方面存在问题,因为它倾向于为仅在末尾不同的字符串生成相同的哈希)。

java.lang.String 是一种特殊情况,因为它的 hashCode() 算法(现在)已记录在案,因此您可能可以依赖它。我仍然认为这是不好的做法。如果您需要具有特殊记录属性的哈希算法,只需编写一个 :-)。

【讨论】:

  • 但是 JDK 1.2 之前的文档中指定了算法吗?如果不是,那就另当别论了。该算法现在已在文档中规定,因此更改它将是对公共合同的重大更改。
  • (我记得它是 1.1。)原始(较差)算法已记录在案。不正确。记录在案的算法实际上引发了 ArrayIndexOutOfBoundsException。
  • @Jon Skeet:啊,不知道 String.hashCode() 的算法有文档记录。当然,这会改变事情。更新了我的评论。
【解决方案4】:

我可以看到早在 Java 1.2 的文档。

虽然确实一般您不应该依赖保持不变的哈希码实现,但它现在已记录在 java.lang.String 的行为中,因此更改它会被视为违反现有合同。

在可能的情况下,您不应依赖哈希码在不同版本等之间保持相同 - 但在我看来 java.lang.String 是一个特殊情况,因为算法 已被指定......所以当然,只要您愿意在指定算法之前放弃与版本的兼容性。

【讨论】:

  • 自 Java 1.2 起已指定 String 的记录行为在 API 的 v1.1 中,未为 String 类指定哈希码计算。
  • 在这种情况下,我们最好编写自己的哈希码'ight matey?
  • @Felype:恐怕我真的不知道你在这里想说什么。
  • @JonSkeet 我的意思是,在这种情况下,我们可能会编写自己的代码来生成我们自己的哈希,以授予可移植性。是吗?
  • @Felype:完全不清楚你在谈论什么样的可移植性,也不清楚你所说的“在这种情况下”是什么意思——在什么具体情况下?我怀疑你应该问一个新问题。
【解决方案5】:

只是为了回答您的问题,而不是继续任何讨论。 Apache Harmony JDK 实现似乎使用了不同的算法,至少看起来完全不同:

Sun JDK

public int hashCode() {
    int h = hash;
    if (h == 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;
}

Apache Harmony

public int hashCode() {
    if (hashCode == 0) {
        int hash = 0, multiplier = 1;
        for (int i = offset + count - 1; i >= offset; i--) {
            hash += value[i] * multiplier;
            int shifted = multiplier << 5;
            multiplier = shifted - multiplier;
        }
        hashCode = hash;
    }
    return hashCode;
}

请自行检查...

【讨论】:

  • 我认为他们只是很酷并对其进行了优化。 :) "(multiplier
  • 好吧,懒得去检查了。谢谢!
  • 但是从我这边说清楚......永远不要依赖哈希码,因为哈希码是内部的。
  • offset、count、hashCode这几个变量是什么意思?我想“哈希码”被用作缓存值,以避免将来计算,“计数”是字符数,但“偏移量”是什么?假设我希望使用此代码,使其保持一致,给定一个字符串,我应该怎么做?
  • @androiddeveloper 现在这是一个有趣的问题——尽管我应该根据您的用户名猜到。从Android docs 看起来合同是一样的:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 除非我弄错了,这是因为 Android 使用 Sun 的 String 对象的实现,没有任何变化。
【解决方案6】:

我发现了一些关于 JDK 1.0 和 1.1 和 >= 1.2 的信息:

在 JDK 1.0.x 和 1.1.x 中,hashCode 长字符串的函数由 每隔 n 个字符采样一次。这 很好保证你会有 许多字符串散列到相同 值,从而减慢 Hashtable 抬头。在 JDK 1.2 中,该函数具有 已改进以将结果相乘 到目前为止 31 然后添加下一个 字符顺序。这是一个 稍微慢一点,但在 避免碰撞。 来源:http://mindprod.com/jgloss/hashcode.html

有些不同,因为您似乎需要一个数字:使用 CRC32 或 MD5 代替哈希码怎么样,您很高兴 - 无需讨论,也无需担心...

【讨论】:

    【解决方案7】:

    如果您担心更改和可能不兼容的虚拟机,只需将现有的哈希码实现复制到您自己的实用程序类中,然后使用它来生成您的哈希码。

    【讨论】:

    • 我正要说这个。虽然其他答案确实回答了这个问题,但编写一个单独的 hashCode 函数可能是解决 knorv 问题的合适方法。
    【解决方案8】:

    哈希码将根据字符串中字符的 ASCII 值计算。

    这是String类中的实现如下

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            hash = h = isLatin1() ? StringLatin1.hashCode(value)
                                  : StringUTF16.hashCode(value);
        }
        return h;
    }
    

    哈希码中的冲突是不可避免的。例如,字符串“Ea”和“FB”给出与 2236 相同的哈希码

    【讨论】:

      猜你喜欢
      • 2022-11-16
      • 1970-01-01
      • 1970-01-01
      • 2014-01-05
      • 1970-01-01
      • 1970-01-01
      • 2015-05-24
      • 1970-01-01
      • 2011-04-01
      相关资源
      最近更新 更多