انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-041-L01

دوال التجزئة (Hash functions)

  • الدرس 1 من 5
  • 11 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

Lesson Overview

Imagine an office with a wall of numbered mailboxes. When a letter arrives for "Ana", the clerk does not walk past every box looking for her name. A rule turns the name into a box number, and the letter goes straight into that box. Later, the same rule gives the same number, so the letter is found at once.

That rule is a hash function. This lesson is about the rule itself: what it takes in, what it gives back, and what separates a good rule from a bad one. The next lessons use it to build the mailbox wall (a hash table), handle two names sharing a box, and look inside Java's HashMap.

Why this matters: HashMap, HashSet, caches, and the way large systems split data across servers all start with a hash function. If you understand it, the "why" behind the equals/hashCode rules you met earlier becomes clear.

This lesson builds on the equals/hashCode contract (module BEG-026), on String and int arithmetic, and on the idea of growth rates from the Big O module (BEG-039). Collections appear only as a preview here; the next lessons explain them.

Learning Objectives

By the end of this lesson you will be able to:

  • Define a hash function and a hash code.
  • State the two rules every hash function must follow, and the one quality we want on top.
  • Compute a polynomial hash such as String.hashCode() by hand for a short string.
  • Explain what a collision is and why collisions cannot be avoided completely.
  • Tell a poorly spreading hash function from a well spreading one.
  • Explain why hash codes can be negative and why that matters for the next lesson.

Written Explanation

What a hash function does

A hash function takes a key (a name, a number, an object) and returns an integer called the hash code. In Java the function is hashCode(), which returns an int: a number between about minus two billion and plus two billion.

java
"Ana".hashCode();   // 65972, and it is the same every time

The hash code is not a secret code and not an encryption. It is only a number computed from the key, cheap to calculate, and used to decide where the key belongs.

The rules

Every usable hash function follows two rules, and a good one has a third quality.

  1. Deterministic. The same key gives the same number every time. If "Ana" gave 5 today and 9 a moment later, you could never find her letter again. (The Java documentation limits this slightly: the number must stay the same within one run of a program, as long as the data used by equals does not change. It need not be the same on the next run.)
  2. Consistent with equals. If a.equals(b) is true, then a.hashCode() == b.hashCode() must be true. You met this in the equals/hashCode lesson. Two equal keys must land in the same mailbox, or the clerk would look in the wrong box.
  3. Well spread (a quality, not a rule). Different keys should usually give different numbers, scattered widely, so they do not pile up.

The reverse of rule 2 is not a rule. Different keys may share a hash code. The Java documentation says this is allowed, while noting that distinct results for unequal objects can improve hash table performance. Two different keys with the same hash code is a collision.

Why collisions cannot be avoided

An int has about 4.3 billion possible values, but there are far more possible strings than that. By the pigeonhole idea (more pigeons than holes), some different keys must share a number. So a collision is not a bug in a hash function; it is a fact of life. What we want is for collisions to be rare and scattered, not common.

A hash function you can compute by hand

String.hashCode() is a polynomial hash. The Java documentation gives the formula

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

using int arithmetic, where s[i] is the character at position i (as a number) and n is the length. The empty string gives 0. The same result comes from a simple loop: start with h = 0, and for each character do h = 31 * h + character.

For "Hi": H is 72 and i is 105, so 72 * 31 + 105 = 2337. For "Ana": A is 65, n is 110, a is 97:

  • after A: 65
  • after n: 65 * 31 + 110 = 2125
  • after a: 2125 * 31 + 97 = 65972

That matches what Java prints. Two things to notice: each character changes the result, and the order matters, because earlier characters get multiplied again and again.

A bad hash function, and why

Suppose we add the characters instead of multiplying:

java
static int sumHash(String s) {
    int h = 0;
    for (int i = 0; i < s.length(); i++) h += s.charAt(i);
    return h;
}

This is deterministic, and equal strings give equal numbers, so it obeys both rules. But it ignores order: "listen" and "silent" use the same letters, so they get the same number. Worse, all words of similar length produce numbers in a narrow range. Many keys will collide. The function is valid but poorly spread, and in the next lessons you will see that a poor spread costs speed.

Multiplying by 31 fixes the order problem, because a letter in position 1 now counts differently from the same letter in position 5. The number 31 is an odd prime commonly used in this style of hash. Its choice is a convention more than a law, so do not worry about why not 30 or 33.

Hash codes can be negative and can overflow

The sum 31 * h + character is computed in int arithmetic, and int overflows silently, wrapping around instead of failing. So hash codes of longer strings are often negative, such as -2048738041. This is fine for a hash code. But a negative number cannot be an array index, so the next lesson must turn the hash code into a valid index. Do not use Math.abs for that: according to the Java documentation, Math.abs(Integer.MIN_VALUE) returns Integer.MIN_VALUE, which is still negative.

Hashing your own classes

For your own classes, you combine the fields that equals compares. Objects.hash(...) does this for you:

java
@Override public int hashCode() { return Objects.hash(suit, rank); }

Use exactly the fields used by equals, no more and no fewer. A record writes this pair for you, as you saw in the record preview.

Examples

Example 1: hash codes are repeatable (simple)

java
System.out.println("Ana".hashCode() + " " + "Ana".hashCode());
System.out.println("Omar".hashCode() + " " + "Sara".hashCode());
System.out.println("".hashCode());
System.out.println(Integer.valueOf(42).hashCode());

Real output:

text
65972 65972
2461359 2569501
0
42

Same key, same number. Different keys, different numbers here. The empty string is 0, as documented. For an Integer, the documentation says the hash code is simply its value, so 42 hashes to 42.

Example 2: the order-blind hash against the polynomial hash (practical)

Compare sumHash with a loop that multiplies by 31:

java
static int polyHash(String s) {
    int h = 0;
    for (int i = 0; i < s.length(); i++) h = 31 * h + s.charAt(i);
    return h;
}
System.out.println(sumHash("listen") + " " + sumHash("silent"));
System.out.println(polyHash("listen") + " " + polyHash("silent"));
System.out.println(polyHash("Ana") == "Ana".hashCode());

Real output:

text
655 655
-1102508601 -902327211
true

The sum hash cannot tell the anagrams apart. The polynomial hash can, and it reproduces String.hashCode() exactly (and shows negative numbers).

Example 3: collisions exist even in a good hash (realistic)

java
System.out.println("Aa".hashCode() + " " + "BB".hashCode());

Real output:

text
2112 2112

Check by hand: "Aa" is 65 * 31 + 97 = 2112; "BB" is 66 * 31 + 66 = 2112. Even Java's own String hash has collisions among short strings, which is allowed because "Aa" and "BB" are not equals. So any hash table must still behave correctly when two keys share a hash code. That is the topic of lesson 3.

Backend Insight

Hashing is everywhere in backend work. A cache decides where to store a key by hashing it. A system that splits users across several servers often picks the server from a hash of the user id, so the same user always reaches the same server (later modules on caching and partitioning build on this). A hash function that spreads poorly sends most traffic to a few servers.

Two practical habits follow. First, base hashCode on the same stable fields as equals. Second, never use a hash code as a unique id: collisions are normal, and a hash code can differ between program runs, so do not save it in a database or file and expect it to be stable.

Common Mistakes

Mistake 1: Thinking different hash codes are required for different keys

  • Incorrect: "These keys have the same hash code, so they must be equal."
  • Correct: "Equal keys must have equal hash codes. Equal hash codes only suggest the keys might be equal, and equals decides."

Mistake 2: Overriding equals but not hashCode

The default hashCode is based on object identity, so two objects that are equals can get different numbers. This breaks rule 2. The equals/hashCode module showed the symptom; the next lessons explain why it happens.

Mistake 3: A hash code that changes

  • Incorrect: return System.nanoTime(); or a random number.
  • Correct: compute it from the same fields every time.

This breaks the deterministic rule. A key hashed to one box and then to another is lost.

Mistake 4: Using Math.abs(hash) to get a non-negative number

It fails for Integer.MIN_VALUE, and it is also a poor way to pick an index. The next lesson shows the right way.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. State in one sentence each: the two rules for a hash function and the one quality we want.

أظهر الحل A1

A1. Rules: (1) deterministic, the same key always gives the same number; (2) equal keys give equal hash codes. Quality: different keys should be spread widely so they rarely collide.

A2. Compute "Hi".hashCode() by hand. (H is 72 and i is 105.)

أظهر الحل A2

A2. 72 * 31 + 105 = 2232 + 105 = 2337.

A3. True or false: (a) if two keys have the same hash code, they are equal; (b) if two keys are equal, they have the same hash code.

أظهر الحل A3

A3. (a) False: collisions exist, for example "Aa" and "BB". (b) True: this is rule 2.

Level B: Application

B1. A class Card has fields String suit and int rank, and equals compares both. Write a matching hashCode using Objects.hash.

أظهر الحل B1

B1.

java
@Override public int hashCode() { return Objects.hash(suit, rank); }

Compiled and run, two cards with suit "hearts" and rank 12 give the same value. Both fields appear because equals compares both.

B2. A developer writes hashCode() as return (int) System.nanoTime();. Which rule does it break, and what goes wrong for a HashSet?

أظهر الحل B2

B2. It breaks the deterministic rule (and so rule 2 as well, because two equal objects get different numbers). A HashSet would put the same object in different places on different calls, so contains would usually say false for an object you just added.

B3. Using the sum-of-characters idea, hash "ab" and "ba". Then compute both with the multiply-by-31 loop. (a is 97, b is 98.)

أظهر الحل B3

B3. Sum: 97 + 98 = 195 for both, a collision. Multiply-by-31: "ab" is 97 * 31 + 98 = 3105; "ba" is 98 * 31 + 97 = 3135. Different, because order matters.

Level C: Challenge

C1. Show by hand that "Aa" and "BB" collide under String.hashCode(). Then explain in two sentences why a hash table that assumed "different keys never share a hash code" would be incorrect.

أظهر الحل C1

C1. "Aa": 65 * 31 + 97 = 2112. "BB": 66 * 31 + 66 = 2112. Because there are far more possible keys than possible int values, collisions must exist. A table that assumed none would treat two different keys as one, or lose one of them. A correct table checks with equals after finding the box.

C2. A Date-like class has year, month, day, and a developer writes hashCode as year + month + day. Find two different dates with the same hash and say what to use instead. Run it if you can.

أظهر الحل C2

C2. 2025-03-14 and 2025-14-03 both give 2042 (so does 2024-04-14). The sum ignores which number is the year, month, or day, and its results are packed in a narrow range. Use Objects.hash(year, month, day), which gave 1975923, 1976253, and 1974993 for those three dates when run.

Mini Challenge

Write a small class Name holding a String first and a String last, with equals and hashCode that treat two names as equal when both parts are equal ignoring case. Then print the hash codes of new Name("Ana", "Hassan") and new Name("ANA", "HASSAN") and check that they match.

Check your work: the two hash codes are equal, you used the same normalised fields (for example lower-cased) in both equals and hashCode, and a differently spelled name gives a different number.

Summary & Next Lesson

Key takeaways

  • A hash function turns a key into an integer, the hash code. In Java it is hashCode().
  • It must be deterministic and consistent with equals: equal keys give equal hash codes.
  • A good function also spreads keys, because collisions slow hash tables down.
  • A collision (different keys, same hash code) is allowed and unavoidable.
  • String.hashCode() is a polynomial hash with multiplier 31; hash codes may be negative because int arithmetic wraps around.

Next lesson

Next, "Hash tables and buckets" uses the hash code to pick a box in an array, so a key can be found without scanning everything. You will also meet the right way to turn a hash code into a valid index.

Self-check

  1. What are the two rules every hash function must follow?
  2. If a.equals(b) is true, what must be true of their hash codes?
  3. Can two different keys share a hash code? Is that a bug?
  4. Why is "add up the characters" a poor hash for strings?
  5. Why can a hash code be negative, and why can it not be used directly as an array index?
أظهر الإجابات
  1. It must be deterministic (same key, same number), and equal keys must give equal hash codes.
  2. They must be equal.
  3. Yes. It is called a collision, it is allowed, and it cannot be avoided entirely; the table must handle it.
  4. It ignores the order of the characters and gives numbers in a narrow range, so many keys collide.
  5. int arithmetic wraps around on overflow, so large results can be negative. An array index must be zero or higher, so the hash code must first be converted.

References

  • Oracle, Object (Java SE 21). It states the hashCode contract: consistent within a run, equal objects must give equal hash codes, and unequal objects may share a hash code, though distinct values can improve hash table performance.
  • Oracle, String (Java SE 21). It gives the hashCode formula with the multiplier 31 in int arithmetic, and says the empty string hashes to zero.
  • Oracle, Integer (Java SE 21). It says an Integer's hash code equals its int value.
  • Oracle, Math (Java SE 21). It says Math.abs(Integer.MIN_VALUE) returns Integer.MIN_VALUE (still negative).
  • The mailbox picture, the "why 31" remark, and the backend uses (caching, choosing a server) are this course's own teaching additions and were not checked against a single source. The sumHash, polyHash, and Name examples were compiled and run on JDK 21.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.