What goes wrong when you use BigDecimal as a key in a HashSet or HashMap, and how do you fix it?
answer
- HashMap uses hashCode() then equals() — both include scale
- 2.0 and 2.00 = different buckets, different keys
- get with wrong scale silently returns null
- TreeSet/TreeMap use compareTo() -> ignore scale
- Fix: stripTrailingZeros / setScale before put & get
basics
~20 sHash-based collections find keys using equals() and hashCode(), which both include scale. So new BigDecimal("2.0") and new BigDecimal("2.00") are treated as different keys — a lookup with the 'wrong' scale misses. Fix it by normalizing scale (e.g. stripTrailingZeros) before storing or looking up.
solid answer
~40 sHashSet and HashMap locate entries via hashCode() to pick a bucket, then equals() to confirm a match. BigDecimal's hashCode() and equals() both incorporate scale, so 2.0 and 2.00 hash differently and are unequal. Practical fallout: you put a value in with one scale and a get() / contains() with another scale silently misses, you can store both 2.0 and 2.00 as 'duplicate' entries in a Set, and de-duplication fails. The fixes: normalize the scale on every key before insert and lookup — typically stripTrailingZeros() (watch zero edge cases) or setScale(n, RoundingMode) to a canonical scale; or for a sorted structure use a TreeSet/TreeMap, because those use compareTo() (which ignores scale) instead of equals()/hashCode(). The cleanest rule is: never use raw BigDecimal as a hash key without canonicalizing scale.
code
java · 15 lines// Hash map: scale-sensitive -> surprising miss
Map<BigDecimal, String> hm = new HashMap<>();
hm.put(new BigDecimal("2.0"), "two");
System.out.println(hm.get(new BigDecimal("2.00"))); // null
// Tree map: compareTo-based -> works
Map<BigDecimal, String> tm = new TreeMap<>();
tm.put(new BigDecimal("2.0"), "two");
System.out.println(tm.get(new BigDecimal("2.00"))); // two
// HashSet fails to dedupe; TreeSet succeeds
System.out.println(new HashSet<>(List.of(
new BigDecimal("2.0"), new BigDecimal("2.00"))).size()); // 2
System.out.println(new TreeSet<>(List.of(
new BigDecimal("2.0"), new BigDecimal("2.00"))).size()); // 1go deeper
Recognizes that storing a BigDecimal in a HashMap and looking it up with a different number of decimals can fail to find it.
Explains that both hashCode() and equals() include scale, causing missed lookups and duplicate set entries.
Can choose between canonicalizing scale and switching to TreeMap/TreeSet, and knows the stripTrailingZeros zero caveat and the O(1) vs O(log n) trade-off.
Sets a system-wide convention (canonical scale or value object / minor-units keys), considers serialization/DB round-trips, and bakes the rule into shared money types and code review/static analysis.
## How hash-based collections find keys `HashMap` and `HashSet` work in two steps: 1. Call `key.hashCode()` to choose a **bucket**. 2. Within that bucket, call `equals()` to find the matching entry. Both steps must agree with how you stored the key. If `hashCode()` differs, you look in the wrong bucket and never even reach the `equals()` check. ## Why BigDecimal breaks this BigDecimal's `equals()` and `hashCode()` are both defined on **(unscaled value, scale)** — i.e. they include scale (see the representation question). Therefore: ``` new BigDecimal("2.0").hashCode() != new BigDecimal("2.00").hashCode() new BigDecimal("2.0").equals(new BigDecimal("2.00")) // false ``` These two — though the same number — behave as **completely different keys**. ## Concrete failure modes ```java Map<BigDecimal, String> m = new HashMap<>(); m.put(new BigDecimal("2.0"), "two"); m.get(new BigDecimal("2.00")); // null! different scale -> different key m.containsKey(new BigDecimal("2.00")); // false Set<BigDecimal> s = new HashSet<>(); s.add(new BigDecimal("2.0")); s.add(new BigDecimal("2.00")); s.size(); // 2 — failed to de-duplicate the same number ``` This is insidious because it depends on where the values came from: a value parsed from `"2.00"` and one computed as `2.0` will mismatch even though they're equal numbers. ## Fix 1 — canonicalize the scale (preferred for hash maps) Normalize every key to one canonical scale **both** when inserting and when looking up: ```java BigDecimal canon(BigDecimal v) { return v.stripTrailingZeros(); // or v.setScale(2, RoundingMode.HALF_UP) } m.put(canon(a), x); m.get(canon(b)); ``` - `stripTrailingZeros()` shrinks scale to the minimum. **Caveat:** for zero, older JDKs returned `0E-8` (a non-zero scale) instead of `0` — guard zero, or prefer `setScale`. - `setScale(n, RoundingMode)` forces a fixed scale (e.g. 2 for currency), which is usually the most predictable choice for a domain. ## Fix 2 — use a sorted collection `TreeMap` / `TreeSet` locate entries with `compareTo()` (or a `Comparator`), **not** `equals()`/`hashCode()`. Since `compareTo()` ignores scale, a TreeMap treats 2.0 and 2.00 as the same key: ```java Map<BigDecimal, String> t = new TreeMap<>(); t.put(new BigDecimal("2.0"), "two"); t.get(new BigDecimal("2.00")); // "two" — works ``` Trade-off: O(log n) instead of O(1), and ordering semantics. But it sidesteps the scale problem entirely. (This is the well-known case where a TreeSet and a HashSet of the same BigDecimals can report different sizes.) ## Fix 3 — wrap or use a different key type Wrap the BigDecimal in a key object whose own `equals`/`hashCode` normalizes scale, or key by a normalized `String`/long-of-minor-units instead. ## The rule Never use raw BigDecimal as a hash key unless you control its scale. Either canonicalize scale on every put/get, or use a `compareTo`-based collection (TreeMap/TreeSet).
- Why does a TreeSet of BigDecimals dedupe 2.0 and 2.00 but a HashSet does not?TreeSet orders and identifies elements via compareTo(), which ignores scale, so 2.0 and 2.00 compare as equal and only one is kept. HashSet uses equals()/hashCode(), which include scale, so it keeps both.
- What is the catch with using stripTrailingZeros() to canonicalize keys?On zero, some JDK versions returned 0E-8 rather than BigDecimal.ZERO, so two zeros could still differ in scale. Guard the zero case or prefer setScale to a fixed canonical scale.
It's like filing documents by the exact spelling on the label. 'Color' and 'Colour' mean the same thing, but the filing clerk (hashCode) sends them to different drawers, so you can't find one by searching for the other.
saying these in an interview costs you the question
- Assuming HashMap.get with a different scale will still find the key — it returns null.
- Believing a HashSet de-duplicates numerically equal BigDecimals — it dedupes by equals(), which includes scale.
- Claiming TreeMap also uses hashCode() — it uses compareTo()/Comparator.
- Thinking stripTrailingZeros() always yields a canonical zero (older JDKs return 0E-8).