Why does an empty Java String have hash code zero?

Viewed 819

Only recently, I discovered that an empty String has hash code zero. This is surprising to me because null is normally assigned hash code zero, e.g., Objects.hashCode(Object) and ArrayList.hashCode().

Here is the JDK 11 source code for String.hashCode():

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

Idea: An empty String could have hash code one because that would match Arrays.hashCode(Object[]) for empty arrays. Alternatively, any other hard-coded, non-zero value could be used, similar to serialVersionUID. The purpose would be to distinguish from null. If this idea is flawed (except for backwards compatibility concerns), please kindly explain why.

I found other questions/answers that approach the issue... but none answer exactly:

2 Answers

Why does an empty Java String have hash code zero?

The short answer is because that it was how it was specified way back in Java 1.2. (And the Java 1.2 spec probably matched the implementation in earlier Java versions.)

I can't think of a strong technical reason why the String.hashcode("") should be zero.

However, I disagree with your argument that String.hashCode("") should be non-zero because Objects.hashCode(null) is zero.

  1. The Objects class was added in Java 7. Likewise the Arrays.hashCode methods were added in Java 1.5. So if anything, it is Objects and Arrays that are incorrect here.

  2. There is no expectation in the hashCode() definition that any particular different pair of values should be different. At best changing the hashCode value for "" would be a small optimization. Note that String.equals(null) is handled efficiently via an instanceof test.

  3. It is unusual for a hash table to have both null and "" as keys in the same table. Indeed, I would go so far as to say that it is quite likely indicates a design or implementation flaw that you need to have entries for both null and "".

  4. It could be argued that null should not be supported as a Map key at all. I know that null can be used as a key in a HashMap or LinkedHashMap, or as a value of a HashSet. But it is not the case for ConcurrentHashMap or HashTable or TreeMap or TreeSet. Indeed, I have heard from sources who should know that:

    • the Java designers responsible for the collection types think it was a mistake to support null keys, and

    • that is one reason why ConcurrentHashMap doesn't support this.

Given that the use of null keys in an application is (arguably) misguided, a breaking optimization that offers a small improvement for null keys is equally misguided.

It could be argued that not much code actually depends on the specified details of the String.hashCode algorithm. But the problem is, neither we or the Java designers have a good way of quantifying how many old applications would actually break1.

But breaking as little as 0.001% of existing Java applications is still a lot of applications, and a lot of annoyed Oracle customers. That is sufficient to make your idea a non-starter ... for Java.


1 - The argument that it would be the application programmers fault because relying on the hashcode values is somehow "back practice" doesn't wash with me. The fact that the algorithm is specified in this case (for whatever reason) means that programmers should be able to rely on it.

The hashCode() original purpose is This method is supported for the benefit of hash tables such as those provided by HashMap.

So this means that the actual value of the hashCode does not carry any meaning other than probable similarity, as the doc states : 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.

About the arbitrary value of 0 for null and empty strings, the way the string hashCode is computed leads to other possible 0 hashCode even for non-empty strings.

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

So the 0 value of the empty string makes sense in that the computation is

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

So even of the shortcut if (h == 0 && value.length > 0) was not used, it would still lead to 0, this is just an optimization path.

In a way, one could argue that the null hashCode should not be 0 but maybe something like -1. But since the hashCode does not and should not carry any meaning, it should not matter anyway.

Related