2011-04-21 25 views
11

Estoy trabajando con una aplicación que está basada completamente en dobles, y estoy teniendo problemas en un método de utilidad que analiza una cadena en un doble. Encontré una solución donde el uso de BigDecimal para la conversión resuelve el problema, pero plantea otro problema cuando voy a convertir el BigDecimal en un doble: estoy perdiendo varios lugares de precisión. Por ejemplo:perdiendo la conversión de precisión de BigDecimal java a doble

import java.math.BigDecimal; 
import java.text.DecimalFormat; 

public class test { 
    public static void main(String [] args){ 
     String num = "299792.457999999984"; 
     BigDecimal val = new BigDecimal(num); 
     System.out.println("big decimal: " + val.toString()); 
     DecimalFormat nf = new DecimalFormat("#.0000000000"); 
     System.out.println("double: "+val.doubleValue()); 
     System.out.println("double formatted: "+nf.format(val.doubleValue())); 
    } 
} 

Esto produce el siguiente resultado:

$ java test 
big decimal: 299792.457999999984 
double: 299792.458 
double formatted: 299792.4580000000 

La doble formato demuestra que se pierde la precisión después de la tercera posición (la aplicación requiere que los lugares más bajos de precisión).

¿Cómo puedo obtener BigDecimal para conservar esos lugares adicionales de precisión?

Gracias!


Actualizar después de alcanzar esta publicación. Varias personas mencionan que esto excede la precisión del tipo de datos dobles. A menos que esté leyendo esta referencia incorrectamente: http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.2.3 entonces el doble primitiva tiene un valor máximo exponencial de E max = 2K-1 -1, y la implementación estándar tiene K = 11. Entonces, el máximo exponente debería ser 511, ¿no?

Respuesta

19

Has alcanzado la precisión máxima para un double con ese número. No se puede hacer. El valor se redondea en este caso. La conversión de BigDecimal no está relacionada y el problema de precisión es el mismo de cualquier manera. Ver esto por ejemplo:

System.out.println(Double.parseDouble("299792.4579999984")); 
System.out.println(Double.parseDouble("299792.45799999984")); 
System.out.println(Double.parseDouble("299792.457999999984")); 

de salida es:

299792.4579999984 
299792.45799999987 
299792.458 

Para estos casos double tiene más de 3 dígitos de precisión después del punto decimal. Simplemente son ceros para su número y esa es la representación más cercana que puede caber en un double. Está más cerca de redondear en este caso, por lo que sus 9 parecen desaparecer. Si intenta esto:

System.out.println(Double.parseDouble("299792.457999999924")); 

Se dará cuenta de que mantiene su hijo de 9, ya que estaba más cerca de redondear hacia abajo:

299792.4579999999 

Si requiere que todo de los dígitos de su número de haber conservado, entonces tendrá que cambiar su código que opera en double. Puede usar BigDecimal en lugar de ellos. Si necesita rendimiento, puede explorar BCD como una opción, aunque no conozco ninguna biblioteca de forma directa.


En respuesta a la actualización: el máximo exponente de un número de coma flotante de doble precisión es en realidad 1023. Eso no es el factor limitante aquí, sin embargo. Su número excede la precisión de los 52 bits fraccionarios que representan el significado, consulte IEEE 754-1985.

Use this floating-point conversion para ver su número en binario.El exponente es 18 ya que 262144 (2^18) es el más cercano. Si usted toma los bits fraccionarios y subir o bajar una en binario, se puede ver que no hay suficiente precisión para representar su número:

299792.457999999900 // 0010010011000100000111010100111111011111001110110101 
299792.457999999984 // here's your number that doesn't fit into a double 
299792.458000000000 // 0010010011000100000111010100111111011111001110110110 
299792.458000000040 // 0010010011000100000111010100111111011111001110110111 
+0

casi justo; cambie el modo de redondeo, y ha resuelto el problema del OP – Anon

+4

@Anon: ¿De qué está hablando? No hay solución para el problema del OP usando un 'doble'. – WhiteFang34

+0

Vea mi respuesta, o vuelva a leer la pregunta del OP para comprender su problema al perder los 9 – Anon

0

si se basa enteramente en dobles ... ¿Por qué utiliza BigDecimal? ¿No tendría más sentido el Double? Si es demasiado grande de valor (o demasiada precisión) para eso, entonces ... no puede convertirlo; esa sería la razón para usar BigDecimal en primer lugar.

cuanto a por qué se está perdiendo precisión, desde el javadoc

convierte esta BigDecimal a un doble. Esta conversión es similar a la conversión primitiva de estrechamiento de doble a flotante como se define en la Especificación del lenguaje Java: si este BigDecimal tiene una magnitud demasiado grande como doble, se convertirá en Double.NEGATIVE_INFINITY o Double.POSITIVE_INFINITY según corresponda. Tenga en cuenta que incluso cuando el valor de retorno es finito, esta conversión puede perder información sobre la precisión del valor de BigDecimal.

+0

Y esto se aplica a la pregunta de OP cómo? ¿Has buscado en el JLS para aprender a reducir las conversiones primitivas? – Anon

0

Has alcanzado la máxima precisión posible para el doble. Si aún desea almacenar el valor de las primitivas ... una forma posible es almacenar la parte antes de la coma decimal en una larga

long l = 299792; 
double d = 0.457999999984; 

Puesto que usted no está utilizando hasta (que es una mala elección de palabras) la precisión para almacenar la sección decimal, puede contener más dígitos de precisión para el componente fraccionario. Esto debería ser lo suficientemente fácil de hacer con algunos redondeos, etc.

+2

Pero una mejor solución en general sería usar 'BigDecimal' en todas partes, que está destinado a ser usado para números de precisión arbitraria. –

+0

estuvo de acuerdo, aunque de alguna manera tuve la impresión de que el OP estaba buscando una alternativa a BigDecimal. –

+0

Esto pierde el rango de exponente del doble para números grandes y no beneficia a los números pequeños. Si vas a seguir por esta ruta, tiene más sentido usar "punto fijo" (con la parte fraccionaria como un numerador sobre 2^63 o 2^64, por ejemplo). Alternativamente, represente el número como una suma de dobles (es decir, una aproximación más algunos términos de error); incluso hay maneras de hacer sumas precisas con esto (por ejemplo, en [math.fsum()] de Python), pero no estoy seguro de lo fácil que esto se generaliza a la multiplicación/división. –

2

Solo se imprimen muchos dígitos para que, al volver a duplicar la cadena, se obtenga exactamente el mismo valor.

Algunos detalles se pueden encontrar en el Javadoc para Double#toString

¿Cuántos dígitos debe ser impreso para la parte fraccionaria de m o una? Debe haber al menos un dígito para representar la parte fraccionaria, y más allá de eso tantos, pero solo tantos, más dígitos como sean necesarios para distinguir de manera única el valor del argumento de los valores adyacentes de tipo doble. Es decir, supongamos que x es el valor matemático exacto representado por la representación decimal producida por este método para un argumento finito distinto de cero d. Entonces d debe ser el valor doble más cercano a x; o si dos valores dobles son igualmente cerca de x, a continuación, d debe ser uno de ellos y el bit menos significativo de la mantisa de D debe ser 0.

3

El problema es que un double puede contener 15 dígitos, mientras un BigDecimal puede contener un número arbitrario. Cuando llame al toDouble(), intenta aplicar un modo de redondeo para eliminar el exceso de dígitos. Sin embargo, dado que tienes muchos 9 en la salida, eso significa que siguen siendo redondeados a 0, con un carry al siguiente dígito más alto.

Para mantener tanta precisión como sea posible, es necesario cambiar el modo de redondeo de la BigDecimal para que trunca:

BigDecimal bd1 = new BigDecimal("12345.1234599999998"); 
System.out.println(bd1.doubleValue()); 

BigDecimal bd2 = new BigDecimal("12345.1234599999998", new MathContext(15, RoundingMode.FLOOR)); 
System.out.println(bd2.doubleValue()); 
Cuestiones relacionadas