Why is Java's double/float Math.min() implemented this way?
I was looking through some things in the source of java.lang.Math
, and I noticed that while Math.min(int, int)
(or its long counterpart) is implemented this way:
public static int min(int a, int b) {
return a <= b ? a : b;
}
And this makes complete sense to me and it is the same as what I would do. However, the double/float implementation is this:
public static float min(float a, float b) {
if (a != a) {
return a;
} else if (a == 0.0F && b == 0.0F && (long)Float.floatToRawIntBits(b) == negativeZeroFloatBits) {
return b;
} else {
return a <= b ? a : b;
}
}
I'm completely dumbfounded. Comparing a
to itself? What's the second check even for? Why isn't it implemented in the same way as the int/long version?
Solution 1:
Floating-point numbers are way more complicated than integer values.
For this specific case two distinctions are important:
-
NaN
is a valid value forfloat
anddouble
which represents "not a number" and behaves weirdly. Namely, it doesn't compare equal to itself. - Floating point numbers can differentiate between 0.0 and -0.0. A negative zero could conceivably be useful when you're calculating the limit of some function. Distinguishing whether a limit approaches 0 from the positive or the negative direction could be beneficial.
So this part:
if (a != a) {
return a;
}
ensures that NaN
is returned if a
is NaN
(if a
is not NaN
, but b
is, then the "normal" check later on will return b
, i.e. NaN
, so no explicit check is needed for this case). This is a common pattern: when calculating anything where one input is NaN
, the output will also be NaN
. Since NaN
usually represents some error in the calculation (such as dividing 0 by 0), it's important that it "poisons" all further calculations to ensure the error isn't silently swallowed.
This part:
if (a == 0.0F && b == 0.0F && (long)Float.floatToRawIntBits(b) == negativeZeroFloatBits) {
return b;
}
ensures that if you compare two zero-valued floating point numbers and b
is negative zero then that negative zero is returned (since -0.0 is "smaller" than 0.0). Similarly to NaN
the normal check will correctly return a
if it's -0.0 and b
is 0.0.
Solution 2:
I recommend carefully reading the documentation for Math.min
and also the numeric comparison operators on floating points. Their behaviours are quite different.
Relevant parts from Math.min
:
If either value is NaN, then the result is NaN. Unlike the numerical comparison operators, this method considers negative zero to be strictly smaller than positive zero.
and from JLS §15.20.1 "Numerical Comparison Operators <, <=, >, and >="
The result of a floating-point comparison, as determined by the specification of the IEEE 754 standard, is:
If either operand is NaN, then the result is false.
Positive zero and negative zero are considered equal.
If any argument is NaN, Math.min
picks that one, but if any operand is NaN, <=
evaluates to false
. This is why it has to check if a
not equal to itself - this would mean a
is NaN. If a
is not NaN but b
is, the last case would cover it.
Math.min
also considers -0.0
to be "less than" +0.0
, but the numeric comparison operators think they are equal. This is the purpose of the second check.
Solution 3:
Just for completeness/clarity, let's draw up a table of all possible outcomes:
-
Either of
a
andb
can be either- NaN,
- −0,
- 0 (i.e. +0), or
- some other non-NaN non-zero value, marked as "(other)".
Writing out all combinations of these for completeness, and distinguishing between positive and negative numbers for clarity in some cases, gives the 20 rows in the table below, though most of them are straightforward and unproblematic.
-
The column titled "Correct min" is the correct value that is supposed to be returned according to the IEEE 754 standard and Java documentation of
Math.min
, and the column titled "Naive min" is the value that would have been returned ifMath.min
had been implemented asreturn a <= b ? a : b;
instead.
a | b | Correct min | Naive min | Notes on naive min | Naive min wrong? |
---|---|---|---|---|---|
NaN | NaN | NaN | NaN | b, as NaN comparison gives false. | |
NaN | −0 | NaN | −0 | b, as NaN comparison gives false. | Wrong |
NaN | 0 | NaN | 0 | b, as NaN comparison gives false. | Wrong |
NaN | (other) | NaN | (other) | b, as NaN comparison gives false. | Wrong |
−0 | NaN | NaN | NaN | b, as NaN comparison gives false. | |
−0 | −0 | −0 | −0 | a, as −0 ≤ −0. | |
−0 | 0 | −0 | −0 | a, as −0 ≤ 0. | |
−0 | (other>0) | −0 | −0 | ||
−0 | (other<0) | (other<0) | (other<0) | ||
0 | NaN | NaN | NaN | b, as NaN comparison gives false. | |
0 | −0 | −0 | 0 | a, as "0 ≤ −0" per IEEE 754. | Wrong |
0 | 0 | 0 | 0 | a, as 0 ≤ 0. | |
0 | (other>0) | 0 | 0 | ||
0 | (other<0) | (other<0) | (other<0) | ||
(other) | NaN | NaN | NaN | b, as NaN comparison gives false. | |
(other<0) | −0 | (other<0) | (other<0) | ||
(other>0) | −0 | −0 | −0 | ||
(other<0) | 0 | (other<0) | (other<0) | ||
(other>0) | 0 | 0 | 0 | ||
(other) | (other) | (other) | (other) |
[The "(other)" in the last row for "Correct min" and "Naive min" means the correct minimum, in the straightforward sense without any confusion because of NaN or −0.]
So you see there are four rows in the table above in which the naive function would give a wrong answer:
-
three of them are the case when
a
is NaN, butb
is not. This is what the first check in the function is for. -
the other is the case where
Math.min(0, -0)
is documented by Java as returning −0, even though IEEE 754 treats 0 and −0 as equal for comparison (and therefore the comparison "0 ≤ −0" evaluates as true). This is what the second check in the function is for.