Estaba revisando algunas cosas en el código fuente de java.lang.Math y noté que mientras Math.min(int, int) (o su contraparte larga) se implementa de esta manera:
public static int min(int a, int b) { return a <= b ? a : b; }Y esto tiene mucho sentido para mí y es lo mismo que yo haría. Sin embargo, la implementación doble/flotante es esta:
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; } } Estoy completamente estupefacto. ¿Comparando a consigo mismo? ¿Para qué sirve el segundo cheque? ¿Por qué no se implementa de la misma manera que la versión int/long?
Los números de punto flotante son mucho más complicados que los valores enteros.
Para este caso específico, dos distinciones son importantes:
NaN es un valor válido para float y double que representa "no un número" y se comporta de forma extraña. Es decir, no se compara igual a sí mismo.Entonces esta parte:
if (a != a) { return a; } asegura que se devuelva NaN si a es NaN (si a no es NaN , pero b sí lo es, entonces la verificación "normal" más adelante devolverá b , es decir, NaN , por lo que no se necesita una verificación explícita para este caso). Este es un patrón común: al calcular algo donde una entrada es NaN , la salida también será NaN . Dado que NaN generalmente representa algún error en el cálculo (como dividir 0 por 0), es importante que "envenene" todos los cálculos posteriores para garantizar que el error no se trague en silencio.
Esta parte:
if (a == 0.0F && b == 0.0F && (long)Float.floatToRawIntBits(b) == negativeZeroFloatBits) { return b; } asegura que si compara dos números de punto flotante con valor cero y b es cero negativo, se devuelve ese cero negativo (ya que -0.0 es "menor" que 0.0). De manera similar a NaN , la verificación normal devolverá correctamente a si es -0.0 b es 0.0.
Recomiendo leer detenidamente la documentación de Math.min y también los operadores de comparación numérica en puntos flotantes. Sus comportamientos son bastante diferentes.
Partes relevantes de Math.min :
Si cualquiera de los valores es NaN, entonces el resultado es NaN. A diferencia de los operadores de comparación numérica, este método considera que el cero negativo es estrictamente menor que el cero positivo.
y de JLS §15.20.1 "Operadores de comparación numérica <, <=, > y >="
El resultado de una comparación de punto flotante, según lo determinado por la especificación del estándar IEEE 754, es:
Si alguno de los operandos es NaN, el resultado es falso.
El cero positivo y el cero negativo se consideran iguales.
Si algún argumento es NaN, Math.min elige ese, pero si algún operando es NaN, <= se evalúa como false . Es por eso que tiene que verificar si a no es igual a sí mismo; esto significaría que a es NaN. Si a no es NaN pero b sí, el último caso lo cubriría.
Math.min también considera que -0.0 es "menor que" +0.0 , pero los operadores de comparación numérica creen que son iguales. Este es el propósito de la segunda verificación.
Puedo ayudarte en la primera comparación if (a != a) . Obviamente, esto solo analiza a , entonces, ¿en qué casos podría ser a el mínimo independientemente de b ?
Los números float se diferencian de int por tener valores especiales , por ejemplo, NAN . Y una propiedad especial de NAN es que una comparación siempre es falsa. Entonces, la primera condición devuelve a si cada operador de comparación devuelve falso en a .
La misma condición para b se puede encontrar en la última línea. Si una comparación en b siempre devuelve false, la última línea siempre devuelve b .
En la segunda condición, solo puedo adivinar que esto está relacionado con "cero negativo" y "cero positivo", otros dos valores especiales de float . Y por supuesto, un cero negativo es más pequeño que un cero positivo.
Solo por completitud/claridad, elaboremos una tabla de todos los resultados posibles:
Cualquiera de a y b puede ser cualquiera
Escribir todas las combinaciones de estos para completar, y distinguir entre números positivos y negativos para mayor claridad en algunos casos, da las 20 filas en la tabla a continuación, aunque la mayoría de ellas son sencillas y sin problemas.
La columna titulada "Correct min" es el valor correcto que se supone que se devolverá de acuerdo con elestándar IEEE 754 y la documentación de Java de Math.min , y la columna titulada "Naive min" es el valor que se habría devuelto si Math.min se había implementado como return a <= b ? a : b; en lugar de.
| a | B | Mínimo correcto | minuto ingenuo | Notas sobre min ingenuo | Ingenuo min mal? |
|---|---|---|---|---|---|
| Yaya | Yaya | Yaya | Yaya | b, ya que la comparación de NaN da falso. | |
| Yaya | −0 | Yaya | −0 | b, ya que la comparación de NaN da falso. | Incorrecto |
| Yaya | 0 | Yaya | 0 | b, ya que la comparación de NaN da falso. | Incorrecto |
| Yaya | (otro) | Yaya | (otro) | b, ya que la comparación de NaN da falso. | Incorrecto |
| −0 | Yaya | Yaya | Yaya | b, ya que la comparación de NaN da falso. | |
| −0 | −0 | −0 | −0 | a, como −0 ≤ −0. | |
| −0 | 0 | −0 | −0 | a, como −0 ≤ 0. | |
| −0 | (otro>0) | −0 | −0 | ||
| −0 | (otro<0) | (otro<0) | (otro<0) | ||
| 0 | Yaya | Yaya | Yaya | b, ya que la comparación de NaN da falso. | |
| 0 | −0 | −0 | 0 | a, como "0 ≤ −0" según IEEE 754. | Incorrecto |
| 0 | 0 | 0 | 0 | a, como 0 ≤ 0. | |
| 0 | (otro>0) | 0 | 0 | ||
| 0 | (otro<0) | (otro<0) | (otro<0) | ||
| (otro) | Yaya | Yaya | Yaya | b, ya que la comparación de NaN da falso. | |
| (otro<0) | −0 | (otro<0) | (otro<0) | ||
| (otro>0) | −0 | −0 | −0 | ||
| (otro<0) | 0 | (otro<0) | (otro<0) | ||
| (otro>0) | 0 | 0 | 0 | ||
| (otro) | (otro) | (otro) | (otro) |
[El "(otro)" en la última fila para "Correct min" y "Naive min" significa el mínimo correcto, en el sentido directo sin ninguna confusión debido a NaN o −0.]
Como puede ver, hay cuatro filas en la tabla anterior en las que la función ingenua daría una respuesta incorrecta:
tres de ellos son el caso cuando a es NaN, pero b no lo es. Para esto es el primer cheque en la función.
el otro es el caso en el que Java documenta que Math.min(0, -0) devuelve −0, aunque IEEE 754 trata 0 y −0 como iguales para la comparación (y, por lo tanto, la comparación "0 ≤ −0" se evalúa como cierto). Para esto es el segundo cheque en la función.