¿Doble versus BigDecimal?
Tengo que calcular algunas variables de punto flotante y mi colega me sugiere usarlas BigDecimal
en lugar de double
ya que será más preciso. Pero quiero saber qué es y cómo aprovecharlo al máximo BigDecimal
.
A BigDecimal
es una forma exacta de representar números. A Double
tiene cierta precisión. Trabajar con dobles de varias magnitudes (digamos d1=1000.0
y d2=0.001
) podría dar como resultado que 0.001
se eliminen por completo al sumar, ya que la diferencia de magnitud es muy grande. Con BigDecimal
esto no pasaría.
La desventaja BigDecimal
es que es más lento y es un poco más difícil programar algoritmos de esa manera (debido a +
-
*
que /
no está sobrecargado).
Si se trata de dinero o la precisión es imprescindible, utilice BigDecimal
. De lo contrario, Doubles
suelen ser lo suficientemente buenos.
Recomiendo leer el javadoc porque BigDecimal
explican las cosas mejor que yo aquí :)
Mi inglés no es bueno, así que escribiré aquí un ejemplo sencillo.
double a = 0.02;
double b = 0.03;
double c = b - a;
System.out.println(c);
BigDecimal _a = new BigDecimal("0.02");
BigDecimal _b = new BigDecimal("0.03");
BigDecimal _c = _b.subtract(_a);
System.out.println(_c);
Salida del programa:
0.009999999999999998
0.01
¿Alguien todavía quiere usar el doble? ;)
Hay dos diferencias principales con el doble:
- Precisión arbitraria, de manera similar a BigInteger, pueden contener un número de precisión y tamaño arbitrarios (mientras que un doble tiene un número fijo de bits)
- Base 10 en lugar de Base 2, un BigDecimal es
n*10^-scale
donde n es un entero grande arbitrario con signo y la escala se puede considerar como el número de dígitos para mover el punto decimal hacia la izquierda o hacia la derecha.
Todavía no es cierto decir que BigDecimal pueda representar cualquier número. Pero dos razones por las que debería utilizar BigDecimal para cálculos monetarios son:
- Puede representar todos los números que se pueden representar en noción decimal y eso incluye prácticamente todos los números del mundo monetario (nunca transfieres 1/3 $ a alguien).
- La precisión se puede controlar para evitar errores acumulados. Con a
double
, a medida que aumenta la magnitud del valor, su precisión disminuye y esto puede introducir un error significativo en el resultado.
Si escribe un valor fraccionario como 1 / 7
un valor decimal, obtendrá
1/7 = 0.142857142857142857142857142857142857142857...
con una repetición infinita de los dígitos 142857
. Como sólo puedes escribir un número finito de dígitos, inevitablemente introducirás un error de redondeo (o truncamiento).
Los números similares 1/10
o 1/100
expresados como números binarios con parte fraccionaria también tienen un número infinito de dígitos después del punto decimal:
1/10 = binary 0.0001100110011001100110011001100110...
Doubles
almacena valores como binarios y, por lo tanto, podría introducir un error únicamente al convertir un número decimal en un número binario, sin siquiera hacer ninguna aritmética.
Los números decimales, por otro lado, a menudo almacenan cada dígito decimal tal como está (codificado en binario, pero cada decimal por sí solo) o, en el caso de BigDecimal
, como dos números enteros binarios con signo en la forma x*10 y (gracias a @ AlexSalauyou por señalarlo). Ver: Clase BigDecimal . Esto significa que un tipo decimal no es más preciso que un tipo binario de punto flotante o de punto fijo en un sentido general (es decir, no puede almacenarse 1/7
sin pérdida de precisión), pero es más preciso para números que tienen un número finito de dígitos decimales como Este suele ser el caso de los cálculos monetarios.
Java BigDecimal
tiene la ventaja adicional de que puede tener un número arbitrario (pero finito) de dígitos en ambos lados del punto decimal, limitado únicamente por la memoria disponible.
Si se trata de cálculo, existen leyes sobre cómo se debe calcular y qué precisión se debe utilizar. Si fallas, estarás haciendo algo ilegal. La única razón real es que la representación de bits de los casos decimales no es precisa. Como dijo simplemente Basil, un ejemplo es la mejor explicación. Sólo para complementar su ejemplo, esto es lo que sucede:
static void theDoubleProblem1() {
double d1 = 0.3;
double d2 = 0.2;
System.out.println("Double:\t 0,3 - 0,2 = " + (d1 - d2));
float f1 = 0.3f;
float f2 = 0.2f;
System.out.println("Float:\t 0,3 - 0,2 = " + (f1 - f2));
BigDecimal bd1 = new BigDecimal("0.3");
BigDecimal bd2 = new BigDecimal("0.2");
System.out.println("BigDec:\t 0,3 - 0,2 = " + (bd1.subtract(bd2)));
}
Producción:
Double: 0,3 - 0,2 = 0.09999999999999998
Float: 0,3 - 0,2 = 0.10000001
BigDec: 0,3 - 0,2 = 0.1
También tenemos eso:
static void theDoubleProblem2() {
double d1 = 10;
double d2 = 3;
System.out.println("Double:\t 10 / 3 = " + (d1 / d2));
float f1 = 10f;
float f2 = 3f;
System.out.println("Float:\t 10 / 3 = " + (f1 / f2));
// Exception!
BigDecimal bd3 = new BigDecimal("10");
BigDecimal bd4 = new BigDecimal("3");
System.out.println("BigDec:\t 10 / 3 = " + (bd3.divide(bd4)));
}
Nos da la salida:
Double: 10 / 3 = 3.3333333333333335
Float: 10 / 3 = 3.3333333
Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion
Pero:
static void theDoubleProblem2() {
BigDecimal bd3 = new BigDecimal("10");
BigDecimal bd4 = new BigDecimal("3");
System.out.println("BigDec:\t 10 / 3 = " + (bd3.divide(bd4, 4, BigDecimal.ROUND_HALF_UP)));
}
Tiene la salida:
BigDec: 10 / 3 = 3.3333