Baseado no artigo “Four common pitfalls of the BigDecimal class and how to avoid them” de Frank Kiwy em Java Magazine.
O uso do construtor double
BigDecimal x = new BigDecimal(0.1);
System.out.println("x=" + x);
Poderíamos considerar que a saída do exemplo acima seria x=0.1, mas quando executamos e olhamos no console:
x=0.1000000000000000055511151231257827021181583404541015625
podemos perceber que o resultado usando esse construtor é imprevisível.
Isso ocorre porque os números de ponto flutuante são representados no hardware do computador como frações de base 2 (binário). No entanto, a maioria das frações decimais não podem ser representadas exatamente como frações binárias:
Número | Binário | Decimal |
---|---|---|
0.1 | 0.0001100110011001101 | 0.1000003814697265625 |
0.2 | 0.00110011001100110011 | 0.19999980926513671875 |
… |
Os números binários de ponto flutuante realmente armazenados na máquina se aproximam apenas dos números de ponto flutuante decimais que você insere. O valor passado para o construtor double não é exatamente igual a 0.1.
Já o construtor string é perfeitamente previsível e produz um BigDecimal que é exatamente igual a 0.1:
BigDecimal y = new BigDecimal("0.1");
System.out.println("y=" + y);
saída
y=0.1
Portanto, você deve considerar o construtor string de preferência ao construtor double.
Se, por algum motivo, um double deve ser usado para criar um BigDecimal, considere usar o método estático BigDecimal.valueOf(double). Isso dará o mesmo resultado que converter o double em uma string usando o método Double.toString(double) e, em seguida, usando o construtor BigDecimal(string).
O método estático valueOf(double)
Se estivermos usando o método estático BigDecimal.valueOf(double) para criar um BigDecimal, devemos está ciente de sua precisão limitada:
BigDecimal x = BigDecimal.valueOf(1.01234567890123456789);
BigDecimal y = new BigDecimal("1.01234567890123456789");
System.out.println("x=" + x);
System.out.println("y=" + y);
saída
x=1.0123456789012346
y=1.01234567890123456789
O valor “x” perdeu quatro dígitos decimais porque um double tem uma precisão de apenas 15-17 dígitos, enquanto um BigDecimal é de precisão arbitrária (limitado apenas pela memória).
Portanto, considere usar o construtor string, já que dois grandes problemas causados pelo construtor double serão evitados.
O método equals(bigDecimal)
BigDecimal x = new BigDecimal("1");
BigDecimal y = new BigDecimal("1.0");
System.out.println(x.equals(y));
saída
false
Esta saída se deve ao fato de que um BigDecimal consiste em um valor inteiro não dimensionado com precisão arbitrária e uma escala inteira de 32 bits, ambos os quais devem ser iguais aos valores correspondentes do outro BigDecimal que está sendo comparado. Neste caso:
- “x” tem um valor não dimensionado de 1 e uma escala de 0.
- “y” tem um valor não dimensionado de 10 e uma escala de 1.
Portanto, “x” não é igual a “y”.
Por esse motivo, duas instâncias de BigDecimal não devem ser comparadas usando o método equals(), mas em vez disso o método compareTo() deve ser usado, porque compara os valores numéricos (x = 1; y = 1.0) representados pelas duas instâncias de BigDecimal:
System.out.println(x.compareTo(y) == 0);
saída
true
O método round(mathContext)
Ao usar o método round (new MathContext(precision, roundingMode)) para arredondar um BigDecimal para “dizer” duas casas decimais. Acabamos não tendo um resultado esperado:
BigDecimal x = new BigDecimal("12345.6789");
x = x.round(new MathContext(2, RoundingMode.HALF_UP));
System.out.println("x=" + x.toPlainString());
System.out.println("scale=" + x.scale());
saída
x=12000
scale=-3
“x” não é o valor esperado de “12345,68” e a escala não é o valor esperado de “2”.
O valor não dimensionado “123456789” foi arredondado para dois dígitos significativos 12, o que representa uma precisão de 2. No entanto, como o ponto decimal foi deixado intocado, o valor real representado por este BigDecimal é 12000.0000(isso também pode ser escrito como 12000).
Já para escala, o valor sem escala deste BigDecimal é 12 e, portanto, tem que ser multiplicado por 1000, que é 10 para a potência de 3, e “12 x 10^3” é igual a 12000.
Uma escala positiva representa o número de dígitos da fração (ou seja, o número de dígitos à direita do ponto decimal), enquanto uma escala negativa representa o número de dígitos insignificantes à esquerda do ponto decimal (neste caso, os zeros à direita, uma vez que são apenas espaços reservados para indicar a escala do número).
Finalmente, o número representado por um BigDecimal é, portanto, unscaledValue x 10 ^ -scale.
Observe também que o código acima usou o método toPlainString(), que não exibe o resultado em notação científica (1.2E+4).
Para obter o resultado esperado de 12345.68, podemos usar o método setScale(scale, roundingMode):
BigDecimal x = new BigDecimal("12345.6789");
x = x.setScale(2, RoundingMode.HALF_UP);
System.out.println("x=" + x));
saida
x=12345.68
O método setScale(scale, roundingMode) arredonda a parte da fração para duas casas decimais de acordo com o modo de arredondamento especificado.
É possível usar o método round(new MathContext(precision, roundingMode)) para arredondamento, mas isso exigiria que soubessemos o número total de dígitos à esquerda da vírgula decimal do resultado do cálculo:
BigDecimal x = new BigDecimal("12345.12345");
BigDecimal y = new BigDecimal("23456.23456");
BigDecimal z = x.multiply(y);
System.out.println("z=" + z);
saída
z=289570111.3153564320
Para arredondar “z” para duas casas decimais, você teria que usar um objeto mathContext com precisão de 11:
BigDecimal xy = z.round(new MathContext(11, RoundingMode.HALF_UP));
System.out.println("xy=" + z);
saída
xy=289570111.32
O número total de dígitos à esquerda do ponto decimal pode ser calculado assim:
bigDecimal.precision() - bigDecimal.scale() + newScale
onde
- bigDecimal.precision() é a precisão do resultado não arredondado.
- bigDecimal.scale() é a escala do resultado não arredondado.
- newScale é a escala para a qual você deseja arredondar.
Neste caso:
BigDecimal xy = z.round(new MathContext(c.precision() - c.scale() + 2, RoundingMode.HALF_UP));
saida
xy=289570111.32
Mas comparando essa expressão:
z.round(new MathContext(c.precision() - c.scale() + 2, RoundingMode.HALF_UP));
com essa
z.setScale(2, RoundingMode.HALF_UP);
a segunda garante um código mais legível e conciso.
Referências
- Four common pitfalls of the BigDecimal class and how to avoid them
- BigDecimal (Java SE 17 & JDK 17)
- BigDecimal and BigInteger in Java