Ints and floats are pretty different beasts in Java. Ints are encoded as two's complement, which has a single 0 value. Floats use IEEE 754 (the 32-bit variant for floats, and 64-bit for doubles). IEEE 754 is somewhat complex, but for purpose of this answer, you just need to know that it has three sections, the first of which is a sign bit. That means for any float, there's a positive and negative variant¹. That includes 0, so floats actually have two "zero" values, +0 and -0.
As an aside, the two's complement that ints use is not the only way to encode integers in computer science. There are other methods, like ones' complement, but they have quirks — like having both a +0 and -0 as distinct values. ;-)
When you compare float primitives (and doubles), Java treats +0 and -0 as equal. But when you box them, Java treats them separately, as described in Float#equals
. This lets the equals method be consistent with their hashCode
implementation (as well as compareTo
), which just uses the bits of the float (including that signed value) and shoves them as-is into an int.
They could have picked some other option for equals/hashCode/compareTo, but they didn't. I'm not sure what the design considerations there were. But in at least one regard, Float#equals
was always going to diverge from the float primitive's ==
: In primitives, NaN != NaN
, but for all objects, o.equals(o)
must also be true. That means that if you had Float f = Float.NaN
, then f.equals(f)
even though f.floatValue() != f.floatValue()
.
¹ NaN (not-a-number) values have a sign bit, but it doesn't have any meaning other than for ordering, and Java ignores it (even for ordering).