Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 4 – Arrays and strings in depth

String hashing, interning and locale traps

How Java computes String.hashCode and why Aa and BB collide, when equal strings share one object in the pool, and the Turkish I trap in toLowerCase().

  • Intermediate
  • 20 minutes
  • Examples run with Java temurin-25.0.4+101.0.LTS
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Compute String.hashCode by hand, and turn a negative hash code into an index with Math.floorMod
  • Build strings that share one hash code, and explain why a HashMap stays correct and when it slows down
  • Predict which equal strings share one object in the string pool, and compare text with equals
  • Explain why a key that changes is lost inside a HashMap, and choose keys that cannot change
  • Convert the case of keys and identifiers with Locale.ROOT, so the result is the same on every computer

Before you start

What you’ll learn · Before you start

What you will learn

  • Compute String.hashCode by hand, and turn a negative hash code into an index with Math.floorMod
  • Build strings that share one hash code, and explain why a HashMap stays correct and when it slows down
  • Predict which equal strings share one object in the string pool, and compare text with equals
  • Explain why a key that changes is lost inside a HashMap, and choose keys that cannot change
  • Convert the case of keys and identifiers with Locale.ROOT, so the result is the same on every computer

Before you start

On this page

"Aa" and "BB" have the same hash code, 2112, and so do all 65,536 strings that can be built from 16 such blocks. String.hashCode is not a secret or random number: its formula is part of the documentation, so collisions are easy to make, and thousands of colliding keys can slow a HashMap down. Equal strings sometimes share one object in the string pool and sometimes do not, which is why == is unreliable for comparing text. And toLowerCase() without arguments follows the language the computer is set to: on a Turkish system "TITLE" becomes "tıtle", and a lookup by that key fails.

You will compute the hash by hand, flood a map with collisions and time it, see which strings the pool shares and why a key that changes is lost, and fix the locale bug with Locale.ROOT. The exercise finds a repeated substring with a rolling hash that must survive a collision.

hashCode multiplies by 31 and adds each char

The documented formula is s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] in int arithmetic, where s[i] are the chars. The usual way to evaluate it is a loop: multiply the result so far by 31 and add the next char. HashFormula.java does that for "Pune" and compares the result with hashCode():

The formula by hand Java · HashFormula.java
// String.hashCode is documented as s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1], computed in int arithmetic.
// The loop below is the usual way to evaluate that sum: multiply by 31, add the next char.
void main() {
    int h = 0;
    for (char c : "Pune".toCharArray()) {
        int before = h;
        h = 31 * h + c;
        IO.println(String.format("31 * %,9d + '%c' (%3d) = %,10d", before, c, (int) c, h));
    }
    IO.println("\"Pune\".hashCode() = " + "Pune".hashCode());
    IO.println("\"Mumbai\".hashCode() = " + "Mumbai".hashCode() + ": the sum passed 2,147,483,647 and wrapped round");
    IO.println("\"Aa\".hashCode() = " + "Aa".hashCode() + ", \"BB\".hashCode() = " + "BB".hashCode());
    IO.println("because 65 * 31 + 97 = " + (65 * 31 + 97) + " and 66 * 31 + 66 = " + (66 * 31 + 66));
    IO.println("\"\".hashCode() = " + "".hashCode());

    // A hash table picks a bucket from the hash code. With a negative code, % gives a negative index.
    int mumbai = "Mumbai".hashCode();
    IO.println("Mumbai's hash % 8 = " + (mumbai % 8) + ", Math.floorMod(hash, 8) = " + Math.floorMod(mumbai, 8));
}

Output

31 *         0 + 'P' ( 80) =         80
31 *        80 + 'u' (117) =      2,597
31 *     2,597 + 'n' (110) =     80,617
31 *    80,617 + 'e' (101) =  2,499,228
"Pune".hashCode() = 2499228
"Mumbai".hashCode() = -1979126203: the sum passed 2,147,483,647 and wrapped round
"Aa".hashCode() = 2112, "BB".hashCode() = 2112
because 65 * 31 + 97 = 2112 and 66 * 31 + 66 = 2112
"".hashCode() = 0
Mumbai's hash % 8 = -3, Math.floorMod(hash, 8) = 5

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java HashFormula.java

Step through the loop, then see what happens to a longer name:

Interactive · stage by stage "Pune".hashCode(), one char at a time Step through h = 31 * h + c for the four chars of Pune, then see what int arithmetic does to a longer name.
"Pune".hashCode(), one char at a time, step 6 of 6. Showing 'P', code 80 (h = 80), 'u', code 117 (h = 2,597), 'n', code 110 (h = 80,617), 'e', code 101 (h = 2,499,228), hashCode() (% 8 is -3, floorMod is 5). In focus: hashCode().'P', code 80h = 80'u', code 117h = 2,597'n', code 110h = 80,617'e', code 101h = 2,499,228hashCode()% 8 is -3, floorMod is 5

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. P. The loop starts with h = 0. The first char, P, has the code 80, so h becomes 31 × 0 + 80 = 80, the first line HashFormula.java printed.
  2. u. Each step multiplies the hash so far by 31 and adds the next char: 31 × 80 + 117 = 2,597. The chars before keep their weight, one more factor of 31 per step.
  3. n. 31 × 2,597 + 110 = 80,617. After three chars the hash already needs five digits; it grows about 31 times with every char.
  4. e. 31 × 80,617 + 101 = 2,499,228, exactly the value "Pune".hashCode() returned in the run: the loop is the documented formula s[0]·31³ + s[1]·31² + s[2]·31 + s[3].
  5. A longer name wraps round. Six chars push the sum past 2,147,483,647, the largest int, and int arithmetic wraps round to negative numbers: "Mumbai" hashes to -1,979,126,203.
  6. From hash to bucket. A table of 8 buckets cannot use hash % 8, which is -3 for Mumbai and no index at all. Math.floorMod(hash, 8) gives 5, and it always lands between 0 and 7.

  • The four steps for "Pune" give 2,499,228, the same number hashCode() returns.
  • Longer strings pass the largest int, 2,147,483,647, and wrap round, an integer overflow: What happens when the result of +, -, * or ++ does not fit its integer type: Java keeps the lowest 32 or 64 bits of the true result, so the value wraps round without an error, and Integer.MAX_VALUE + 1 is Integer.MIN_VALUE. More in “Integer arithmetic, division and overflow” that raises no error: "Mumbai" already hashes to a negative number.
  • Code that picks a bucket: One slot of a hash table's array. A HashMap picks a key's bucket from its hash code, keeps the keys that land in the same bucket together, and searches them with equals. or a shard with hash % 8 then gets a negative index, -3. Math.floorMod(hash, 8), the remainder of floor division: Division that rounds the quotient down, toward negative infinity: Math.floorDiv(-7, 2) is -4. Math.floorMod gives the remainder that goes with it, which has the sign of the divisor. More in “Integer arithmetic, division and overflow”, always lands between 0 and 7.
  • The empty string hashes to 0, because the loop has nothing to add.

Checkpoint

Check: The String.hashCode formula

  1. Question 1 of 2 'H' is 72 and 'i' is 105. What is "Hi".hashCode()?

    Type a number.

    A hint

    For two chars the formula is s[0] × 31 + s[1].

    Show the answer: Question 1 of 2

    Answer: 2337

    72 × 31 + 105 = 2,232 + 105 = 2,337. The loop of HashFormula.java gets there in two steps: 31 × 0 + 72 = 72, then 31 × 72 + 105 = 2,337.

  2. Question 2 of 2 A hash code is -13. Which bucket of 8 does Math.floorMod(-13, 8) give?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 3

    floorMod returns a result with the sign of the divisor, between 0 and 7 here: -13 = -2 × 8 + 3. The % operator gives -5, which is no index of an array.

Aa and BB collide, and so does every string built from them

'A' is 65 and 'a' is 97, so "Aa" is 65 × 31 + 97 = 2112; "BB" is 66 × 31 + 66, also 2112. That is a hash collision: Two different keys with the same hash code, such as Aa and BB, which both hash to 2112. A hash table keeps both and tells them apart with equals; many collisions in one place make its lookups slow., and it does not stay alone. Because the hash of a longer string is built from the hashes of its parts, any string made of such blocks has the same hash as every other string of the same number of blocks. Collisions.java builds the families of 2 and 4 blocks:

Families of strings with one hash code Java · Collisions.java
// "Aa" and "BB" have the same hash code, so any string made of k such blocks has the same hash code as every other
// one: 2^k strings with one hash. Here k = 2, then k = 4.
void main() {
    for (int blocks : new int[] {2, 4}) {
        List<String> family = family(blocks);
        Set<Integer> hashes = new HashSet<>();
        for (String s : family) {
            hashes.add(s.hashCode());
        }
        IO.println(family.size() + " strings: " + String.join(" ", family));
        IO.println("distinct hash codes among them: " + hashes);
    }

    List<String> family = family(4);
    Map<String, Integer> map = new HashMap<>();
    for (int i = 0; i < family.size(); i++) {
        map.put(family.get(i), i);
    }
    IO.println("a HashMap still keeps them apart: size " + map.size() + ", get(\"BBAaBBAa\") = " + map.get("BBAaBBAa"));
}

// Every string of the given number of blocks, each block "Aa" or "BB".
List<String> family(int blocks) {
    List<String> family = new ArrayList<>(List.of(""));
    for (int block = 0; block < blocks; block++) {
        List<String> longer = new ArrayList<>();
        for (String s : family) {
            longer.add(s + "Aa");
            longer.add(s + "BB");
        }
        family = longer;
    }
    return family;
}

Output

4 strings: AaAa AaBB BBAa BBBB
distinct hash codes among them: [2031744]
16 strings: AaAaAaAa AaAaAaBB AaAaBBAa AaAaBBBB AaBBAaAa AaBBAaBB AaBBBBAa AaBBBBBB BBAaAaAa BBAaAaBB BBAaBBAa BBAaBBBB BBBBAaAa BBBBAaBB BBBBBBAa BBBBBBBB
distinct hash codes among them: [-540425984]
a HashMap still keeps them apart: size 16, get("BBAaBBAa") = 10

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Collisions.java

Step from the one collision to families of any size:

Interactive · stage by stage One collision, then a whole family Step from the two blocks with one hash code to families of 4, 16 and 65,536 strings, then into a HashMap.
One collision, then a whole family, step 5 of 5. Showing one block: Aa or BB, two blocks: 4 strings, four blocks: 16 strings, 16 blocks: 65,536 strings, a HashMap of 16 of them. In focus: a HashMap of 16 of them.one block: Aa or BBboth hash to 2112two blocks: 4 stringsone hash: 2,031,744four blocks: 16 stringsone hash: -540,425,98416 blocks: 65,536 stringswhat Flood.java usesa HashMap of 16 of themequals keeps them apart

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Two blocks, one hash. A is 65 and a is 97, so "Aa" hashes to 65 × 31 + 97 = 2112. "BB" is 66 × 31 + 66, also 2112: the 1 that B adds to the first char is worth 31, exactly what it takes from the second.
  2. Blocks in a row. The hash of s followed by a 2-char block t is the hash of s × 31² plus the hash of t. Both blocks hash alike, so AaAa, AaBB, BBAa and BBBB all hash to 2,031,744, as the run printed.
  3. Each block doubles the family. Every extra block doubles the strings and keeps their hash shared: 4 blocks give 2⁴ = 16 strings, all hashing to -540,425,984, the one value the run found among them.
  4. As many as you like. 16 blocks give 2¹⁶ = 65,536 strings of 32 chars with one hash code. That is the large family Flood.java puts into a map in the next section, and making it takes a loop of a few lines.
  5. A map stays correct. HashMap compares keys with equals once their hashes match, so the 16 strings are 16 entries, and get("BBAaBBAa") finds its own value, 10. Collisions cost time, not correctness.

4 blocks give 2⁴ = 16 strings with one hash code, and 16 blocks give 65,536. Equal hash codes do not make keys equal: HashMap compares keys with equals once their hashes match, so all 16 strings are separate entries and each is found again. Collisions cost time, not correctness, as the next section measures.

Checkpoint

Check: Families of colliding strings

  1. Question 1 of 2 Which pair of strings has equal hash codes?

    Choose one answer.

    A hint

    Raising the first char by 1 adds 31 to the hash. What must happen to the second char?

    Show the answer: Question 1 of 2

    Answer: Ab and BC

    Any two-char strings whose first chars differ by 1 and whose second chars differ by 31 the other way collide, which is why Aa and BB are not a rare accident.

  2. Question 2 of 2 How many strings of 10 blocks, each block Aa or BB, share one hash code?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 1024 strings

    Each block doubles the family, so 10 blocks give 2¹⁰ = 1,024 strings with one hash code. 4 blocks gave the run's 16, and 16 blocks give the 65,536 of Flood.java.

Colliding keys slow a HashMap down unless they can be ordered

HashMap keeps the keys of one bucket together and searches them by equals. Its documentation warns that many keys with the same hash code slow down any hash table, and says that for keys that are Comparable it may use their order to break ties. Since JDK 8 a crowded bucket can become a balanced tree (JEP 180). String is Comparable; the record below is not:

Colliding keys: String against a record that is not Comparable Java · Flood.java
// Puts thousands of keys with one shared hash code into a HashMap, then looks each one up again.
// HashMap keeps colliding keys in one bucket; for keys that are Comparable, such as String, it can order a crowded
// bucket as a tree. Tag is a record that is not Comparable and whose hash code is always 42.
record Tag(String name) {
    @Override
    public int hashCode() {
        return 42;
    }
}

List<String> family(int blocks) {
    List<String> family = new ArrayList<>(List.of(""));
    for (int b = 0; b < blocks; b++) {
        List<String> longer = new ArrayList<>();
        for (String s : family) {
            longer.add(s + "Aa");
            longer.add(s + "BB");
        }
        family = longer;
    }
    return family;
}

long fillAndFind(List<?> keys) {
    long start = System.nanoTime();
    Map<Object, Integer> map = new HashMap<>();
    for (Object k : keys) {
        map.put(k, 1);
    }
    int found = 0;
    for (Object k : keys) {
        if (map.containsKey(k)) {
            found++;
        }
    }
    if (found != keys.size()) {
        throw new AssertionError("lost keys");
    }
    return (System.nanoTime() - start) / 1_000_000;
}

void main() {
    List<String> small = family(13);      // 8,192 strings
    List<String> large = family(16);      // 65,536 strings
    List<Tag> tags = small.stream().map(Tag::new).toList();
    IO.println(String.format("%,6d String keys, one hash code: %,6d ms", small.size(), fillAndFind(small)));
    IO.println(String.format("%,6d String keys, one hash code: %,6d ms", large.size(), fillAndFind(large)));
    IO.println(String.format("%,6d Tag keys, one hash code:    %,6d ms", tags.size(), fillAndFind(tags)));
}

Output

 8,192 String keys, one hash code:     51 ms
65,536 String keys, one hash code:     38 ms
 8,192 Tag keys, one hash code:       673 ms

This output changes from run to run: The times change from run to run with the load on the computer; which kind of key is faster does not.

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Flood.java

All the keys of each run share one bucket. Step through one lookup in it, first for the strings, then for the records:

Interactive · step by step One lookup in a bucket that holds every key Step through a lookup when all keys share one hash code, first with String keys, then with the Tag record.
One lookup in a bucket that holds every key, step 5 of 5. Showing hashCode() of the key, one crowded bucket, String: by compareTo (about 16 steps for 65,536), Tag: no order to use (up to 8,192 per lookup), equals confirms. In focus: equals confirms.Comparable keysother keyshashCode() of the keythe same for every keyone crowded bucketevery key lands hereString: by compareToabout 16 steps for 65,536Tag: no order to useup to 8,192 per lookupequals confirmsthe key is found

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. One hash for every key. Every String key of the run is a block family with one hash code, and every Tag returns 42 from its hashCode method. A lookup starts by computing that hash.
  2. One bucket. So every key lands in the same bucket: 8,192 keys in the first run and 65,536 in the second, while the rest of the table stays empty.
  3. String keys. String is Comparable, so a crowded bucket can be kept as a tree in compareTo order. A lookup goes down the tree: about 13 comparisons among 8,192 keys and about 16 among 65,536.
  4. Tag keys. Tag is a record that is not Comparable, so the order of its keys tells a lookup nothing. It may have to look at every key in the bucket, and every one of the 8,192 lookups pays for that again.
  5. equals decides. Either way, equals has the last word: a key is found only when equals says it is the same key. Collisions slow a map down, far more for 8,192 Tags than for 65,536 Strings, but never give a wrong answer.

The 65,536 colliding String keys were stored and found again quickly, because the bucket became a tree ordered by compareTo, so each lookup takes about 16 comparisons instead of tens of thousands. The 8,192 Tag keys took many times longer, with an eighth as many keys: without an order, a lookup may have to visit every key in the bucket, and the total work grows with the square of the number of keys.

Colliding keys from outside

The formula is public, so anyone can produce millions of colliding strings in advance and send them to a program that stores them in one map, which is called Sending a program many keys that share one hash code, so that one bucket of its hash table holds them all and every lookup slows down. Limiting how many keys one request may carry protects against it.. A service that puts names from untrusted input into a map, such as the field names of a JSON request or the parameters of a form, should limit how many it accepts. Never use hashCode where an attacker gains by finding two inputs with the same value: it is not a cryptographic hash.

Hash Generator Compute a SHA-256 hash, the kind to use when two inputs must never be made to collide; String.hashCode is not one.

Checkpoint

Check: Many colliding keys in a HashMap

  1. Question 1 of 2 About how many comparisons does one lookup take in a crowded bucket of 1,024 String keys that is kept as a tree?

    Type a number.

    A hint

    A balanced tree halves the keys left with each comparison.

    Show the answer: Question 1 of 2

    Answer: 10 comparisons (anything from 9 to 11 counts)

    A balanced tree of n keys is about log₂ n levels deep, and log₂ 1,024 = 10. That is why 65,536 colliding String keys took only about 16 comparisons a lookup.

  2. Question 2 of 2 Why did 8,192 colliding Tag keys take far longer than 65,536 colliding String keys?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Tag is not Comparable, so the crowded bucket cannot be searched in key order

    HashMap may use the order of Comparable keys to break ties in a crowded bucket. String gives it that order; Tag does not, and the run's times show the difference.

Literals and constant expressions share one pooled object

Seven expressions below all have the text "Pune". The figure shows which of them refer to one shared object and which make objects of their own:

Five ways of writing "Pune" point to one interned object in the string pool; a run-time join and new String each make their own object.Which strings share one objectcity = "Pune""Pu" + "ne"fixedPart + "ne"a text block "Pune"(part + "ne").intern()part + "ne"new String("Pune")string pool"Pune"the interned copyother String objects"Pune"built at run time"Pune"a new object== is true only when both sides are one object.

Which strings share one object

Text description of the diagram

On the left are seven expressions that all have the value "Pune". Arrows lead from each to the String object it refers to.

  1. Five expressions point to one object in the string pool, the interned copy: the literal "Pune" in city = "Pune", the constant expression "Pu" + "ne", fixedPart + "ne" where fixedPart is a final variable with a constant value, a text block holding Pune, and (part + "ne").intern().
  2. part + "ne", where part is an ordinary variable, points to a separate object, built at run time, outside the pool.
  3. new String("Pune") points to another separate object, a new object every time.

The note below says that == is true only when both sides are one object, so == between expressions of the first group is true, and false between any of them and the last two.

The Java Language Specification makes one promise about shared string objects: string literals, and more generally strings that are the values of constant expressions, are interning: Keeping one shared String object for each distinct text. Every string literal and every String constant expression of a program is interned, and intern() returns the shared copy, which is why == can be true between them. More in “Strings: first look and equality”, so every occurrence of the same such string refers to one object. Everything else is an ordinary object:

Seven comparisons with == Java · Pool.java
// When do two equal strings share one object? == answers that question (and only that one).
void main() {
    String city = "Pune";
    String joinedByCompiler = "Pu" + "ne";          // a constant expression: javac joins it
    String part = "Pu";
    String joinedAtRunTime = part + "ne";           // part is a variable, so this runs at run time
    final String fixedPart = "Pu";
    String joinedConstant = fixedPart + "ne";       // a final variable with a constant value is a constant too
    String copy = new String("Pune");
    String block = """
            Pune""";

    IO.println("city == \"Pu\" + \"ne\":             " + (city == joinedByCompiler));
    IO.println("city == part + \"ne\":             " + (city == joinedAtRunTime));
    IO.println("city == fixedPart + \"ne\":        " + (city == joinedConstant));
    IO.println("city == new String(\"Pune\"):      " + (city == copy));
    IO.println("city == a text block \"Pune\":     " + (city == block));
    IO.println("city == (part + \"ne\").intern():  " + (city == joinedAtRunTime.intern()));
    IO.println("city.equals(part + \"ne\"):        " + city.equals(joinedAtRunTime));
}

Output

city == "Pu" + "ne":             true
city == part + "ne":             false
city == fixedPart + "ne":        true
city == new String("Pune"):      false
city == a text block "Pune":     true
city == (part + "ne").intern():  true
city.equals(part + "ne"):        true

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Pool.java

  • "Pu" + "ne" is a constant expression: An expression the compiler works out itself, made only of literals, constant variables and operators, such as "Pu" + "ne" or 60 * 60. Its value is fixed at compile time, and one of type String is interned like a literal. More in “Strings: first look and equality”, so the compiler joins it and it is the same pooled object as "Pune". part + "ne" reads a variable, so it runs at run time and creates a new string: == is false although the text is the same.
  • A final variable initialised with a constant is itself a constant, a constant variable: A final variable of a primitive type or String initialised with a constant expression, such as static final int LIMIT = 500. javac copies its value into the code that reads it. More in “How Java runs: bytecode, class loading, JIT”, so fixedPart + "ne" is a constant expression again and shares the pooled object. A text block is a literal too.
  • new String("Pune") always creates a new object, so it is never == to another string. There is almost never a reason to write it.
  • intern() returns the pooled object with the same contents, adding the string to the pool if needed, so s.intern() == t.intern() is true exactly when s.equals(t). Application code rarely needs it: compare text with equals, and if a program holds millions of duplicate strings, the JVM can share their contents for you with -XX:+UseStringDeduplication, which JEP 192 introduced for the G1 collector.

Checkpoint

Check: The string pool and ==

  1. Question 1 of 2 static final String START = "Pu"; Is START + "ne" == "Pune" true?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Yes, because START is a constant variable, so START + "ne" is a constant expression and shares the pooled "Pune"

    Constant expressions are interned. Make START non-final, and START + "ne" is computed at run time into a new object, so == is false.

  2. Question 2 of 2 What does new String("Pune") == "Pune" give?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: false, because new always creates a new object

    There is almost never a reason to write new String("…"). Use the literal, and compare text with equals.

A key that changes is lost inside a HashMap

Strings make good map keys because they never change: a String is an immutable object: An object whose state can never change once it exists, such as a String. Methods that seem to change it, like toUpperCase(), return a new object, so one object can be shared safely by many variables and threads. More in “Strings: first look and equality”, so its hash code stays the same for as long as it is a key. A List key is different. MutableKey.java changes one after it went into a HashMap, then tries a String key:

A list key that changes, and a string key that cannot Java · MutableKey.java
// Why immutable keys matter: a key that changes after it went into a HashMap cannot be found again.
void main() {
    Map<List<String>, Integer> byRoute = new HashMap<>();
    List<String> route = new ArrayList<>(List.of("Pune", "Mumbai"));
    IO.println("hash code of the route when it goes in: " + route.hashCode());
    byRoute.put(route, 12127);
    route.add("Surat");                                  // the key changes, and so does its hash code
    IO.println("hash code of the same list after add:   " + route.hashCode());
    IO.println("get(route) = " + byRoute.get(route) + ", size = " + byRoute.size());
    IO.println("get(List.of(\"Pune\", \"Mumbai\")) = " + byRoute.get(List.of("Pune", "Mumbai")));

    Map<String, Integer> byName = new HashMap<>();
    String name = "Pune-Mumbai";
    byName.put(name, 12127);
    String longer = name.concat("-Surat");               // a String never changes: concat makes a new one
    IO.println("get(name) = " + byName.get(name) + ", and the new string is a different key: " + byName.get(longer));
}

Output

hash code of the route when it goes in: -1901649174
hash code of the same list after add:   1258668217
get(route) = null, size = 1
get(List.of("Pune", "Mumbai")) = null
get(name) = 12127, and the new string is a different key: null

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java MutableKey.java

Step through what the map does with the changed key:

Interactive · step by step A key that changes after put Step through MutableKey.java and watch the entry stay filed under the hash code its key had when it went in.
A key that changes after put, step 5 of 5. Showing route ([Pune, Mumbai, Surat]), the entry for 12127, get(route) (null, size 1), get(List.of(...)) (null again). In focus: the entry for 12127.put(routewrong hash codeequals is falseroute[Pune, Mumbai, Surat]the entry for 12127filed under -1,901,649,174get(route)null, size 1get(List.of(...))null again

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. put. put(route, 12127) files the entry under the hash code route has at that moment, -1,901,649,174, the first number the run printed. A List computes its hash code from its elements.
  2. The key changes. route.add("Surat") changes the list inside the map, and with it the hash code: 1,258,668,217. The entry stays filed under the old number; nothing tells the map that its key changed.
  3. get(route). get(route) computes the new hash code and finds no entry filed under it, so it returns null, although size() is still 1.
  4. get with a copy. An equal copy of the old key, List.of("Pune", "Mumbai"), has the old hash code, but the stored key now has three elements, so equals says no: null again. The entry can no longer be found at all.
  5. A String key. A String key cannot change: concat returned a new string, and the original key still found its value, 12127. The Map documentation warns about keys that change while they are in a map.

A List computes its hash code from its elements, so the changed list has a new hash code, and the map looks for it under the wrong one. Neither the changed list nor an equal copy of the original finds the entry, although the map still holds it. The Map documentation warns about this very case: once a key held by a map can stop being equals to what it was, the map promises nothing about how it behaves. A String can never change: concat returned a new string, and the original key still found its value.

Key idea

A hash table finds an entry by the hash code its key had when it went in, so a key must never change: use Strings, records of values that cannot change, or copies.

Checkpoint

Check: Keys that change

  1. Question 1 of 2 set is a HashSet<List<Integer>> that holds list, the list [1, 2]. After list.add(3), what does set.contains(list) return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: false, because the set looks under the list's new hash code, while the element is filed under the old one

    The Map documentation promises nothing about a map whose key stops being equal to what it was while the map holds it. Keep keys immutable.

  2. Question 2 of 2 Which of these is the safest kind of HashMap key?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A String, or a record of Strings and ints, which nothing can change after it goes in

    Immutable keys keep their hash code forever. That is one of the main reasons String is immutable.

toLowerCase() follows the default locale unless you pass Locale.ROOT

toLowerCase() and toUpperCase() without arguments use the default locale: The locale the JVM takes from the computer's settings when it starts, which String.format, printf and formatted use whenever no Locale is passed. The options -Duser.language and -Duser.country set it. More in “Formatted output with printf and formatted”, the language and country the JVM takes from the computer’s settings. Most of the time nobody notices, until the program runs somewhere else:

Lowercasing on a computer set to Turkish, and lengths that change Java · Locales.java
// Case conversion follows the rules of a language, and the default language is whatever the computer is set to.
void main() {
    Map<String, String> settings = Map.of("title", "Results", "id", "42");
    String key = "TITLE";

    Locale.setDefault(Locale.of("tr", "TR"));        // as on a computer set to Turkish
    IO.println("default locale " + Locale.getDefault().toLanguageTag() + ":");
    IO.println("  \"TITLE\".toLowerCase()            = " + key.toLowerCase());
    IO.println("  \"title\".toUpperCase()            = " + "title".toUpperCase());
    IO.println("  settings.get(key.toLowerCase())  = " + settings.get(key.toLowerCase()));
    IO.println("  \"TITLE\".toLowerCase(Locale.ROOT) = " + key.toLowerCase(Locale.ROOT));
    IO.println("  settings.get(with Locale.ROOT)   = " + settings.get(key.toLowerCase(Locale.ROOT)));
    IO.println("  \"TITLE\".equalsIgnoreCase(\"title\") = " + key.equalsIgnoreCase("title"));

    Locale.setDefault(Locale.US);
    IO.println("lengths can change:");
    IO.println("  \"Straße\".toUpperCase(Locale.ROOT)   = " + "Straße".toUpperCase(Locale.ROOT) + " (6 chars became " + "Straße".toUpperCase(Locale.ROOT).length() + ")");
    String istanbul = "\u0130stanbul".toLowerCase(Locale.ROOT);    // İstanbul, with a dotted capital İ
    IO.println("  \"İstanbul\".toLowerCase(Locale.ROOT) = " + istanbul + " (8 chars became " + istanbul.length()
            + ": i + " + String.format("U+%04X", (int) istanbul.charAt(1)) + " COMBINING DOT ABOVE)");
}

Output

default locale tr-TR:
  "TITLE".toLowerCase()            = tıtle
  "title".toUpperCase()            = TİTLE
  settings.get(key.toLowerCase())  = null
  "TITLE".toLowerCase(Locale.ROOT) = title
  settings.get(with Locale.ROOT)   = Results
  "TITLE".equalsIgnoreCase("title") = true
lengths can change:
  "Straße".toUpperCase(Locale.ROOT)   = STRASSE (6 chars became 7)
  "İstanbul".toLowerCase(Locale.ROOT) = i̇stanbul (8 chars became 9: i + U+0307 COMBINING DOT ABOVE)

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Locales.java

Side by side, the two ways of lowercasing the run’s key:

The two ways of lowercasing the run's key on a computer set to Turkish
Criterion toLowerCase()toLowerCase(Locale.ROOT)
Rules it follows The default locale'sNo language's
TITLE becomes tıtle, with a dotless ıtitle
settings.get finds nullResults
Same on every computer NoYes
When to choose Text shown to a personKeys, identifiers, protocol words
  • Turkish has a dotted and a dotless i in both cases, so under the Turkish locale I lowercases to the dotless ı and i uppercases to the dotted İ. "TITLE".toLowerCase() became "tıtle", and the settings lookup returned null: the key title was never found.
  • toLowerCase(Locale.ROOT) applies the rules that belong to no language, so the result is the same on every computer. Locale.ROOT: The locale that belongs to no language or country, so text converted with it is the same on every computer. Pass it for text a program reads, such as keys, identifiers and protocol words. is for anything a program, not a person, reads: map keys, file extensions, HTTP header names, enum names, command words. Use the user’s locale only for text you show to that user.
  • equalsIgnoreCase compares code point by code point without any locale, so it is safe for such checks; for language-aware comparison of names use Collator.
  • Case conversion can change the length: ß uppercases to SS, and the Turkish capital İ lowercases in the root locale to i followed by U+0307, a combining mark: A code point that attaches to the character before it instead of standing alone, such as a vowel sign, the virama or an accent. Its Unicode category is M (Mn, Mc or Me), so Character.isLetter is false for it. More in “Unicode, UTF-16 and Indian scripts” dot. Never apply indexes found in one form of a string to the other.
Case Converter Convert TITLE and title with the Turkish rules and without them, and see where the dotless ı and the dotted İ appear.

Checkpoint

Check: Case conversion and Locale.ROOT

  1. Question 1 of 2 On a computer whose default locale is tr-TR, what does "file".toUpperCase() return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: FİLE, with a dotted capital İ

    A check such as name.toUpperCase().equals("FILE") then fails on that computer. Pass Locale.ROOT for text a program reads.

  2. Question 2 of 2 Which of these should be lowercased with toLowerCase(Locale.ROOT)? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • An HTTP header name before it is looked up
    • An enum name read from a settings file
    • A file extension such as .PNG before it is compared

    Locale.ROOT for anything a program reads, keys, identifiers, protocol words; the user's locale for text a person reads.

Practise: find a repeated substring with a rolling hash

A rolling hash: A hash of a window of text that is updated in constant time as the window slides by one char: take out the char that leaves, multiply by the base, and add the char that arrives. lets you hash every window of k characters in a text in O(n) total, by updating the hash as the window slides instead of recomputing it. RepeatFinder.java asks for the first window whose content appeared at an earlier position, and two of its checks contain the colliding pair Aa and BB. As it starts, the method returns null, which is right for only two of the six checks:

The starting point Java · RepeatFinder.java
// Your turn: find the first substring of length k that occurs twice in a text, with a rolling hash.
//
// Slide a window of k chars along the text. Keep the hash of the window in the form String.hashCode uses,
// h = s[i]*31^(k-1) + ... + s[i+k-1], and update it in O(1) when the window moves one char to the right:
// subtract the char that leaves times 31^(k-1), multiply by 31, add the char that arrives (all in int arithmetic,
// which wraps round exactly as String.hashCode does). Return the window whose content appeared at an earlier
// position, the earliest such window, or null when no window repeats. Equal hashes do not prove equal text:
// "Aa" and "BB" share one. Check a match with regionMatches before you return it.
String repeatedSubstring(String text, int k) {
    return null;    // Write your code here.
}

int failed = 0;
int total = 0;

void check(String text, int k, String expected) {
    total++;
    String got = repeatedSubstring(text, k);
    if (!java.util.Objects.equals(got, expected)) {
        failed++;
        IO.println("FAIL repeatedSubstring(\"" + text + "\", " + k + "): expected " + quoted(expected) + ", got " + quoted(got));
    }
}

String quoted(String s) {
    return s == null ? "null" : "\"" + s + "\"";
}

void main() {
    check("banana", 3, "ana");
    check("abcd", 2, null);
    check("aaaa", 1, "a");
    check("AaBB", 2, null);              // "Aa" and "BB" collide, but nothing repeats
    check("AaBBAa", 2, "Aa");            // the colliding "BB" comes first and must not be returned
    check("मेरा नाम मेरा", 4, "मेरा");
    IO.println(failed + " of " + total + " checks failed");
    if (failed > 0) {
        System.exit(1);
    }
}

Output (exit status 1)

FAIL repeatedSubstring("banana", 3): expected "ana", got null
FAIL repeatedSubstring("aaaa", 1): expected "a", got null
FAIL repeatedSubstring("AaBBAa", 2): expected "Aa", got null
FAIL repeatedSubstring("मेरा नाम मेरा", 4): expected "मेरा", got null
4 of 6 checks failed

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java RepeatFinder.java

You are done when it prints 0 of 6 checks failed. Hints: compute 31^(k-1) once with a loop; keep a Map<Integer, List<Integer>> from each hash to the start positions of earlier windows; when the current hash is in the map, confirm the match with text.regionMatches(earlier, text, start, k) before returning it; then slide the window with h = (h - text.charAt(start) * power) * 31 + text.charAt(start + k). A version that trusts the hash alone returns "BB" for both collision checks.

Six ideas came before this exercise, from the formula to Locale.ROOT. Test each of them below: the practice begins with whichever you are least sure of and gives the reason behind every choice you pick:

Practice that adapts to you

Practise this lesson

15 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 15 'A' is 65 and 'b' is 98. What is "Ab".hashCode()?

    Type a number.

    Show the answer: Question 1 of 15

    Answer: 2113

    The formula gives s[0] × 31 + s[1] for two characters: 65 × 31 + 98 = 2,015 + 98 = 2113. "Aa" is 2112 and "BB" is 66 × 31 + 66 = 2112 too, which is why those two collide.

  2. Question 2 of 15 A hash code is -3. What is Math.floorMod(-3, 8)?

    Type a number.

    Show the answer: Question 2 of 15

    Answer: 5

    floorMod returns a result with the sign of the divisor, between 0 and 7 here: -3 = -1 × 8 + 5. The % operator gives -3, which would be an invalid index for an array of 8 buckets.

  3. Question 3 of 15 What is "".hashCode()?

    Type a number.

    Show the answer: Question 3 of 15

    Answer: 0

    The loop starts at 0 and has no chars to add, so the empty string hashes to 0, as the run printed.

  4. Question 4 of 15 Why can a String's hash code be negative?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: The sum is computed in int arithmetic, which wraps round past 2,147,483,647

    A negative hash code is normal, which is why a bucket index needs Math.floorMod rather than %.

  5. Question 5 of 15 Two different keys have the same hash code. What does a HashMap do with them?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: It keeps both, and tells them apart with equals

    Collisions cost time, because equals has to be called, never correctness.

  6. Question 6 of 15 How many different strings of 3 blocks, each Aa or BB, are there, all with one hash code?

    Type a number.

    Show the answer: Question 6 of 15

    Answer: 8 strings

    2³ = 8: AaAaAa, AaAaBB, AaBBAa, AaBBBB, BBAaAa, BBAaBB, BBBBAa and BBBBBB.

  7. Question 7 of 15 Why did 65,536 String keys with one shared hash code still go into a HashMap quickly?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: String is Comparable, so the crowded bucket could be kept as a tree ordered by compareTo

    HashMap may order the keys of a crowded bucket when they are Comparable (JEP 180, JDK 8), so a lookup takes about log₂ n comparisons. The lesson's record keys had no order, and 8,192 of them took far longer.

  8. Question 8 of 15 Why is String.hashCode unsuitable for checking that a downloaded file has not been changed?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: Its formula is public and collisions are easy to build, so it is not a cryptographic hash

    String.hashCode is made for spreading keys over buckets, not for resisting someone who wants two inputs to match.

  9. Question 9 of 15 String part = "Pu"; (not final). Which comparison is true?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: "Pu" + "ne" == "Pune"

    "Pu" + "ne" is a constant expression, so the compiler joins it and it refers to the interned "Pune". part is not final, so part + "ne" is computed at run time into a new object, and new String always creates a new object. Compare text with equals.

  10. Question 10 of 15 Which strings does the Java Language Specification promise to intern?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: String literals, and more generally the values of constant expressions

    Everything else is interned only when the program calls intern(), which application code rarely needs.

  11. Question 11 of 15 Why could the map in MutableKey.java no longer find route after route.add("Surat")?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: Its hash code changed after it went in, so the map looked for it under a different hash code

    A List's hash code is computed from its elements, so changing the elements changes the hash code.

  12. Question 12 of 15 byName.put(name, 12127), then name.concat("-Surat"). What does byName.get(name) return?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: 12127, because concat returned a new string and name did not change

    Strings make safe keys because no method can change one. The new string, Pune-Mumbai-Surat, is a different key, which the map does not hold.

  13. Question 13 of 15 On a computer set to Turkish, which expression returns "title" for the key "TITLE"?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: "TITLE".toLowerCase(Locale.ROOT)

    Without an argument, and with the default locale, Turkish rules apply and I becomes the dotless ı, giving "tıtle". Locale.ROOT applies rules that belong to no language, so the result is the same everywhere.

  14. Question 14 of 15 Which check gives the same result on every computer, whatever its locale?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: "TITLE".equalsIgnoreCase("title")

    For a language-aware comparison of names that people read, use a Collator instead.

  15. Question 15 of 15 What is "Maß".toUpperCase(Locale.ROOT).length()?

    Type a number.

    Show the answer: Question 15 of 15

    Answer: 4 chars

    ß uppercases to SS, so the 3 chars become MASS, 4 chars, just as the run's Straße became STRASSE. Never apply positions found in one case form of a string to the other.

Interview questions

Fresher · What does intern() do, and why is == unreliable for comparing strings?

== asks whether two references point to one object, not whether two strings hold the same text. Java guarantees that string literals and other constant expressions are interned, so every "Pune" literal in a program refers to one object and == between two of them is true. A string built at run time, by concatenating a variable, reading input or calling new String, is a new object, so == with an equal literal is false. Code that compares with == therefore works in a quick test with literals and fails with real input. intern() returns the pooled object with the same contents, adding this string to the pool if it is not there, so s.intern() == t.intern() is true exactly when s.equals(t). Compare text with equals; intern() is rarely needed in application code.

An interviewer who likes this question usually probes the edges of the constant rule and the cost of interning. Two such probes, each with its reasoning:

Checkpoint

Interview round: two follow-up questions

  1. Question 1 of 2 An interviewer shows this method and asks what joined("Pu") returns.

    Read the code, then choose one answer.

    boolean joined(final String part) {
        return part + "ne" == "Pune";
    }
    Show the answer: Question 1 of 2

    Answer: false

    A final local variable initialised with "Pu", as in Pool.java, is a constant; a final parameter is not. Either way, compare text with equals, and the question never arises.

  2. Question 2 of 2 The interviewer asks whether calling intern() on every string a program reads from a large file is a good way to save memory. Which answer is strongest?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Rarely. Every intern() is a lookup in the pool, and G1 can share duplicate strings itself with -XX:+UseStringDeduplication, without changing the code

    Measure first: if a heap dump shows millions of equal strings, try string deduplication before changing code.

Beyond the warm-up, the module bank has two harder cases from production: a request whose field names all share one hash code, and settings that fall back to their defaults for one customer only.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    An API puts the field names of each incoming JSON object into a HashMap. A client sends objects with 100,000 field names that all share one hash code, and CPU use jumps. Why does it hurt, why less than it would have before JDK 8, and what would you change?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    An application reads its settings with config.get(key.toLowerCase()) and works everywhere except at one customer in Istanbul, where some settings silently fall back to their defaults. Explain the cause, fix it, and say how you would find other places with the same bug.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • String.hashCode is s[0]*31^(n-1) + … + s[n-1] in int arithmetic: it can be negative, and "Aa" and "BB" collide, so blocks of them give 2ᵏ strings with one hash code.
  • Use Math.floorMod(hash, n), not hash % n, to turn a hash code into an index.
  • HashMap stays correct with collisions; Comparable keys such as String let a crowded bucket become a tree, while colliding keys without an order make lookups slow.
  • Literals and constant expressions are interned; strings built at run time are new objects; compare with equals.
  • Immutable keys such as String keep their hash code; a mutable key that changes is lost inside a HashMap.
  • toLowerCase() and toUpperCase() use the default locale: pass Locale.ROOT for keys and identifiers, and expect the length to change in some languages.

Terms in this lesson

bucket
One slot of a hash table's array. A HashMap picks a key's bucket from its hash code, keeps the keys that land in the same bucket together, and searches them with equals.
hash collision
Two different keys with the same hash code, such as Aa and BB, which both hash to 2112. A hash table keeps both and tells them apart with equals; many collisions in one place make its lookups slow.
Locale.ROOT
The locale that belongs to no language or country, so text converted with it is the same on every computer. Pass it for text a program reads, such as keys, identifiers and protocol words.
rolling hash
A hash of a window of text that is updated in constant time as the window slides by one char: take out the char that leaves, multiply by the base, and add the char that arrives.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.