Urgent.News

What's breaking now, across thousands of outlets.

Tech

The hashCode()-to-Array-Index Bug Almost Everyone Writes Once

"hash % capacity" looks trivial until you trace it by hand. Two separate bugs hide in that one line — and the fix for one of them has its own hole. Building a HashMap from scratch, the "turn a key into a bucket index" line looks like it should be the easy part: int bucket = key . hashCode () % capacity ; I traced this by hand before writing a single line of MyHashMap , and it broke immediately —…

The bug that often trips developers when turning a hash code into an array index looks simple at first: simply applying the modulo operator. However, this innocuous line of code hides two distinct bugs. The fix for one of those bugs, in turn, introduces its own issue. When constructing a HashMap from scratch, the line that maps a key to a bucket index appears straightforward: int bucket = key.hashCode() % capacity; Upon manual tracing of this calculation, a critical error becomes apparent.

The modulo operator (%) returns a signed 32-bit integer result, which can be negative if the dividend is negative. Consider the result of -7 % 16: it is -7, not 9. Feeding this negative index into array[bucket] leads to an ArrayIndexOutOfBoundsException instead of a legitimate but harmless location. The instinct to correct this by wrapping the result in Math.abs() seems promising at first glance.

After all, Math.abs(Integer.MIN_VALUE) returns Integer.MIN_VALUE itself, still negative, which is the intended behavior. However, this approach fails for one specific case: Integer.MIN_VALUE (-2147483648) has no positive counterpart in 32-bit two's complement arithmetic. As a result, Math.abs(Integer.MIN_VALUE) remains negative, thereby failing to guarantee a non-negative result for every possible integer input.

The correct remedy lies in employing Math.floorMod(hash, capacity), a method introduced in Java 8. This function performs a true floored modulo, ensuring non-negative results for positive divisors without any edge cases. Bug two reveals a design choice rather than a flaw in the algorithm itself. Instead of using the modulo operator, java.util.HashMap utilizes the bitwise AND operation (hash & (capacity - 1)).

This bitmask approach only functions correctly when the capacity is a power of two (e.g., 16 - 1 = 15 = 0b01111, a clean sequence of 1-bits). For non-power-of-two capacities, this bitmask operation deviates significantly from true modulo behavior. MyHashMap deviates from this approach by opting for Math.floorMod instead of a bitmask, sacrificing the performance advantage for the flexibility of not enforcing the capacity to be a power of two.

It's essential to be transparent about the chosen method and its underlying rationale, rather than blindly adhering to the JDK's default behavior. Bug three introduces a separate issue that arises even after addressing the previous two bugs. Bitwise operations primarily utilize the lower bits of the hash code. If a class's hashCode() method generates a value with significant variation in its upper bits—a common scenario for certain hash codes such as those derived from floats or sequential object identity—the limited number of buckets may still experience collisions.

This phenomenon occurs despite the hashes being distinct. To mitigate this, java.util.HashMap applies an additional transformation to the hash code by XOR-ing it with shifted bits (h ^ (h << 16)). While this simple operation—consisting of a single shift and one XOR—helps mix the high bits into the lower bits, it's more computationally intensive than the JDK's straightforward approach.

MyHashMap takes this a step further by employing a full FNV-1a hash function over all four bytes of the hashCode, ensuring a more robust mixing of bits. However, this increased complexity comes at the cost of additional computational effort. The decision between these approaches depends on the confidence in the hashCode() distribution of the keys and the frequency of the code path executions.

The JDK's chosen method strikes a balance between simplicity and adequacy for general-purpose use. In conclusion, a seemingly straightforward line of code to map a hash code to an array index can conceal three distinct bugs—each stemming from different aspects of hashing and modulo operations. Understanding these intricacies is crucial for developers to avoid subtle yet pervasive errors in their implementations.

Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.

Read the original at dev.to →

More in Tech

When SQL Has Nothing to Say: Understanding NULLs

NULL in SQL carry different meanings; missing value unknown value no value inapplicable data customer_id customer_name contact 101 Alice 0712345678 102 Brian NULL 103 Carol 0798765432 You shouldn't…

  • NULL in SQL signifies missing, unknown or not applicable values
  • SQL uses three-valued logic with TRUE, FALSE and UNKNOWN
  • IS NULL and IS NOT NULL functions retrieve rows with or without NULL values

Devbox: Portable and Isolated Development Environments

Introduction I am one of those people who need to have everything in its place to be able to do something. This applies, of course, when I work with a local or remote repository.

  • Devbox simplifies isolated development environments
  • Devbox generates project-specific repositories
  • Devbox offers user-friendly package management

Review Agent PRs for Silent Interface Drift

A checkout squad opened an agent pull request on Monday. The title claimed a small null-safety cleanup. The diff looked tiny in GitHub. The helper loadInvoice stopped throwing NotFoundError .

  • Agent PR changes return shape from NotFoundError to null
  • Staging converts missing invoices to HTTP 500 errors
  • Reviewers must manually inventory call sites to detect contract drift

More from Sunday 6 September →