skip to content

Across NaN, infinities, and negative zero, why do Java's primitive == / < / > disagree with Double.compare and Double.equals, and what rule should you follow when implementing equals/hashCode or sorting doubles?

level: seniorimportance: should knowfreq 45%

answer

  1. Two worlds: primitive ==/</> (IEEE) vs Double.compare/equals (total order)
  2. IEEE: NaN unordered (all false), -0.0 == 0.0
  3. Total order: NaN == itself & sorts last, -0.0 < +0.0
  4. equals/hashCode/sort/keys → Double.compare / Double.hashCode
  5. == only for numeric tests, and handle NaN explicitly

basics

~20 s

Primitive ==/</> follow IEEE 754: NaN is never equal (even to itself) and -0.0 equals 0.0. Double.compare/Double.equals instead define a clean total order: NaN equals itself and sorts last, and -0.0 sorts below +0.0. For equals/hashCode and sorting, use Double.compare.

solid answer

~50 s

There are two comparison 'worlds' for doubles. The primitive operators ==, <, > implement raw IEEE 754 semantics: NaN is unordered so every comparison with it (including NaN == NaN) is false, while -0.0 == 0.0 is true. These semantics break the Comparable/equals contracts: a sort can't tolerate NaN comparing false to everything, and equals must be reflexive (NaN == NaN being false would violate that). So Double.compare and Double.equals define a consistent total ordering instead: NaN is treated as equal to itself and greater than every other value, and -0.0 is treated as strictly less than +0.0. The rule: when implementing equals/hashCode for value objects, sorting, or using doubles as map keys, use Double.compare(a, b) == 0 (or Double.hashCode) — never the primitive ==. Use the primitive operators only when you genuinely want numeric/value semantics and you've handled NaN explicitly. This single rule subsumes the NaN, infinity, and negative-zero edge cases.

code

java · 14 lines
java
double[] a = { 1.0, Double.NaN, -0.0, 0.0, -1.0 };
java.util.Arrays.sort(a);
System.out.println(java.util.Arrays.toString(a));
// [-1.0, -0.0, 0.0, 1.0, NaN]  -> total order: -0.0 before +0.0, NaN last

// equals/hashCode for a value type holding a double:
record Measure(double value) {
    @Override public boolean equals(Object o) {
        return o instanceof Measure m && Double.compare(value, m.value) == 0;
    }
    @Override public int hashCode() { return Double.hashCode(value); }
}
System.out.println(new Measure(Double.NaN).equals(new Measure(Double.NaN))); // true
System.out.println(new Measure(-0.0).equals(new Measure(0.0)));              // false

go deeper

for a junior

Aware that comparing doubles with == is risky and that helper methods exist.

for a middle

Knows NaN and -0.0 behave specially with == and that Double.compare differs, even if not the full rationale.

for a senior

Articulates the two-worlds model and applies the rule: Double.compare/Double.hashCode for equals/sorting/keys, == only for numeric tests.

for a principal

Sets the team convention, explains the equals/Comparable contract rationale, and audits code (and static-analysis rules) for unsafe == on floating types.

## The two comparison worlds Java gives you **two inconsistent ways** to compare doubles, and knowing which to use is the unifying lesson of this whole topic. ### World 1 — primitive operators (`==`, `<`, `>`, `<=`, `>=`) These implement **raw IEEE 754** semantics: - **NaN is unordered**: every comparison involving NaN is `false`, including `NaN == NaN`. Only `!=` is `true`. - **`-0.0 == 0.0` is `true`** (the two zeros compare equal numerically). - Infinities are ordered normally (`+Inf` greatest, `-Inf` least). These match the math/hardware but **violate Java's object contracts**: - `equals` must be **reflexive** (`x.equals(x)` true). If a value object used `==` on a NaN field, an object would not equal itself. - `Comparable`/`Comparator` require a **total order** (consistent, antisymmetric, transitive). With raw NaN, `sort` can corrupt arrays or loop, because NaN is neither `<`, `>`, nor `==` anything. ### World 2 — library total ordering (`Double.compare`, `Double.equals`, `Double.hashCode`, boxed `compareTo`) To satisfy those contracts, the library defines a **total order** that differs from `==` in exactly the special cases: - **NaN equals itself** and is **greater than every other value** (sorts last). `Double.compare(NaN, NaN) == 0`; `Double.valueOf(NaN).equals(NaN)` is `true`. - **`-0.0` is strictly less than `+0.0`**. `Double.compare(-0.0, 0.0) == -1`; the two zeros are **unequal** under `Double.equals`. - Infinities order the same as in world 1. ## Why the divergence is deliberate The designers couldn't make `==` follow the total order (that would violate IEEE 754 and break numeric code), nor make sorting/equals follow raw IEEE (that would break collections). So Java keeps **both**, and you choose per use case. ## The single rule to remember > For **equals/hashCode, sorting, comparators, and map keys**, use the **total-ordering** path: `Double.compare(a, b) == 0`, `Double.hashCode`, or `Double.valueOf(a).equals(b)`. For pure **numeric value tests**, use the primitive operators — but then handle NaN explicitly (e.g. `Double.isNaN`). This one rule automatically handles all three special-value cases: - It makes a NaN field equal to itself (reflexivity holds). - It treats `-0.0` and `+0.0` as different keys/elements consistently. - It sorts NaN to the end deterministically. ## Concrete consequences ```java // Value object: use Double.compare in equals @Override public boolean equals(Object o) { if (!(o instanceof Measure m)) return false; return Double.compare(value, m.value) == 0; // NOT value == m.value } @Override public int hashCode() { return Double.hashCode(value); } ``` ```java double[] a = { 1.0, Double.NaN, -0.0, 0.0, -1.0 }; Arrays.sort(a); // [-1.0, -0.0, 0.0, 1.0, NaN] — total order: -0.0 before +0.0, NaN last ``` ```java List<Double> list = ...; list.contains(Double.NaN); // works, because List uses Double.equals (true for NaN) // but a primitive scan `for (double d : arr) if (d == Double.NaN)` NEVER matches ``` ## Catch points - IDEs/static analysis (e.g. SonarLint) flag `==` on floating types for exactly this reason. - `Objects.equals` on boxed `Double`s uses world 2; manual `==` uses world 1 — don't mix. - Mixing the two in one class (e.g. `equals` via `Double.compare` but a `==` guard elsewhere) yields contradictory behavior.

  • What would go wrong if a value object's equals used value == other.value with a NaN field?
    equals would not be reflexive: an object holding NaN would not equal itself, breaking the equals contract and causing it to misbehave in collections (e.g. it could never be found in a HashSet that contains it).
  • Where does Arrays.sort place NaN and -0.0?
    It uses Double.compare's total order: -0.0 is placed before +0.0, and NaN is sorted to the very end as the largest value.

saying these in an interview costs you the question

  • Using == on double fields in equals() (breaks reflexivity for NaN)
  • Assuming Double.compare and == agree (they differ on NaN and -0.0)
  • Scanning an array with d == Double.NaN expecting a match
  • Mixing primitive == and Double.compare semantics in the same class

context