【问题标题】:Why does an empty Java String have hash code zero?为什么空 Java 字符串的哈希码为零?
【发布时间】:2020-12-13 12:45:11
【问题描述】:

直到最近,我才发现空的String 的哈希码为零。这让我很吃惊,因为null 通常分配的哈希码为零,例如Objects.hashCode(Object)ArrayList.hashCode()

这是String.hashCode() 的 JDK 11 source code

/** Cache the hash code for the string */
private int hash; // Default to 0

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

想法:一个空的String 可以有一个哈希码,因为这将匹配空数组的Arrays.hashCode(Object[])。或者,可以使用任何其他硬编码的非零值,类似于serialVersionUID。目的是与null 区分开来。如果这个想法有缺陷(除了向后兼容性问题),请解释原因。

我找到了解决该问题的其他问题/答案...但没有一个确切的答案:

【问题讨论】:

  • 甚至没有任何向后兼容性问题,因为hashCode 被明确允许在同一应用程序的运行之间进行更改。这是为了防止哈希冲突攻击。
  • @Thomas 但是,String#hashCode() is specified 的行为。
  • 如果你问为什么选择这个,你必须问他们。如果您询问是否有人依赖这种行为,您必须检查是否编写过 Java 代码(这似乎不太可能,因为所有哈希值为零都意味着字符串的哈希值为零)。
  • hashCode 不能返回 null,因为 int 不能为 null。
  • “我想知道是否有人真的依赖这种行为?” - 这无法回答。尤其是因为有些人可能在不知不觉中依赖它。

标签: java hashcode


【解决方案1】:

为什么空 Java 字符串的哈希码为零?

简短的回答是因为它是在 Java 1.2 中指定的方式。 (Java 1.2 规范可能与早期 Java 版本中的实现相匹配。)

我想不出为什么String.hashcode("") 应该为零的强有力的技术原因。

但是,我不同意你关于 String.hashCode("") 应该是非零的论点,因为 Objects.hashCode(null) 是零。

  1. Objects 类是在 Java 7 中添加的。同样,Arrays.hashCode 方法是在 Java 1.5 中添加的。所以如果有的话,这里不正确的是ObjectsArrays

  2. hashCode() 定义中不期望任何特定的不同值对应该不同。充其量更改"" 的hashCode 值将是一个small 优化。请注意,String.equals(null) 是通过 instanceof 测试有效处理的。

  3. 哈希表在同一个表中同时具有null"" 作为键是不常见的。事实上,我什至可以说,这很可能表明您需要同时拥有null"" 的条目的设计或实现缺陷。

  4. 可以说null 根本不应该支持 作为Map 键。我知道null 可以用作HashMapLinkedHashMap 中的键,或者用作HashSet 的值。但ConcurrentHashMapHashTableTreeMapTreeSet 的情况并非如此。事实上,我从应该知道的消息来源那里听说过:

    • 负责集合类型的 Java 设计者认为支持 null 键是错误的,并且

    • 这就是ConcurrentHashMap 不支持此功能的原因之一。

鉴于在应用程序中使用null 键(可以说)是错误的,破坏 优化为null 键提供 改进同样被误导了。

可以说实际上并没有多少代码依赖于String.hashCode 算法的指定细节。但问题是,无论是我们还是 Java 设计人员都没有一个好的方法来量化有多少旧应用程序实际上会崩溃1

但是,仅破坏 0.001% 的现有 Java 应用程序仍然是大量的应用程序,以及许多恼怒的 Oracle 客户。这足以让您的想法成为 Java 的初学者......。


1 - 这将是应用程序程序员的错误的论点,因为依赖哈希码值在某种程度上是“回溯实践”并不会被我洗掉。在这种情况下(无论出于何种原因)指定算法这一事实意味着程序员应该能够依赖它。

【讨论】:

  • 请注意:更改String.hashCode() 的计算方式会破坏对字符串表达式执行 switch 语句的所有操作。
  • 非常有帮助。我还查看了标准集合:空 ArrayList 的哈希码为 1,但空的 HashSetHashMap 的哈希码为 0。也许是我想太多了……
  • @ThomasKläger 你能分享更多吗?
  • @kevinarpe compiles a switch on string 解释算法的代码:“[..] 两个链式 switch 语句:第一个合成语句切换参数字符串的哈希值 [..]”
【解决方案2】:

hashCode()最初的用途是This method is supported for the benefit of hash tables such as those provided by HashMap.

所以这意味着 hashCode 的实际值除了可能相似性之外没有任何意义,正如文档所述:It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results.

关于空字符串和空字符串的0 的任意值,字符串hashCode 的计算方式会导致其他可能的0 hashCode,即使对于非空字符串也是如此。

System.out.println("".hashCode());             // >> 0
System.out.println("\0".hashCode());           // >> 0
System.out.println("\u0000".hashCode());       // >> 0
System.out.println("\u0000\u0000".hashCode()); // >> 0
System.out.println("\0\0\0".hashCode());       // >> 0

所以空字符串的0 值是有意义的,因为计算是

int h = 0;
for (byte v : value) {
    h = 31 * h + (v & 0xff);
}
return h;

所以即使没有使用快捷方式if (h == 0 && value.length > 0),它仍然会导致0,这只是一个优化路径。

在某种程度上,有人可能会争辩说null hashCode 不应该是0,而应该是-1。 但是由于 hashCode 没有也不应该有任何意义,所以无论如何都应该无关紧要。

【讨论】:

  • 这个答案认为哈希码的价值微不足道是有缺陷的;它不是。在理想的世界中,每个可能的哈希码都应该均匀分布在每个可能的值上。有一个空字符串、null 和具有相同哈希码的 nullbytes 的字符串是令人担忧的,因为它可以预见并始终导致 HashMap 和其他基于哈希的结构变慢。
  • 请修改:官方Java文档有缺陷。我的回答反映了这一点。我不是 Java 的作者。虽然我同意你评论的资助。
  • @Aplet123 他没有说“哈希码无关紧要”;他说“hashCode 没有也不应该有任何意义”,这是真的。散列函数应该均匀分布并在理想世界中完美是另一个话题。这里的问题是0 是否对"" 有价值,答案是:没关系。
  • 这很有趣。现在更陌生了!如果查看Arrays.hashCode() 的源代码,anynull 数组具有至少 个哈希码。即使整个数组被填充为零或null,哈希码也是从一开始开始的。 (是的,我知道你可以构造非空容器,使哈希码为零。见:stackoverflow.com/questions/18746394/…
  • 您是否假设“从 1 开始”意味着“从不小于 1”?如果溢出则不正确。
猜你喜欢
  • 2013-09-15
  • 1970-01-01
  • 2013-11-21
  • 2017-01-15
  • 2012-10-14
  • 1970-01-01
  • 2021-12-23
  • 1970-01-01
  • 2012-04-23
相关资源
最近更新 更多