JAVA • OBJECT-ORIENTED PROGRAMMING

Hashing in HashMap/HashSet — Understand how hashing affects HashMap/HashSet behavior (conceptual)

Discover how hash functions turn keys into lightning-fast lookups inside Java's most popular collections.

Historical Context & Motivation

Imagine you have a huge list of student names and you need to check whether "Alice" is on it. You could scan every name one by one, but that gets painfully slow as the list grows. Computer scientists faced this exact problem for decades: how do you find data fast without checking everything? The answer they arrived at is a technique called hashing, and it changed the way we store and retrieve data forever.

Hashing works by converting a piece of data — like a name or a number — into an integer called a hash code. That integer tells the computer exactly where to look in an internal array, skipping the need to search through everything. Java's HashMap and HashSet rely on this idea to give you near-instant lookups, insertions, and deletions.

1953
Hash Tables Invented
Hans Peter Luhn at IBM filed an internal memo describing a chained hash table, establishing the foundational data structure used in nearly every modern language.
1973
Formal Analysis Published
Donald Knuth published a rigorous mathematical analysis of hashing in The Art of Computer Programming, proving average O(1) lookup time.
1998
Java Collections Framework
Java 2 (JDK 1.2) shipped the Collections Framework, introducing HashMap and HashSet as standard library classes available to every Java developer.
2014
Treeification in Java 8
Java 8 improved HashMap by converting long collision chains into balanced red-black trees, boosting worst-case performance from O(n) to O(log n).

The central question this lesson tackles is: How does hashing actually work inside HashMap and HashSet, and what happens when things go wrong? Understanding this will help you write faster, more correct Java programs and avoid subtle bugs.

Core Principles of Hashing

Before diving into code, you need a solid mental model of the key ideas that make hashing possible. Every object in Java inherits a method called hashCode() from the Object class. This method returns an integer that the HashMap uses to decide where to store or find the object. Paired with equals(), these two methods form the backbone of hash-based collections.

1

Hash Function

A function that takes any input (a key) and produces a fixed-size integer — the hash code. Good hash functions spread values evenly across the range of integers.
2

Bucket Array

Internally, a HashMap keeps an array of slots called buckets. The hash code is mapped to a bucket index using modular arithmetic: index = hashCode % array.length.
3

Collision

When two different keys produce the same bucket index, a collision occurs. Java handles collisions by chaining entries in a linked list (or tree) at that bucket.
4

The hashCode/equals Contract

If two objects are equal according to equals(), they must return the same hashCode(). Breaking this rule causes HashMap to lose track of your data.
5

Load Factor & Rehashing

The load factor (default 0.75) is the ratio of entries to buckets. When exceeded, the array doubles in size and all entries are rehashed — a process called rehashing.
✦ KEY TAKEAWAY
Think of a HashMap like a wall of mailboxes in an apartment lobby. Your hash code is your apartment number — it tells the mail carrier exactly which box to open. If two tenants somehow get the same number (a collision), they share a box and must sort through the contents. The better the numbering system, the fewer shared boxes you need.

How Hashing Works — Visual Explanation

The diagram below shows a simplified HashMap with 8 buckets. Three keys — "Alice", "Bob", and "Eve" — are hashed to bucket indices. Notice that "Alice" and "Eve" land in the same bucket, forming a collision chain.

Keys are hashed to integers, then mapped to bucket indices via modular arithmetic. When "Alice" and "Eve" both map to bucket [5], they form a linked chain — a collision.

In the diagram above, the dashed arrows show each key being routed from its hash code to a bucket. Bucket [1] holds only "Bob", so lookup is instant. Bucket [5] holds two entries chained together, so the HashMap must check equals() on each one to find the right value. This is why minimizing collisions is critical to performance.

How the Hash Function Works Under the Hood

When you call map.put(key, value), Java performs a sequence of steps. First, it calls key.hashCode() to get a raw integer. Then it applies an internal "spread" function that mixes the upper and lower bits of that integer. Finally, it uses bitwise AND with (array.length - 1) to get the bucket index. Because the array length is always a power of 2, this is equivalent to modular arithmetic but faster.

BUCKET INDEX FORMULA
index = (n − 1) & hash(key)
Where n is the array length (always a power of 2), & is bitwise AND, and hash(key) is the spread hash code. This is equivalent to hash(key) % n but uses a single CPU instruction.
LOAD FACTOR
α = entries / buckets
The load factor α measures how full the map is. Java's default threshold is 0.75 — when the map is 75% full, it doubles the bucket array and rehashes every entry.
AVERAGE LOOKUP TIME
O(1 + α) → effectively O(1) when α ≤ 0.75
On average, you check one bucket plus a fraction of the chain. With a good hash function and a reasonable load factor, this stays constant regardless of how many entries the map holds.
⚠ Why Override Both hashCode() and equals()?
If you override equals() but forget hashCode(), two "equal" objects may land in different buckets. The HashMap will never find the second one when you search, even though it's logically equal. This is one of the most common bugs in Java.

Collision Handling — Chaining vs. Treeification

Collisions are inevitable. Even the best hash function cannot guarantee that every key maps to a unique bucket when there are more possible keys than buckets. Java's HashMap uses a strategy called separate chaining: each bucket holds a linked list of all entries that hash to that index. When a collision occurs, the new entry is added to the list. During lookup, Java walks the list and calls equals() on each entry until it finds a match.

Starting with Java 8, if a single bucket's chain grows to 8 or more entries, the linked list is automatically converted into a balanced red-black tree. This process is called treeification. A tree search is O(log n) instead of O(n), which prevents malicious or unfortunate hash distributions from crippling your program.

Left: a simple linked list chain at one bucket. Right: the same data reorganized as a balanced red-black tree after crossing the 8-entry threshold. Tree lookup is logarithmic, not linear.
Linked List vs. Tree comparison inside HashMap buckets
AspectLinked List ChainRed-Black Tree
When usedFewer than 8 entries in a bucket8 or more entries in a bucket
Search timeO(n) — must walk every nodeO(log n) — binary search structure
Memory overheadLow — each node has a single "next" pointerHigher — nodes need left/right/parent pointers and a color bit
Insertion costO(1) — append to headO(log n) — must rebalance

Worked Example — Tracing a HashMap Insertion

Let's trace what happens step by step when we insert a key-value pair into a HashMap that already contains some entries.

Inserting "Charlie" → 85 into a HashMap
1
Step 1 — Compute hashCode()Java calls "Charlie".hashCode(), which returns the integer -67207079. Hash codes can be negative — that's perfectly normal for Java strings.
Raw hash code = -67207079
2
Step 2 — Apply the spread functionHashMap internally XORs the hash code with its own upper 16 bits: h ^ (h >>> 16). This mixes the bit patterns to reduce clustering when the bucket array is small. Suppose the spread result is -67206054.
Spread hash = -67206054
3
Step 3 — Compute bucket indexAssume the internal array has 16 buckets (the default initial capacity). The bucket index is calculated as (16 - 1) & (-67206054), which equals 10. The bitwise AND with 15 (which is 0b1111) extracts just the last 4 bits.
Bucket index = 10
4
Step 4 — Check bucket [10]HashMap looks at bucket [10]. If it's empty, the new entry is placed directly there. If another entry already occupies the bucket, HashMap walks the chain and calls equals() on each key. If a match is found, the old value is replaced. If no match, the new entry is appended to the chain.
Bucket [10] was empty → entry placed directly
5
Step 5 — Check load factorAfter insertion, the map now has 13 entries in a 16-bucket array. The load factor is 13 / 16 = 0.8125, which exceeds the default threshold of 0.75. So HashMap triggers a resize: it creates a new 32-bucket array and rehashes all 13 entries.
Load factor = 0.8125 > 0.75 → rehash to 32 buckets

HashMap vs. HashSet — Strengths and Limitations

You'll often see both HashMap and HashSet in Java programs, and it's important to understand their relationship. A HashSet is actually built on top of a HashMap internally — it uses the elements as keys and a dummy constant as the value. This means everything we've learned about hashing, buckets, collisions, and the hashCode/equals contract applies equally to both.

Comparison of HashMap and HashSet features
FeatureHashMap<K,V>HashSet<E>
StoresKey-value pairsUnique elements only (no duplicates)
Underlying structureArray of buckets (hash table)Backed by a HashMap internally
Lookup speedO(1) averageO(1) average
Preserves insertion order?No (use LinkedHashMap if needed)No (use LinkedHashSet if needed)
Null keys allowed?One null key permittedOne null element permitted
Thread-safe?No — use ConcurrentHashMap insteadNo — use Collections.synchronizedSet()
✦ KEY TAKEAWAY
A HashSet is essentially a HashMap where you only care about the keys. When you call set.add("Alice"), behind the scenes it's doing map.put("Alice", PRESENT) where PRESENT is a meaningless placeholder object. This is why the same hashing rules, collision behavior, and performance characteristics apply to both structures.

Connection to Advanced Topics

Once you understand basic hashing, you're ready to explore more advanced ideas. Concepts like consistent hashing (used in distributed databases), perfect hashing (zero-collision hash functions for static data), and cryptographic hash functions (SHA-256, used in security and blockchain) all build on the same foundation. In the table below, you can see how the hashing you learned today connects to these broader topics.

From basic hashing to advanced topics
ConceptWhat You Learned TodayAdvanced Extension
Hash functionhashCode() maps objects to integersCryptographic hash functions produce fixed-length, irreversible outputs (e.g., SHA-256)
Collision resolutionChaining with linked lists or treesOpen addressing (linear probing, double hashing) — stores entries in the array itself
ResizingDouble array size when load factor > 0.75Consistent hashing avoids rehashing everything when adding/removing servers in distributed systems
Equality contracthashCode()/equals() must agreeComparable/Comparator contracts extend this idea for sorted collections like TreeMap

In AP Computer Science A and college-level data structures courses, you'll dive deeper into analyzing the probability of collisions, designing custom hash functions, and implementing hash tables from scratch. The conceptual model you've built today — keys → hash codes → bucket indices → chain resolution — is the mental framework that supports all of these advanced studies.

Practice Problems

PROBLEM 1 — CONCEPTUAL
Explain in your own words why HashMap provides O(1) average lookup time instead of O(n). What role does the hash code play in achieving this?
PROBLEM 2 — BASIC CALCULATION
A HashMap has an internal array of 16 buckets. A key's hash code (after the spread function) is 43. What bucket index will this key be placed in? Show your calculation.
PROBLEM 3 — INTERMEDIATE
Suppose you create a class Student and override equals() so that two students are equal if they have the same name. However, you forget to override hashCode(). You create new Student("Alice") and add it to a HashSet. Then you check set.contains(new Student("Alice")). What is the result and why?
PROBLEM 4 — APPLIED
You are building a spell-checker that stores 50,000 dictionary words. You use a HashSet with default settings (initial capacity 16, load factor 0.75). How many times will the internal array resize before all words are loaded? What is the final array size?
PROBLEM 5 — CRITICAL THINKING
Imagine a poorly written hashCode() method that always returns the same value, such as return 42;. Describe how this affects HashMap's behavior for 1,000 entries. What is the time complexity of lookup, and how does Java 8's treeification mitigate the damage?

Lesson Summary

Java's HashMap and HashSet achieve near-instant lookup by using a hash function to convert keys into hash codes, which are then mapped to bucket indices in an internal array via the formula index = (n − 1) & hash. When two keys map to the same bucket, a collision occurs and entries are chained in a linked list — or a red-black tree if the chain exceeds 8 entries (Java 8+).

The hashCode/equals contract is non-negotiable: equal objects must produce the same hash code. The load factor (default 0.75) triggers rehashing to keep bucket chains short and performance at O(1) average. A HashSet is simply a HashMap where only the keys matter. Understanding these mechanics lets you choose the right collection, write correct custom classes, and diagnose performance issues in your Java programs.

Varsity Tutors • Java • Hashing in HashMap/HashSet