Java字符串上hashCode()的一致性


134

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()完全不相关。


2
您实际上是否需要此功能?为什么需要精确值?
Brian Agnew

26
@Brian:我想了解String.hashCode()的约定。
knorv

3
@Knorv不一定需要确切了解它的工作原理-更重要的是要了解合同及其别有用心的含义。
mP。

45
@mP:感谢您的输入,但是我想这取决于我。
knorv

他们为什么赋予第一个角色最大的力量?当您想对其速度进行优化以保留额外的计算时,您可以存储前一个的功能,而前一个则是从最后一个字符到第一个字符。这意味着也将有缓存未命中。具有以下算法是否更有效:s [0] + s [1] * 31 + s [2] * 31 ^ 2 + ... + s [n-1] * 31 ^ [n-1 ]?
Android开发人员

Answers:


101

我可以看到该文档最早可以追溯到Java 1.2。

这是真的 总的来说,您不应该依赖散列代码实现,而现在已记录了的行为java.lang.String,因此对其进行更改将视为违反现有合同。

只要有可能,你不应该依赖于哈希码跨版本等保持相同-但在我的脑海里java.lang.String完全是因为算法的特殊情况已经指定了......只要你愿意放弃与以前版本的兼容性当然指定了算法。


7
从Java 1.2开始,已指定String的已记录行为。在API v1.1中,未为String类指定哈希码计算。
Martin OConnor 2009年

在这种情况下,我们最好写自己的哈希码“对吗?
Felype

@Felype:恐怕我真的不知道你要说什么。
乔恩·斯基特

@JonSkeet我的意思是,在这种情况下,我们也许可以编写我们自己的代码来生成我们自己的哈希,以授予可移植性。是吗?
Felype 2013年

@Felype:不清楚您在说什么类型的可移植性,或者“在这种情况下”实际上是什么意思-在什么特定情况下?我怀疑你应该问一个新问题。
乔恩·斯基特

18

我发现了有关JDK 1.0和1.1以及> = 1.2的信息:

在JDK 1.0.x和1.1.x中,长字符串的hashCode函数通过采样第n个字符来工作。这样可以很好地保证您将许多String散列为相同的值,从而减慢了Hashtable查找的速度。在JDK 1.2中,该功能已得到改进,可以将到目前为止的结果乘以31,然后依次添加下一个字符。这有点慢,但是在避免冲突方面要好得多。资料来源:http : //mindprod.com/jgloss/hashcode.html

有所不同,因为您似乎需要一个数字:如何使用CRC32或MD5而不是哈希码,您可以轻松进行-无需讨论,也无需担心...


8

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

hashCode的一般约定为:

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

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


1
您的答案是有效的,但不能解决所提出的特定问题。
knorv

6
那是通用的哈希码合同-但是String 的特定合同提供了算法的详细信息,并有效地覆盖了此通用合同IMO。
乔恩·斯基特

4

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

注意,这不是理论上的。在JDK1.2 中更改了java.lang.String的哈希函数(旧的哈希在诸如URL或文件名之类的层次结构字符串方面存在问题,因为它倾向于为字符串产生相同的哈希,只是结尾处有所不同)。

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


4
但是在JDK 1.2之前的文档中指定了算法吗?如果没有,那就是另外一种情况。该算法现在已在文档中列出,因此对其进行更改将是对公共合同的重大更改。
乔恩·斯基特

(我记得它是1.1。)记录了原始(较差)算法。不正确的。记录在案的算法实际上引发了ArrayIndexOutOfBoundsException。
Tom Hawtin-抢险活动

@Jon Skeet:啊,不知道String.hashCode()的算法已记录在案。当然,这改变了一切。更新了我的评论。
sleske

3

另一个要担心的问题(!)是Java早期/晚期版本之间实现的可能更改。我不相信实现细节是一成不变的,因此潜在地升级到将来的 Java版本可能会引起问题。

最重要的是,我不会依赖的实现hashCode()

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


1
感谢您的回答。您能否给出“这是Java字符串” .hashCode()!= 586653468的具体示例?
knorv

1
不好意思 我的观点是,您测试的所有内容都可以按您想要的方式工作。但这仍然不能保证。因此,如果您在一个可以控制VM等的(例如)短期项目中工作,那么以上内容可能对您有用。但是您不能在更广阔的世界中依靠它。
Brian Agnew

2
“升级到将来的Java版本可能会引起问题”。升级到将来的Java版本可能会完全删除hashCode方法。或者使它始终为字符串返回0。对于您来说,这是不兼容的更改。问题是,Sun JCP Oracle JCP是否会认为这是一个重大更改,因此值得避免。由于算法在合同中,因此希望他们会这样做。
史蒂夫·杰索普

@SteveJessop好,由于switch字符串声明依赖于特定的固定哈希码编译为代码,因此对String的哈希码算法进行更改肯定会破坏现有代码……
Holger

3

仅回答您的问题,不继续任何讨论。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;
}

阿帕奇和谐

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;
}

随时自己检查...


23
我认为他们只是很酷并且正在对其进行优化。:)“(乘数<< 5)-乘数”毕竟是31 *乘数……
展开

好吧,我懒得检查一下。谢谢!
ReneS

1
但是从我这一边说清楚...永远不要依赖哈希码,因为哈希码是内部的。
ReneS

1
“ offset”,“ count”和“ hashCode”的变量是什么意思?我想将“哈希码”用作缓存值,以避免将来进行计算,并且“计数”是字符数,但是“偏移量”是多少?假设我希望使用此代码,以便在给定字符串的情况下保持一致,我应该怎么做?
Android开发人员

1
@androiddeveloper现在,这是一个有趣的问题-尽管基于您的用户名,我应该已经猜到了。从Android文档看来,合同是一样的:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]除非我没有记错,这是因为Android使用Sun的String对象的实现而未做任何更改。
Kartik Chugh

2

如果您担心更改以及VM可能不兼容的问题,只需将现有的哈希码实现复制到您自己的实用工具类中,然后使用它来生成您的哈希码。


我要说这个。当其他答案确实回答了问题时,编写单独的hashCode函数可能是解决knorv问题的适当方法。
尼克,

1

将根据字符串中字符的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相同的哈希码

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.