java - programacion - punto flotante plc
¿Cómo evitar los errores de precisión de punto flotante con flotantes o dobles en Java? (12)
Debe usar un tipo de datos decimales, no flotantes:
https://docs.oracle.com/javase/7/docs/api/java/math/BigDecimal.html
Tengo un problema muy molesto con grandes sumas de flotantes o dobles en Java. Básicamente, la idea es que si ejecuto:
for ( float value = 0.0f; value < 1.0f; value += 0.1f )
System.out.println( value );
Lo que obtengo es:
0.0
0.1
0.2
0.3
0.4
0.5
0.6
0.70000005
0.8000001
0.9000001
Entiendo que hay una acumulación del error de precisión flotante, sin embargo, ¿cómo deshacerse de esto? Intenté usar dobles a la mitad del error, pero el resultado sigue siendo el mismo.
¿Algunas ideas?
En aras de la completitud, recomiendo esta:
Shewchuck, "Predictores geométricos adaptables robustos de punto flotante", si desea más ejemplos de cómo realizar aritmética exacta con coma flotante, o al menos precisión controlada, que es la intención original del autor, http://www.cs.berkeley.edu/~jrs/papers/robustr.pdf
Me enfrenté al mismo problema, resolví lo mismo usando BigDecimal. A continuación se muestra el fragmento que me ayudó.
double[] array = {45.34d, 45000.24d, 15000.12d, 4534.89d, 3444.12d, 12000.00d, 4900.00d, 1800.01d};
double total = 0.00d;
BigDecimal bTotal = new BigDecimal(0.0+"");
for(int i = 0;i < array.length; i++) {
total += (double)array[i];
bTotal = bTotal.add(new BigDecimal(array[i] +""));
}
System.out.println(total);
System.out.println(bTotal);
Espero que te ayude.
No es solo un error acumulado (y no tiene absolutamente nada que ver con Java). 1.0f
, una vez traducido al código real, no tiene el valor 0.1 - ya se obtiene un error de redondeo.
De la guía flotante de puntos:
¿Qué puedo hacer para evitar este problema?
Eso depende de qué tipo de cálculos estés haciendo.
- Si realmente necesita que sus resultados se sumen exactamente, especialmente cuando trabaja con dinero: use un tipo de datos decimal especial.
- Si simplemente no desea ver todos los decimales adicionales: simplemente formatee su resultado redondeado a un número fijo de decimales cuando lo muestre.
- Si no tiene un tipo de datos decimal disponible, una alternativa es trabajar con enteros, por ejemplo, hacer cálculos de dinero en centavos por completo. Pero esto es más trabajo y tiene algunos inconvenientes.
Lea el sitio enlazado para obtener información detallada.
No hay una representación exacta de 0.1 como float
o double
. Debido a este error de representación, los resultados son ligeramente diferentes de lo que esperaba.
Un par de enfoques que puedes usar:
- Cuando use el tipo
double
, solo muestre tantos dígitos como necesite. Al verificar la igualdad, permita una pequeña tolerancia en ambos sentidos. - Alternativamente, utilice un tipo que le permita almacenar exactamente los números que intenta representar, por ejemplo,
BigDecimal
puede representar 0.1 exactamente.
Código de ejemplo para BigDecimal
:
BigDecimal step = new BigDecimal("0.1");
for (BigDecimal value = BigDecimal.ZERO;
value.compareTo(BigDecimal.ONE) < 0;
value = value.add(step)) {
System.out.println(value);
}
No use float / double en un iterador ya que esto maximiza su error de redondeo. Si solo usas el siguiente
for (int i = 0; i < 10; i++)
System.out.println(i / 10.0);
imprime
0.0
0.1
0.2
0.3
0.4
0.5
0.6
0.7
0.8
0.9
Sé que BigDecimal es una opción popular, pero prefiero no hacerlo doblemente porque es mucho más rápido, pero generalmente es mucho más corto / más limpio de entender.
Si cuenta la cantidad de símbolos como una medida de la complejidad del código
- usando doble => 11 símbolos
- use BigDecimal (del ejemplo de @Mark Byers) => 21 símbolos
Por cierto: no use flotador a menos que haya una buena razón para no usar el doble.
Otra solución es renunciar a ==
y verificar si los dos valores están lo suficientemente cerca . (Sé que esto no es lo que preguntaste en el cuerpo pero estoy respondiendo el título de la pregunta).
Primero haz doble . Nunca use float o tendrá problemas para usar las utilidades java.lang.Math
.
Ahora bien, si usted sabe de antemano la precisión que desea y es igual o menor que 15, entonces es fácil decirle a su pareja que se comporte. Verifique a continuación:
// the magic method:
public final static double makePrecise(double value, int precision) {
double pow = Math.pow(10, precision);
long powValue = Math.round(pow * value);
return powValue / pow;
}
Ahora, cada vez que realice una operación, debe decir a su doble resultado que se comporte:
for ( double value = 0.0d; value < 1.0d; value += 0.1d )
System.out.println( makePrecise(value, 1) + " => " + value );
Salida:
0.0 => 0.0
0.1 => 0.1
0.2 => 0.2
0.3 => 0.30000000000000004
0.4 => 0.4
0.5 => 0.5
0.6 => 0.6
0.7 => 0.7
0.8 => 0.7999999999999999
0.9 => 0.8999999999999999
1.0 => 0.9999999999999999
Si necesita más de 15 precisión, entonces no tiene suerte:
for ( double value = 0.0d; value < 1.0d; value += 0.1d )
System.out.println( makePrecise(value, 16) + " => " + value );
Salida:
0.0 => 0.0
0.1 => 0.1
0.2 => 0.2
0.3000000000000001 => 0.30000000000000004
0.4 => 0.4
0.5 => 0.5
0.6 => 0.6
0.7 => 0.7
0.8 => 0.7999999999999999
0.9 => 0.8999999999999999
0.9999999999999998 => 0.9999999999999999
NOTA 1: para el rendimiento, debe almacenar en caché la operación Math.pow
en una matriz. No hecho aquí para mayor claridad.
NOTA 2: Es por eso que nunca usamos double s para los precios, pero long s donde los últimos N (es decir, donde N <= 15, generalmente 8) dígitos son los dígitos decimales. Entonces puedes olvidarte de lo que escribí arriba :)
Puede evitar este problema específico usando clases como BigDecimal
. float
y double
, siendo IEEE 754 punto flotante, no están diseñados para ser perfectamente precisos, están diseñados para ser rápidos. Pero tenga en cuenta el punto de Jon a continuación: BigDecimal
no puede representar "un tercio" con precisión, más del double
puede representar "una décima" con precisión. Pero para (por ejemplo) cálculos financieros, BigDecimal
y clases como esta tienden a ser el camino a seguir, ya que pueden representar números en la forma en que los humanos tendemos a pensar en ellos.
Si quiere seguir usando float
y evitar la acumulación de errores al agregar 0.1f
repetidamente, intente algo como esto:
for (int count = 0; count < 10; count++) {
float value = 0.1f * count;
System.out.println(value);
}
Sin embargo, tenga en cuenta que, como otros ya han explicado, ese float
no es un tipo de datos infinitamente preciso.
Solo necesita conocer la precisión requerida en su cálculo y la precisión de la que es capaz su tipo de datos elegido y presentar sus respuestas en consecuencia.
Por ejemplo, si se trata de números con 3 cifras significativas, el uso de float
(que proporciona una precisión de 7 cifras significativas) es apropiado. Sin embargo, no puede citar su respuesta final con una precisión de 7 cifras significativas si sus valores iniciales solo tienen una precisión de 2 cifras significativas.
5.01 + 4.02 = 9.03 (to 3 significant figures)
En su ejemplo, está realizando adiciones múltiples, y con cada adición hay un impacto consecuente en la precisión final.
package loopinamdar;
import java.text.DecimalFormat;
public class loopinam {
static DecimalFormat valueFormat = new DecimalFormat("0.0");
public static void main(String[] args) {
for (float value = 0.0f; value < 1.0f; value += 0.1f)
System.out.println("" + valueFormat.format(value));
}
}