BigDecimal's compareTo() is documented as 'inconsistent with equals'. What does that mean, what general contract does it bend, and how should an API designer reason about it?
answer
- Comparable: ordering consistent with equals is recommended, not required
- compareTo==0 should equal equals() — BigDecimal breaks it
- Standard doc note: 'natural ordering inconsistent with equals'
- TreeSet uses compareTo; HashSet uses equals -> different sizes
- Designer fix: value object / fixed scale / minor units
basics
~20 sJava recommends that a.compareTo(b) == 0 should mean the same as a.equals(b). BigDecimal breaks this on purpose: 2.0 and 2.00 compare as equal (0) but are not equals() (scale differs). It's allowed but must be documented, and it causes surprises in collections that mix the two notions.
solid answer
~50 sThe Comparable contract strongly recommends — but does not strictly require — that natural ordering be consistent with equals, meaning a.compareTo(b) == 0 iff a.equals(b). BigDecimal deliberately violates this: compareTo() ignores scale while equals() includes it, so 2.0 and 2.00 are compareTo-equal but not equals-equal. The Javadoc explicitly calls this out. The contract permits the violation if documented, but it has real consequences: sorted collections (TreeSet/TreeMap) define element identity via compareTo(), while hash collections use equals(), so the same data yields different sizes/dedup behavior across them. As an API designer you reason about it by: (1) deciding whether your type's equality is value-based or representation-based; (2) keeping compareTo consistent with equals when feasible; (3) if you can't, documenting it loudly like BigDecimal does; and (4) protecting callers — e.g. canonicalizing on the way into hash structures, or steering them to compareTo-based comparisons.
code
java · 15 linesBigDecimal a = new BigDecimal("2.0");
BigDecimal b = new BigDecimal("2.00");
// Inconsistent with equals:
assert a.compareTo(b) == 0; // ordering says equal
assert !a.equals(b); // equals says different
// Same data, different collection sizes:
int hashSize = new HashSet<>(List.of(a, b)).size(); // 2 (equals-based)
int treeSize = new TreeSet<>(List.of(a, b)).size(); // 1 (compareTo-based)
// Designer's safe value object idea: pin scale so both agree
BigDecimal ca = a.setScale(2, RoundingMode.HALF_UP);
BigDecimal cb = b.setScale(2, RoundingMode.HALF_UP);
assert ca.equals(cb) && ca.compareTo(cb) == 0; // now consistentgo deeper
Aware that compareTo and equals can disagree for BigDecimal and that the docs mention it.
Can quote the 'consistent with equals' recommendation and give the 2.0/2.00 example of the violation.
Explains the downstream TreeSet-vs-HashSet contract divergence and why the rule is a recommendation, not a requirement.
Designs around it: chooses an equality model, enforces consistency via a value object / fixed scale / minor-units, documents with the standard note, and institutionalizes the rule via review and static analysis.
## The Comparable contract A class implements `Comparable<T>` to define a **natural ordering** via `compareTo`, returning negative / zero / positive. The Javadoc for `Comparable` includes a **strong recommendation** (not a hard requirement): > It is strongly recommended (though not required) that natural orderings be consistent with equals. ... A class's natural ordering is said to be *consistent with equals* if and only if `e1.compareTo(e2) == 0` has the same boolean value as `e1.equals(e2)` for every `e1` and `e2`. It also notes that any class whose ordering is inconsistent with equals **should clearly say so**, with the canonical wording: *"Note: this class has a natural ordering that is inconsistent with equals."* ## How BigDecimal bends it BigDecimal's `equals()` is **representation equality** (unscaled value + scale); its `compareTo()` is **numeric ordering** (value only, scale ignored). So: ``` new BigDecimal("2.0").compareTo(new BigDecimal("2.00")) == 0 // true new BigDecimal("2.0").equals(new BigDecimal("2.00")) // false ``` The two disagree → BigDecimal's ordering is **inconsistent with equals**, and its Javadoc says exactly that. ## Why is this *allowed*? Because the consistency rule is a *recommendation*, not a requirement. The designers chose representation equality (to preserve precision as identity — `2.0` cents vs `2.00` cents may matter) yet a sane numeric ordering (you'd never want 2.0 and 2.00 to sort as different positions). Both choices are individually reasonable; together they're inconsistent, so they documented it. ## Why it bites in practice The JDK's own collections rely on **different** notions of "equal": - **Hash-based** (`HashMap`, `HashSet`): use `hashCode()` + `equals()`. - **Sorted** (`TreeMap`, `TreeSet`): use `compareTo()` (or a supplied `Comparator`) — they ignore `equals()` entirely. So a `SortedSet` is, in effect, defined to be consistent with `compareTo`, while a `Set`'s general contract is defined in terms of `equals`. When the two disagree, a `TreeSet` and `HashSet` built from the same BigDecimals can have **different sizes** — and the `TreeSet` technically violates the `Set` interface's `equals`-based general contract. The Javadoc for `SortedSet`/`SortedMap` explicitly warns about this: they are well-defined but behave oddly when used as plain `Set`/`Map`. ## How an API designer should reason 1. **Pick your equality model deliberately.** Is identity *value-based* (most domains) or *representation/precision-based* (BigDecimal)? Default to value-based unless precision is genuinely part of identity. 2. **Prefer consistency.** If you can make `compareTo == 0` ⇔ `equals`, do it — it removes a whole class of collection bugs. 3. **If you can't, document it loudly** using the standard "inconsistent with equals" note, exactly as BigDecimal does, so callers aren't surprised. 4. **Protect callers structurally.** Provide a canonicalizing factory / value object (e.g. a `Money` type with fixed scale and consistent equals/compareTo), or expose helpers that steer comparisons through `compareTo`. For shared money types, store minor units (a `long` of cents) so equality is trivially consistent. 5. **Code-review / static analysis.** Flag raw `BigDecimal.equals` in business logic and raw BigDecimal hash keys; require `compareTo` for numeric equality. ## Takeaway "Inconsistent with equals" is a precise, documented contract relaxation: `compareTo == 0` and `equals` can disagree. BigDecimal does it because precision-as-identity and numeric ordering are both desirable but mutually inconsistent. The mature response is to wrap the type behind a value object with consistent semantics, or document and steer callers — never to silently expose the inconsistency in domain APIs.
- Does putting BigDecimals in a TreeSet technically violate the Set interface contract?Yes, in spirit. The Set general contract is defined in terms of equals(), but TreeSet uses compareTo(). Because BigDecimal's compareTo is inconsistent with equals, a TreeSet of BigDecimals can dedupe values that equals() considers distinct, so it behaves oddly when viewed as a plain Set. The SortedSet Javadoc warns about exactly this.
- How would you design a Money type to avoid all of this?Store the amount as a long of minor units (cents) plus a currency, or as a BigDecimal pinned to a fixed scale via setScale in the constructor. Define equals/hashCode/compareTo all on the canonical value so they are mutually consistent, and make the type immutable.
Two rulers: equals() measures the engraving (2.0 vs 2.00 cm marks look different), compareTo() measures the actual length (same). A well-designed ruler API would pick one meaning and document it, not hand callers two rulers that disagree.
saying these in an interview costs you the question
- Saying the Comparable consistency rule is a strict requirement — it is only a strong recommendation.
- Claiming BigDecimal's behavior is a bug — it is a deliberate, documented design choice.
- Asserting TreeSet uses equals()/hashCode() — it uses compareTo()/Comparator.
- Believing documenting the inconsistency removes the runtime surprises — it only warns; you still need to protect callers.