Como faço para calcular corretamente com BigDecimal do Java? [duplicado]

Oct 26 2020

Pelo que entendi, BigDecimalexiste para lidar adequadamente com números com casas decimais fixas (ou seja, dinheiro). Aqui está um pequeno programa que escrevi:

import java.math.*;

public class Main {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal(11.22, new MathContext(2, RoundingMode.HALF_UP));
        System.out.println("a: " + a);
        BigDecimal b = a.add(new BigDecimal(0.04));
        System.out.println("b: " + b);
    }
}

Eu esperava ver:

a: 11.22
b: 11.26

Mas o que consegui é:

a: 11
b: 11.040000000000000000832667268468867405317723751068115234375

Eu configurei apara ter duas casas decimais, mas ambos não as imprime, e até mesmo as esquece e as arredonda para int simples. Por quê? bdeve somar 0,04 e saber aque tem duas casas decimais também. Isso era pelo menos o que eu esperava.

Como isso é resolvido corretamente usando a edição BigDecimal : com dois valores duplos como entrada e um número conhecido de casas decimais? [Ou seja, porque uma API não me dá nada além desses dois duplos.] (Eu sei que existem outras maneiras de calcular com dinheiro de forma confiável (começando pelo cálculo em centavos com int), mas isso está fora do escopo da minha pergunta.)

Respostas

1 morras Oct 26 2020 at 20:32

Como @Progman também escreveu, usar o construtor duplo criará uma representação decimal exata do valor duplo, e isso é desaconselhado na documentação .

No entanto, o motivo pelo qual você está obtendo 11 em vez de 11,22 é que você definiu a precisão de seu MathContext como 2. A
precisão é o número de dígitos usados ​​e não o número de casas decimais. Portanto, se você alterou seu código para usar 4 como a precisão, você obteria a saída

a: 11.22
b: 11.260000000000000000832667268468867405317723751068115234375

Ainda com o problema do duplo valor a ser utilizado, mas agora com mais casas decimais!

A definição da precisão que o número de dígitos é na documentação da classe MathContext documentação

4 Progman Oct 26 2020 at 20:23

Não use o construtor BigDecimal com o doubleargumento exatamente por esse motivo:

  • Os resultados desse construtor podem ser um tanto imprevisíveis. Pode-se supor que escrever new BigDecimal(0.1)em Java cria um BigDecimal que é exatamente igual a 0.1(um valor fora de escala de 1, com uma escala de 1), mas na verdade é igual a 0.1000000000000000055511151231257827021181583404541015625. Isso ocorre porque 0.1não pode ser representado exatamente como um double(ou, nesse caso, como uma fração binária de qualquer comprimento finito). Portanto, o valor que está sendo passado para o construtor não é exatamente igual a 0.1, apesar das aparências.

Use o construtor com o Stringargumento :

public static void main(String[] args) {
    BigDecimal a = new BigDecimal("11.22");
    System.out.println("a: " + a);
    BigDecimal b = a.add(new BigDecimal("0.04"));
    System.out.println("b: " + b);
}

Isso irá gerar a saída conforme o esperado:

a: 11.22
b: 11.26
1 WJS Oct 26 2020 at 20:58

O problema de usar valores duplos em vez de strings como argumento para BigDecimalé que eles geralmente não são exatos e resultarão em expansões decimais repetidas. Na verdade, apenas os números de ponto flutuante que são potências de 5e ou 2no denominador podem ser representados exatamente como flutuante ou duplo (por exemplo, 1/20 = 0,05 1/4 = 0,25, 1/5 = 0,2). Isso ocorre porque 5e 2são os únicos fatores primos de base 10e retornarão uma expansão finita (ou seja, não repetitiva) da fração. Qualquer outro valor resultará em uma expansão decimal repetida (por exemplo, 1/3 = .3333333333, 1/6 = .16666666), e é isso que está causando o problema.

Ao especificar Strings em vez de doubles, BigDecimalpode operar no valor desejado esperado, em oposição ao valor binário sobre o qual limitou ou não controlou.

Se seus valores fossem os seguintes.

BigDecimal a = new BigDecimal(11.25);             
System.out.println("a: " + a);
BigDecimal b = a.add(new BigDecimal(.50));
System.out.println("b: " + b);

A saída teria sido

11.25
11.75

Porque as partes fracionárias e a soma resultante têm apenas 5 e 2 como divisores.

Por esse motivo, você deve especificar a representação de string de valores de ponto flutuante ao inicializar BigDecimalobjetos.

Para obter mais informações sobre a representação interna de números de ponto flutuante, verifique IEEE 754