¿Qué hay de malo en usar == para comparar flotantes en Java?

177

De acuerdo con esta página java.sun == es el operador de comparación de igualdad para números de coma flotante en Java.

Sin embargo, cuando escribo este código:

if(sectionID == currentSectionID)

en mi editor y ejecuto un análisis estático, obtengo: "JAVA0078 Valores de coma flotante en comparación con =="

¿Qué tiene de malo usar ==para comparar valores de coma flotante? ¿Cuál es la forma correcta de hacerlo? 

usuario128807
fuente
29
Debido a que comparar flotantes con == es problemático, no es prudente usarlos como ID; los nombres en su código de ejemplo sugieren que eso es lo que está haciendo; Se prefieren los enteros largos (longs), y el estándar de facto para las ID.
Carl Manaster el
44
Sí, ¿fue solo un ejemplo aleatorio o realmente utilizas flotadores como ID? ¿Hay una razón?
Según Wiklander
55
"para los campos flotantes, use el método Float.compare; y para los campos dobles, use Double.compare. El tratamiento especial de los campos flotantes y dobles se hace necesario por la existencia de Float.NaN, -0.0f y las constantes dobles análogas; consulte la documentación de Float.equals para más detalles ". (Joshua Bloch: Java efectivo)
lbalazscs

Respuestas:

211

La forma correcta de probar flotadores para 'igualdad' es:

if(Math.abs(sectionID - currentSectionID) < epsilon)

donde épsilon es un número muy pequeño como 0.00000001, dependiendo de la precisión deseada.

Víctor
fuente
26
Consulte el enlace en la respuesta aceptada ( cygnus-software.com/papers/comparingfloats/comparingfloats.htm ) para ver por qué un épsilon fijo no siempre es una buena idea. Específicamente, a medida que los valores en los flotadores que se comparan se hacen grandes (o pequeños), el épsilon ya no es apropiado. (Sin embargo, usar epsilon está bien si sabe que sus valores flotantes son relativamente razonables).
PT
1
@PT ¿Puede multiplicar épsilon con un número y cambiar la función if(Math.abs(sectionID - currentSectionID) < epsilon*sectionIDpara abordar ese problema?
enthusiasticgeek
3
Esta puede incluso ser la mejor respuesta hasta ahora, pero aún tiene fallas. ¿De dónde sacas el épsilon?
Michael Piefel
1
@MichaelPiefel ya dice: "dependiendo de la precisión deseada". Los flotadores, por su naturaleza, son como valores físicos: solo le interesan algunas posiciones limitadas dependiendo de la inexactitud total, cualquier diferencia más allá de eso se considera discutible.
ivan_pozdeev
Pero el OP realmente solo quería probar la igualdad, y dado que se sabe que esto no es confiable, tiene que usar un método diferente. Aún así, no creo que él sepa cuál es su "precisión deseada"; así que si todo lo que quiere es una prueba de igualdad más confiable, la pregunta sigue siendo: ¿de dónde obtiene el épsilon? Propuse usar Math.ulp()en mi respuesta a esta pregunta.
Michael Piefel
53

Los valores de coma flotante pueden desactivarse un poco, por lo que pueden no informar exactamente igual. Por ejemplo, estableciendo un flotante en "6.1" y luego imprimiéndolo nuevamente, puede obtener un valor reportado de algo como "6.099999904632568359375". Esto es fundamental para la forma en que funcionan los flotadores; por lo tanto, no desea compararlos utilizando la igualdad, sino más bien la comparación dentro de un rango, es decir, si la diferencia del flotante con el número con el que desea compararlo es menor que un cierto valor absoluto.

Este artículo en el Registro ofrece una buena descripción de por qué este es el caso; Lectura útil e interesante.

Paul Sonier
fuente
@kevindtimm: así que harás tus pruebas de igualdad de la misma manera que si (número == 6.099999904632568359375) cada vez que deseas saber el número es igual a 6.1 ... Sí, tienes razón ... todo en la computadora es estrictamente determinista, solo que las aproximaciones utilizadas para las carrozas son contra intuitivas cuando se hacen problemas matemáticos.
Newtopian
Los valores de coma flotante son solo imprecisos no deterministas en hardware muy específico .
Stuart P. Bentley
1
@Stuart Podría estar equivocado, pero no creo que el error FDIV no fuera determinista. Las respuestas dadas por el hardware no se ajustaban a las especificaciones, pero eran deterministas, ya que el mismo cálculo siempre producía el mismo resultado incorrecto
Gravity
@Gravity Puede argumentar que cualquier comportamiento es determinista dado un conjunto específico de advertencias.
Stuart P. Bentley
Los valores de coma flotante no son imprecisos. Cada valor de coma flotante es exactamente lo que es. Lo que puede ser impreciso es el resultado de un cálculo de coma flotante . ¡Pero cuidado! Cuando ve algo como 0.1 en un programa, ese no es un valor de coma flotante. Ese es un literal de coma flotante --- una cadena que el compilador convierte en un valor de coma flotante haciendo un cálculo .
Solomon Slow
22

Solo para dar la razón detrás de lo que todos los demás dicen.

La representación binaria de un flotador es un poco molesta.

En binario, la mayoría de los programadores conocen la correlación entre 1b = 1d, 10b = 2d, 100b = 4d, 1000b = 8d

Bueno, también funciona a la inversa.

.1b = .5d, .01b = .25d, .001b = .125, ...

El problema es que no hay una forma exacta de representar la mayoría de los números decimales como .1, .2, .3, etc. Todo lo que puede hacer es aproximarse en binario. El sistema hace un poco de redondeo cuando se imprimen los números para mostrar .1 en lugar de .10000000000001 o .999999999999 (que probablemente estén tan cerca de la representación almacenada como .1)

Editar del comentario: La razón por la que esto es un problema son nuestras expectativas. Esperamos totalmente que 2/3 se falsifique en algún momento cuando lo convertimos a decimal, ya sea .7 o .67 o .666667 .. Pero no esperamos automáticamente que .1 se redondee de la misma manera que 2/3 Y eso es exactamente lo que está sucediendo.

Por cierto, si tiene curiosidad, el número que almacena internamente es una representación binaria pura usando una "notación científica" binaria. Entonces, si le dice que almacene el número decimal 10.75d, almacenaría 1010b para el 10 y .11b para el decimal. Por lo tanto, almacenaría .101011 y luego guardaría algunos bits al final para decir: Mueva el punto decimal cuatro lugares a la derecha.

(Aunque técnicamente ya no es un punto decimal, ahora es un punto binario, pero esa terminología no habría hecho las cosas más comprensibles para la mayoría de las personas que encontrarían esta respuesta útil).

Bill K
fuente
1
@Matt K - um, no punto fijo; si "guarda algunos bits al final para decir mover el punto decimal [N] bits a la derecha", eso es punto flotante. El punto fijo toma la posición del punto radix para que sea, bueno, fijo. Además, en general, dado que siempre se puede cambiar el punto binamal (?) Para dejarlo con un '1' en la posición más a la izquierda, encontrará algunos sistemas que omiten el '1' inicial, dedicando el espacio así liberado (1 bit!) para extender el rango del exponente.
JustJeff el
El problema no tiene nada que ver con la representación binaria vs. decimal. Con coma flotante decimal, todavía tiene cosas como (1/3) * 3 == 0.9999999999999999999999999999.
dan04
2
@ dan04 sí, porque 1/3 no tiene representación decimal O binaria, sí tiene una representación trinaria y se convertiría correctamente de esa manera :). Todos los números que enumeré (.1, .25, etc.) tienen representaciones decimales perfectas pero no tienen representación binaria, y las personas están acostumbradas a aquellos que tienen representaciones "exactas". BCD los manejaría perfectamente. Esa es la diferencia.
Bill K
1
Esto debería tener más votos a favor, ya que describe el problema REAL detrás del problema.
Levite
19

¿Qué tiene de malo usar == para comparar valores de punto flotante?

Porque no es cierto que 0.1 + 0.2 == 0.3

AakashM
fuente
77
¿qué pasa Float.compare(0.1f+0.2f, 0.3f) == 0?
Acuario Power
0.1f + 0.2f == 0.3f pero 0.1d + 0.2d! = 0.3d. Por defecto, 0.1 + 0.2 es un doble. 0.3 es un doble también.
burnabyRails
12

Creo que hay mucha confusión sobre los flotadores (y los dobles), es bueno aclararlo.

  1. No hay nada inherentemente incorrecto en el uso de flotantes como ID en JVM compatible con el estándar [*]. Si simplemente configura la ID del flotador en x, no haga nada con ella (es decir, sin aritmética) y luego pruebe y == x, estará bien. Además, no hay nada de malo en usarlos como claves en un HashMap. Lo que no puede hacer es asumir igualdades x == (x - y) + y, etc. Dicho esto, las personas usualmente usan tipos enteros como ID, y puede observar que la mayoría de las personas aquí están desanimadas por este código, por lo que, por razones prácticas, es mejor cumplir con las convenciones . Tenga en cuenta que hay tantos doublevalores diferentes como largos values, por lo que no gana nada con el uso double. Además, generar el "próximo ID disponible" puede ser complicado con los dobles y requiere cierto conocimiento de la aritmética de coma flotante. No vale la pena.

  2. Por otro lado, confiar en la igualdad numérica de los resultados de dos cálculos matemáticamente equivalentes es arriesgado. Esto se debe a los errores de redondeo y la pérdida de precisión al convertir de representación decimal a binaria. Esto ha sido discutido hasta la muerte en SO.

[*] Cuando dije "JVM compatible con el estándar" quería excluir ciertas implementaciones de JVM con daño cerebral. Mira esto .

cuant_dev
fuente
Al usar flotantes como ID, uno debe tener cuidado de asegurarse de que se comparen usando en ==lugar de hacerlo equals, o de lo contrario, asegurarse de que no se almacene en una tabla ningún flotante que se compare de forma desigual. De lo contrario, un programa que intenta, por ejemplo, contar cuántos resultados únicos se pueden producir a partir de una expresión cuando se alimentan varias entradas, puede considerar cada valor de NaN como único.
supercat
Lo anterior se refiere a Float, no a float.
quant_dev
Lo que está hablando Float? Si uno intenta construir una tabla de floatvalores únicos y los compara ==, las horribles reglas de comparación IEEE-754 harán que la tabla se inunde de NaNvalores.
supercat
floatEl tipo no tiene equalsmétodo.
quant_dev
Ah, no me refería a un equalsmétodo de instancia, sino al método estático (creo que dentro de la Floatclase) que compara dos valores de tipo float.
supercat
9

Este es un problema no específico de Java. Usar == para comparar dos flotantes / dobles / cualquier número de tipo decimal puede causar problemas debido a la forma en que están almacenados. Un flotador de precisión simple (según el estándar IEEE 754) tiene 32 bits, distribuidos de la siguiente manera:

1 bit - Signo (0 = positivo, 1 = negativo)
8 bits - Exponente (una representación especial (sesgo-127) de la x en 2 ^ x)
23 bits - Mantisa. El número actual que se almacena.

La mantisa es la que causa el problema. Es un poco como una notación científica, solo el número en la base 2 (binario) se parece a 1.110011 x 2 ^ 5 o algo similar. Pero en binario, el primer 1 es siempre un 1 (excepto la representación de 0)

Por lo tanto, para ahorrar un poco de espacio en la memoria (juego de palabras), IEEE decidió que se debe asumir el 1. Por ejemplo, una mantisa de 1011 realmente es 1.1011.

Esto puede causar algunos problemas con la comparación, especialmente con 0 ya que 0 no puede representarse exactamente en un flotante. Esta es la razón principal por la cual se desalienta el ==, además de los problemas matemáticos de coma flotante descritos por otras respuestas.

Java tiene un problema único en el sentido de que el lenguaje es universal en muchas plataformas diferentes, cada una de las cuales podría tener su propio formato flotante único. Eso hace que sea aún más importante evitar ==.

La forma correcta de comparar dos flotadores (sin lenguaje específico para usted) para la igualdad es la siguiente:

if(ABS(float1 - float2) < ACCEPTABLE_ERROR)
    //they are approximately equal

donde ACCEPTABLE_ERROR está #definido o alguna otra constante igual a 0.000000001 o cualquier precisión requerida, como ya mencionó Victor.

Algunos idiomas tienen esta funcionalidad o esta constante incorporada, pero generalmente es un buen hábito.

CodeFusionMobile
fuente
3
Java tiene un comportamiento definido para flotadores. No depende de la plataforma.
Yishai
9

A partir de hoy, la forma rápida y fácil de hacerlo es:

if (Float.compare(sectionID, currentSectionID) == 0) {...}

Sin embargo, los documentos no especifican claramente el valor de la diferencia de margen (un épsilon de la respuesta de @Victor) que siempre está presente en los cálculos de los flotadores, pero debería ser algo razonable, ya que es parte de la biblioteca de idiomas estándar.

Sin embargo, si se necesita una precisión mayor o personalizada, entonces

float epsilon = Float.MIN_NORMAL;  
if(Math.abs(sectionID - currentSectionID) < epsilon){...}

Es otra opción de solución.

alisa
fuente
1
Los documentos que vinculó indican "el valor 0 si f1 es numéricamente igual a f2", lo que hace que sea lo mismo que hacer, (sectionId == currentSectionId)que no es preciso para los puntos flotantes. El método epsilon es el mejor enfoque, que se encuentra en esta respuesta: stackoverflow.com/a/1088271/4212710
typoerrpr
8

Los valores del punto de alimentación no son confiables debido al error de redondeo.

Como tal, probablemente no deberían usarse como valores clave, como sectionID. Utilice enteros en su lugar, o longsi intno contiene suficientes valores posibles.

Eric Wilson
fuente
2
Convenido. Dado que estos son ID, no hay razón para complicar las cosas con la aritmética de coma flotante.
Yohnny el
2
O un largo Dependiendo de cuántas ID únicas se generen en el futuro, un int puede no ser lo suficientemente grande.
Wayne Hartman el
¿Qué tan preciso es el doble en comparación con el flotador?
Arvindh Mani
1
@ArvindhMani doubles son mucho más precisos, pero también son valores de coma flotante, por lo que mi respuesta estaba destinada a incluir ambos floaty double.
Eric Wilson
7

Además de las respuestas anteriores, debe tener en cuenta que hay comportamientos extraños asociados con -0.0fy +0.0f(lo son ==pero no lo son equals) y Float.NaN(lo es equalspero no lo es ==) (espero haberlo hecho bien, ¡argh, no lo haga!).

Editar: ¡Vamos a ver!

import static java.lang.Float.NaN;
public class Fl {
    public static void main(String[] args) {
        System.err.println(          -0.0f   ==              0.0f);   // true
        System.err.println(new Float(-0.0f).equals(new Float(0.0f))); // false
        System.err.println(            NaN   ==               NaN);   // false
        System.err.println(new Float(  NaN).equals(new Float( NaN))); // true
    }
} 

Bienvenido a IEEE / 754.

Tom Hawtin - tackline
fuente
Si algo es ==, entonces son idénticos hasta el bit. ¿Cómo podrían no ser iguales ()? Tal vez lo tienes al revés?
mkb
@Matt NaN es especial. Double.isNaN (double x) en Java se implementa realmente como {return x! = X; } ...
quant_dev el
1
Con los flotantes, ==no significa que los números sean "idénticos al bit" (el mismo número puede representarse con diferentes patrones de bits, aunque solo uno de ellos tiene forma normalizada). Además, -0.0fy 0.0festán representados por diferentes patrones de bits (el bit de signo es diferente), pero se compara como igual con ==(pero no con equals). Su suposición de que ==es una comparación bit a bit es, en general, errónea.
Pavel Minaev
5

Puede usar Float.floatToIntBits ().

Float.floatToIntBits(sectionID) == Float.floatToIntBits(currentSectionID)
aamadmi
fuente
1
Estás en el camino correcto. floatToIntBits () es el camino correcto, pero sería más fácil usar la función equals () integrada de Float. Ver aquí: stackoverflow.com/a/3668105/2066079 . Puede ver que el valor predeterminado equals () utiliza floatToIntBits internamente.
dberm22
1
Sí, si son objetos flotantes. Puede usar la ecuación anterior para primitivas.
aamadmi
4

En primer lugar, ¿son flotantes o flotantes? Si uno de ellos es flotante, debe usar el método equals (). Además, probablemente sea mejor usar el método estático Float.compare.

omerkudat
fuente
4

Lo siguiente usa automáticamente la mejor precisión:

/**
 * Compare to floats for (almost) equality. Will check whether they are
 * at most 5 ULP apart.
 */
public static boolean isFloatingEqual(float v1, float v2) {
    if (v1 == v2)
        return true;
    float absoluteDifference = Math.abs(v1 - v2);
    float maxUlp = Math.max(Math.ulp(v1), Math.ulp(v2));
    return absoluteDifference < 5 * maxUlp;
}

Por supuesto, puede elegir más o menos de 5 ULP ('unidad en último lugar').

Si te gusta la biblioteca Apache Commons, la Precisionclase tiene compareTo()y equals()con epsilon y ULP.

Michael Piefel
fuente
Cuando se cambia el flotador a doble, este método no funciona como isDoubleEqual (0.1 + 0.2-0.3, 0.0) == falso
hychou
Parece que necesita más como 10_000_000_000_000_000L como factor para doublecubrir esto.
Michael Piefel
3

es posible que desee que sea ==, pero 123.4444444444443! = 123.4444444444442

KM.
fuente
2

Dos cálculos diferentes que producen números reales iguales no necesariamente producen números iguales de coma flotante. Las personas que usan == para comparar los resultados de los cálculos generalmente terminan sorprendidos por esto, por lo que la advertencia ayuda a marcar lo que de otro modo sería un error sutil y difícil de reproducir.

VoiceOfUnreason
fuente
2

¿Está tratando con código subcontratado que usaría flotantes para cosas llamadas sectionID y currentSectionID? Sólo curioso.

@ Bill K: "La representación binaria de un flotador es un poco molesta". ¿Cómo es eso? ¿Cómo lo harías mejor? Hay ciertos números que no se pueden representar en ninguna base correctamente, porque nunca terminan. Pi es un buen ejemplo. Solo puedes aproximarlo. Si tiene una mejor solución, comuníquese con Intel.

xcramps
fuente
1

Como se menciona en otras respuestas, los dobles pueden tener pequeñas desviaciones. Y podría escribir su propio método para compararlos utilizando una desviación "aceptable". Sin embargo ...

Hay una clase apache para comparar dobles: org.apache.commons.math3.util.Precision

Contiene algunas constantes interesantes: SAFE_MINyEPSILON , que son las desviaciones máximas posibles de operaciones aritméticas simples.

También proporciona los métodos necesarios para comparar, igualar o redondear dobles. (usando ulps o desviación absoluta)

bvdb
fuente
1

En una línea de respuesta que puedo decir, debes usar:

Float.floatToIntBits(sectionID) == Float.floatToIntBits(currentSectionID)

Para que aprenda más sobre el uso correcto de operadores relacionados, estoy elaborando algunos casos aquí: Generalmente, hay tres formas de probar cadenas en Java. Puede usar ==, .equals () u Objects.equals ().

¿En qué se diferencian? == prueba la calidad de referencia en cadenas, lo que significa descubrir si los dos objetos son iguales. Por otro lado, .equals () comprueba si las dos cadenas tienen el mismo valor lógicamente. Finalmente, Objects.equals () prueba cualquier nulo en las dos cadenas y luego determina si se debe llamar a .equals ().

Operador ideal para usar

Bueno, esto ha sido objeto de muchos debates porque cada uno de los tres operadores tiene su conjunto único de fortalezas y debilidades. Ejemplo, == a menudo es una opción preferida cuando se compara la referencia de objeto, pero hay casos en los que puede parecer que también se comparan valores de cadena.

Sin embargo, lo que obtienes es un valor de caída porque Java crea una ilusión de que estás comparando valores pero en el sentido real no lo eres. Considere los dos casos a continuación:

Caso 1:

String a="Test";
String b="Test";
if(a==b) ===> true

Caso 2:

String nullString1 = null;
String nullString2 = null;
//evaluates to true
nullString1 == nullString2;
//throws an exception
nullString1.equals(nullString2);

Por lo tanto, es mucho mejor usar cada operador al probar el atributo específico para el que está diseñado. Pero en casi todos los casos, Objects.equals () es un operador más universal, por lo tanto, los desarrolladores web experimentados optan por él.

Aquí puede obtener más detalles: http://fluentthemes.com/use-compare-strings-java/

Temas fluidos
fuente
-2

La forma correcta sería

java.lang.Float.compare(float1, float2)
Eric
fuente
77
Float.compare (float1, float2) devuelve un int, por lo que no se puede usar en lugar de float1 == float2 en la condición if. Además, en realidad no resuelve el problema subyacente al que se refiere esta advertencia: que si los flotantes son el resultado de un cálculo numérico, float1! = Float2 puede ocurrir solo debido a errores de redondeo.
quant_dev el
1
bien, no puedes copiar y pegar, primero debes consultar el documento.
Eric
2
Lo que puede hacer en lugar de float1 == float2 es Float.compare (float1, float2) == 0.
determine el
29
Esto no te compra nada - todavía obtienesFloat.compare(1.1 + 2.2, 3.3) != 0
Pavel Minaev
-2

Una forma de reducir el error de redondeo es usar doble en lugar de flotante. Esto no hará que el problema desaparezca, pero reduce la cantidad de error en su programa y la opción flotante casi nunca es la mejor. EN MI HUMILDE OPINIÓN.

Peter Lawrey
fuente