Problemas comuns da classe BigDecimal e como evitá-los.

Postado por Rafael Brandão em Aug. 14, 2023

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úmeroBinárioDecimal
0.10.00011001100110011010.1000003814697265625
0.20.001100110011001100110.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