Arredondamento e números gigantes: do double ao BigDecimal

É fácil se deparar com as limitações do double no Java e na maioria das outras linguagens: quando vamos trabalhar com dinheiro notamos que as contas não estão saindo exatamente como esperávamos:

double d1 = 0.1;
double d2 = 0.2;
System.out.println(d1 + d2);

O resultado é um estranho 0.30000000000000004, que pode acarretar em problemas graves dependendo da utilização e arrendondamento aplicado depois nesse número. O problema é que um número com 0.1 não pode ser representado em binário de maneira finita: ele vira uma dízima (no binário ficaria algo como 0.110011001100…) diferente do número 0.25, que pode ser representado perfeitamente (no binário 0.01). A representação é um pouco mais complicada que isso, a JVM segue o padrão IEEE 754 para trabalhar com números de ponto flutuante.

Como obter o esperado 0.3? A sugestão sempre é usar o BigDecimal. BigDecimal é uma classe que trabalha com números de ponto flutuante de precisão arbitrária: você pode escolher quanto de precisão você quer usar. Por padrão ele vai utilizar o que for necessário, e, diferente do double, ele consegue guardar números como 0.1, pois guardará isto como sendo 1 x 10ˆ-1 (isto é, usando a base decimal em vez de binária, evitando a dízima).

// nao use esse construtor:
BigDecimal big1 = new BigDecimal(0.1);
BigDecimal big2 = new BigDecimal(0.2);

System.out.println(big1.add(big2));

O resultado é uma nova surpresa, um incrível 0.300000000000000016653345369377.... O que fizemos de errado agora foi que tentar somar 0.1 e 0.2 sendo que esses dois números já estavam armazenados em memória como double, e, ao serem passados para o construtor do BigDecimal, foram transportados com imprecisão. O próprio javadoc desse construtor diz que “The results of this constructor can be somewhat unpredictable“. Na verdade o resultado é bem previsível pelas suas regras, mas não é o que gostaríamos.

Como resolver? Basta sempre usar o construtor que trabalha com Strings, assim o BigDecimal vai internamente fazer o parsing desses números sem que eles sejam armazenados em um double, evitando os problemas de precisão:

// atencao! usando String no construtor:
BigDecimal big1 = new BigDecimal("0.1");
BigDecimal big2 = new BigDecimal("0.2");

System.out.println(big1.add(big2));

Finalmente obtendo o resultado esperado. Há ainda importantes observações sobre o BigDecimal: por padrão ele não fará nenhum tipo de arredondamento, o que o obriga a lançar java.lang.ArithmeticException no caso de uma dízima decimal (tentar dividir 1/3 por exemplo). Nesses casos é necessário delimitar a quantidade de bits a serem usados ou escolher o modo de arredondamento:

BigDecimal big1 = new BigDecimal("1");
BigDecimal big2 = new BigDecimal("3");

System.out.println(big1.divide(big2, 3, RoundingMode.UP));

Resultando em 0.334. Vale lembrar também da imutabilidade da classe BigDecimal, que traz diversas vantagens mas deve ser usada com cuidado quando diversas operações serão realizadas em cima de um mesmo número dentro de um laço, já que diversos BigDecimals serão instanciados durante a operação, podendo acarretar no mesmo problema de performance do uso de concatenação de Strings. O Donizetti lembrou que esse assunto é bastante discutido no item 48 do Effective Java.

No JavaScript teremos o mesmo problema caso você precise realizar contas no lado do cliente, e aí podemos usar a BigDecimalJS, que funciona de maneira análoga ao Java.

O Rafael Ferreira lembra que podemos ir além, e como dinheiro é algo pertencente ao nosso domínio e lógica de negócios, criamos uma classe Money para encapsular todo esse comportamento e evitar que RoundingMode, MathContext e escalas se espalhem por todo seu código.

29 Comentários

  1. Washington Botelho 15/07/2010 at 15:28 #

    Ótimo post Paulo,

    Quem nunca teve problemas com arredondamento atire a primeira pedra.

    Parabéns.

  2. Simão Neto 15/07/2010 at 16:04 #

    Bancana de post, acredito que as dúvidas foram tiradas… rsrsrsrs

  3. Luca Bastos 15/07/2010 at 16:17 #

    Muito bom, artigo realmente necessário.

    Esta é uma dúvida muito recorrente no GUJ. Tenho a impressão que alguns programadores não passaram na faculdade por uma cadeira de cálculo numérico mais ou menos exigente na questão dos erros. Este deve ser o motivo porque muitos programadores Java se surprendem quando 0.1 + 0.2 não é exatamente igual a 0.3. Provavelmente falta a eles o conceito de que 0.1 + 0.2 é igual a 0.3 a menos de um épsilon (número muito pequeno). Ignoram também que é o programador que sabe se seu problema exige ou não cuidar deste épsilon e tratar os arredondamentos de modo especial.

    Para cálculos matemáticos como por exemplo resolver um sisteminha de umas 1000 equações (coisa relativamente comum no cáculo de estruturas na engenharia civil), é absolutamente inviável pensar em BigDecimal.

  4. André Paes Rodrigues 15/07/2010 at 16:24 #

    Ótimo post! Já tive problemas com o BigDecimal, especialmente na hora de realizar divisões com dízima.

    Uma observação interessante é que, ao invés de utilizar o construtor BigDecimal(String x), você pode usar o método BigDecimal.valueOf(double x). Esse método converte o valor de x para String e cria um BigDecimal a partir dessa String, resolvendo o problema.

  5. Paulo Silveira 15/07/2010 at 17:23 #

    Oi Andre. Otima observaçao do valueOf (que é pos java 5).

  6. Guilherme Moreira 15/07/2010 at 18:12 #

    Bem observado do valueOf. O único porém do valueOf é que ele depende de como o double fica na memória.

    No código abaixo mesmo o valueOf perderia precisão

    double d = 0.333333333333333333333333333333333333333333333333333333333333333333;
    System.out.println(BigDecimal.valueOf(d).add(BigDecimal.valueOf(d).add(BigDecimal.valueOf(d))));

    0.9999999999999999

    Já com o construtor que recebe String não teríamos esse problema

    String s = “0.333333333333333333333333333333333333333333333333333333333333333333”;
    System.out.println(new BigDecimal(s).add(new BigDecimal(s).add(new BigDecimal(s))));

    0.999999999999999999999999999999999999999999999999999999999999999999

    Mas caso o valor que queremos trabalhar JÁ esteja como double é MUITO melhor usar o valueOf do que o construtor que recebe double

    abraços

  7. Roberto Nogueira 16/07/2010 at 15:29 #

    No Ruby tb existe o BigDecimal, mas pelo menos tem sobrecarga de operador. A parte mais chata de usar isso em Java é fazer as operações via métodos.
    Esses detalhes do construtor por exemplo, nem deveria ter sido deixado a brecha.
    Eu não espero que um programador inciante consiga captar tais detalhes da API, e operações financeiras são muito usados em programas de iniciantes.
    Embora em nossa área tudo tenha um fundamento matemático, as coisas podem ser feitaslevando em consideração apenas o dia-a-dia do programador. Ou seja, é tudo uma questão de implementação, e no caso do decimal, sempre achei muito ruim.

  8. Max Mustang 19/07/2010 at 11:00 #

    Valeu Paulo, tirou uma duvida minha xD

  9. Ricardo Lecheta 19/07/2010 at 13:20 #

    Excelente post.

  10. Wilerson 19/07/2010 at 20:06 #

    Uma coisa que eu acho bem mais simples (dependendo, sempre do modelo de negócios) é usar tipos inteiros para representar dinheiro (ou seja, trabalhar com “centavos”).

    Em aplicações que não farão uso de cálculos decimais com precisão maior do que duas casas após a vírgula (ou seja, que não calculam impostos e taxas), a precisão e facilidade de trabalhar com os tipos inteiros mais que compensam o tratamento extra que tem que ser feito na View.

  11. David Vieira 21/08/2010 at 16:31 #

    Show

  12. sombriks 25/08/2010 at 14:31 #

    legal o post,

    como esse problema de formatação só é relevante para exibir o valor em uma tela, outras soluções como o String.format[A] também são válidas.

    e em javascript uma saída que eu recomendaria mais que o bigdecimal.js seria o método “toFixed” como é visto na reintrodução[B] ao javascript.

    Continue com os bons posts!

    [A]:http://download.oracle.com/javase/1.5.0/docs/api/java/lang/String.html#format%28java.lang.String,%20java.lang.Object…%29

    [B]:http://www.w3schools.com/jsref/jsref_tofixed.asp
    [B]:https://developer.mozilla.org/en/a_re-introduction_to_javascript

  13. Jean 07/10/2010 at 10:35 #

    COBOL rules!

    int salario = 150050;
    System.out.println(“Seu salário do mês: ” salario / 100); //1500.5

    Com mod de 100 pega os centavos, com a parte inteira os Reais.

    Claro que isso só é bom para valores baixos, vai trabalhar com zilhões e terás problemas, provavelmente.

  14. Claudemir 18/10/2010 at 00:47 #

    Muito bom o post, e a questão não é se passamos ou não por uma cadeira de cálculo, todos nós não possuimos “toda” a informação, obrigado por todos os esclarecimentos.

  15. camila 29/09/2011 at 14:25 #

    Muito bom o post.
    Sou nova no Java, to fazendo um programa de calculo simples de conta, porém o valor é em reais.
    Alguém sabe como fazer com o resultado saia como em reais, ex.: 300.00.
    Quando o programinha faz o calculo o valor sai 300,000000120051, tem alguma coisa que eu possa fazer para limitar o número de decimais? Por exemplo, até duas casas após a virgula ou até 3 casas até 3 casas após o ponto.
    Desde já agradeço!

  16. Karla 15/05/2012 at 21:59 #

    Camila,

    Você pode usar o %f.
    Na declaração você põe:
    %f1.2 (1 é o parte inteira e após o ponto é a precisão de casas decimais. Se você quer duas casas no caso da moeda real, você põe dois. )

    Como no exemplo abaixo:
    public class teste {

    public static void main(String [] args)
    {
    double salario = 300.00;

    double fgts = 300.50434343;

    System.out.printf(“Salario R$ %1.2f”, salario);

    System.out.printf(“\n\nFGTS R$ %1.2f”, fgts);
    }
    }
    /* SAÍDA:
    Salario R$ 300,00

    FGTS R$ 300,50

    */

    Espero ter ajudado!

  17. Pedro Isaac 04/09/2012 at 10:22 #

    Muito bom o POST, me deparei com esse problema uns anos atras em um modulo de folha de pagamento !
    onde sempre ocorria diferença de 1 a 8 centavos de diferença , em uma folha de 1.4 milhões.

    resolvi de uma bem parecida, dando um getString no Resultset onde o tipo de dado era Float e truncando a quantidade de casas decimais depois fazendo um Parse !

    deu no mesmo, mas é mais elegante e funcional da forma descrita aqui.

    Parabens

  18. Michael Nascimento Santos 12/10/2012 at 04:06 #

    Bom, eu sempre apedrejo sobre esse assunto, então só me falta elogiar aqui o esclarecedor post. Espero ver menos usos indevidos de doubles por aí depois disso! Obrigado!

  19. Paulo Silveira 13/10/2012 at 13:44 #

    Michael, prometo usar mais o BiDecimal mesmo em exemplos pro blog :). Valeu o elogio.

  20. Fernando Gomes 15/10/2012 at 11:19 #

    Paulo, caso queira apenas armazenar valores de moeda, que não sofrerão alterações, somas, etc, vale a pena mesmo assim utilizar BigDecimal ao invés de double?

  21. Paulo Silveira 15/10/2012 at 12:04 #

    use o BigDecimal. o double, mesmo para armazenamento, nao guardara com a exatidao que voce quer se os bits nao baterem perfeitamente.

  22. João Paulo 20/05/2014 at 23:14 #

    Post esclarescedor. Excelente! Não tive uma disciplina de cálculo que me explicasse isso, mas, mesmo assim, consegui entender.

  23. Romero Dias 30/07/2014 at 09:11 #

    Excelente post!
    Estou iniciando minhas primeiras app em Java e essa informação foi muito útil.
    Abraço

  24. Fabio Caritas Barrionuevo 24/03/2015 at 14:59 #

    Uma explicação um pouco mais profunda (e técnica) sobre o tema de aritmética de números de ponto flutuante.

    http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

  25. Leo Lima 02/11/2016 at 11:35 #

    Recentemente comecei um projeto de uma calculadora simples para android como atividade da universidade, e percebi como fomos enganados esse tempo todo pelas calculadoras de nossos celulares. É engraçado por que em algum momento os desenvolvedores tiveram que escolher entre o Double e o BigDecimal.
    Descobri isso quando tentei implementar o comportamento da divisão por exemplo, eu queria que não houvesse perda, e acabei tendo que usar o Double, pois com o BigDecimal ocorria uma exceção. Testei o BigDecimal com arredondamento, mas os cálculos simples também ficaram com o arredondamento e não ficou parecendo calculadora.
    Outro problema em utilizar o BigDecimal foi o de somar uma notação com inteiro. ((0.3 * 10^-48)+1) por exemplo. O resultado não coube na tela, e o layout se desmantelou rs. Então realmente o BigDecimal tem uma tremenda utilidade para cálculos em que não podemos ter erros. LOL post bacana sobre isso. Pra quem quiser testar a calculadora: https://play.google.com/store/apps/details?id=br.uespi.calculadora

  26. Leo Magalhães 15/11/2016 at 22:06 #

    Aonde eu pego essa classe Money???

  27. Thiago Roberto 03/07/2017 at 14:21 #

    Uma calculadora que dá pra entender sobre como se dá a representação de pontos flutuantes, conforme @Paulo esclareceu.

    https://www.h-schmidt.net/FloatConverter/IEEE754.html

Deixe uma resposta