Informações
| Tipo: | Artigo |
|---|---|
| Data de Publicação: | 12/11/2006 |
| Revisado em: | 12/11/2006 |
Vote!
Tags Relacionadas
Comentários ( 17 )
Imprimir
Evite float e double se você quer respostas exatas!
por:
David Pereira (david@jeebrasil.com.br)
Usar double ou float podem trazer muitos problemas para operações que exijam exatidão, como operações financeiras. Saiba como evitar tais problemas utilizando as alternativas aqui apresentadas.
Introdução
Um livro que todo programador Java deveria ler é o Effective Java [1], de Joshua Bloch. Effective Java apresenta um conjunto de boas práticas que você deve ter em mente quando está programando em Java e muitas das dicas que o livro dá são preciosas.
Um tópico abordado que eu considero extremamente importante é o que dá título a este artigo: Avoid float and double if exact answers are required, do capítulo 7 – General Programming. Basicamente, o problema é que quando precisamos usar números decimais e precisamos obter respostas exatas, sem erros de precisão, usar os tipos float e double para representar tais números pode causar problemas. Que problemas?
Pegando emprestado o exemplo do Effective Java, imagine que você tem R$ 1,95 no seu bolso e gasta R$ 1,03. Com quanto você vai ficar? A resposta é óbvia: R$ 0,92. Agora tente executar a seguinte linha de código:
System.out.println(1.95 - 1.03);
Listagem 1: Exemplo de operação com doubles que resulta em número inexato
O que será impresso? Não, não será 0,92, será 0,9199999999999999.
Para ilustrar melhor o problema, veja também o seguinte exemplo: você tem R$ 1,50 e quer comprar balas no Mercadinho do Seu Zé. A máquina de balas do Seu Zé funciona da seguinte maneira: você coloca o dinheiro e aperta um botão referente ao tipo de bala que você quer. Cada vez que você aperta um botão, a bala escolhida é expelida pela máquina e o seu saldo é diminuído. Você poderá tirar balas enquanto o seu saldo for maior que zero.
Só que os programadores da máquina de balas do Mercadinho do Seu Zé não foram muito espertos e usaram double para representar o saldo do cliente e os preços das balas. O que acontecerá se você resolver tirar balas de R$ 0,15 até acabar o seu saldo? Quantas balas será possível obter? Uma mente sã responderia 10 balas, mas não é isso que acontece, basta rodar o código abaixo.
double seuSaldo = 1.50;
int balasRetiradas = 0;
while (seuSaldo > 0) {
balasRetiradas++;
seuSaldo -= 0.15;
System.out.println("Retirou " + balasRetiradas + " bala(s)");
System.out.println("Saldo Atual: " + seuSaldo + "\n");
}
System.out.println("Você retirou " + balasRetiradas + " balas!");
Listagem 2: Algoritmo da máquina de balas do Seu Zé
A saída será a seguinte:
Retirou 1 bala(s) Saldo Atual: 1.35 Retirou 2 bala(s) Saldo Atual: 1.2000000000000002 Retirou 3 bala(s) Saldo Atual: 1.0500000000000003 Retirou 4 bala(s) Saldo Atual: 0.9000000000000002 Retirou 5 bala(s) Saldo Atual: 0.7500000000000002 Retirou 6 bala(s) Saldo Atual: 0.6000000000000002 Retirou 7 bala(s) Saldo Atual: 0.4500000000000002 Retirou 8 bala(s) Saldo Atual: 0.30000000000000016 Retirou 9 bala(s) Saldo Atual: 0.15000000000000016 Retirou 10 bala(s) Saldo Atual: 1.6653345369377348E-16 Retirou 11 bala(s) Saldo Atual: -0.14999999999999983 Você retirou 11 balas!
Listagem 3: Como enganar o Seu Zé...
Por causa dos erros de precisão, o seu saldo não era zero quando deveria ser. Era um número bastante pequeno, bem próximo de zero, mas ainda maior que zero. Isso possibilitou a retirada de mais uma bala, até que o saldo ficou negativo – coisa que nunca deveria ocorrer.
Sistemas que lidam com operações monetárias são especialmente sensíveis ao problema apresentado. Erros que, a princípio, poderiam ser desconsiderados, pois representavam apenas 0,0001 centavo, podem virar erros sérios (de algumas dezenas de centavos – que podem não parecer tão importantes mas são essenciais para uma auditoria) quando o montante com o qual se está trabalhando é grande.
Agora que já conhecemos bem o problema, por que isso ocorre? Para entender, vamos usar um pouco de matemática.
Matemática Binária
Nós usamos no dia-a-dia números na base decimal, ou seja, números representados por algarismos que variam de 0 a 9. Além disso, a posição em que esses algarismos se encontram influencia no seu valor – por exemplo, em 25, o algarismo 5 vale 5, mas em 51 o algarismo 5 vale 50. O número 2546, por exemplo, pode ser decomposto de acordo com o valor de cada algarismo em sua posição:
(2 * 1000) + (5 * 100) + (4 * 10) + (6 * 1)
ou ainda:
(2 * 103) + (5 * 102) + (4 * 101) + (6 * 100)
Podemos considerar então que, começando do zero e crescendo para a esquerda, o valor do algarismo é igual ao seu valor multiplicado por dez elevado à sua posição no número e o valor do número é igual à soma dos valores dos algarismos. Para números decimais, as posições dos números após a vírgula são consideradas negativas, portanto 0,567 é igual a:
(0 * 100) + (5 * 10-1) + (6 * 10-2) + (7 * 10-3)
Já os computadores trabalham com números na base binária: apenas dois algarismos são usados para representar os números – 0 e 1. Na base binária a posição dos algarismos também é importante: no número binário 100, o 1 vale 4. A regra é parecida com a dos números decimais, por exemplo, o número 11001 é igual a:
(1 * 24) + (1 * 23) + (0 * 22) + (0 * 21) + (1 * 20)
ou seja, 11001 em binário é 25 em decimal. Perceba que não usamos agora o dez como base da exponenciação que determina o valor da posição do algarismo, e sim 2. Se estivéssemos trabalhando com números octais (base 8), usaríamos algarismos de 0 a 7 e base 8 na exponenciação, se estivéssemos trabalhando com base hexadecimal (base 16), usaríamos algarismos de 0 a 9 e as letras A, B, C, D, E e F e base 16 na exponenciação. Para saber mais detalhes sobre números binários, visite os artigos Binary numeral system e Positional notation, na Wikipedia.
Voltando ao problema, os erros de precisão ocorrem porque a representação em binário de alguns números decimais exigem uma quantidade de dígitos maior que a quantidade disponível para armazenamento (a máquina virtual Java aloca 4 bytes - 32 bits - para floats e 8 bytes - 64 bits - para doubles), então teríamos que truncar esses dígitos em algum ponto, consequentemente interferindo na precisão do número.
Considere o algoritmo abaixo para converter um número decimal entre zero e um da base 10 para a base 2:
imprima "0," enquanto numero != 0 faça numero = numero * 2 se numero < 1 imprima "0" senão imprima "1" numero = numero - 1 fim fim
Listagem 4: Algoritmo para conversão decimal para binário de números entre 0 e 1
Tente executá-lo para um número como 0,1, por exemplo:
| Número | Resultado |
|---|---|
| 0.1 | 0. |
| 0.1 x 2 = 0.2 < 1 | 0.0 |
| 0.2 x 2 = 0.4 < 1 | 0.00 |
| 0.4 x 2 = 0.8 < 1 | 0.000 |
| 0.8 x 2 = 1.6 ≥ 1 | 0.0001 |
| 0.6 x 2 = 1.2 ≥ 1 | 0.00011 |
| 0.2 x 2 = 0.4 < 1 | 0.000110 |
| 0.4 x 2 = 0.8 < 1 | 0.0001100 |
| 0.8 x 2 = 1.6 ≥ 1 | 0.00011001 |
| 0.6 x 2 = 1.2 ≥ 1 | 0.000110011 |
| 0.2 x 2 = 0.4 < 1 | 0.0001100110 |
Listagem 5: Passos do algoritmo da listagem 4 para o número 0.1
Você perceberá que entrará em um loop infinito, pois 0,1 em binário é 0.00011001100110011...
A forma como os computadores armazenam o número não é exatamente essa, mas com isso é possível se ter uma idéia dos problemas decorrentes da representação em binário de números em ponto flutuante. Sugiro que leiam o artigo What every computer scientist should know about floating-point arithmetic [2], de David Goldberd, para entender todos os detalhes deste problema.
3. E agora, como resolver?
O Effective Java sugere que se use int, long ou BigDecimal para representar os valores monetários. A classe BigDecimal foi desenvolvida para resolver dois tipos de problemas associados a números de ponto flutuante (floats e doubles): primeiro, resolve o problema da inexatidão da representação de números decimais; segundo, pode ser usado para trabalhar com números com mais de 16 dígitos significativos. Em compensação, utilizar BigDecimal pode tornar o programa menos legível por não haver sobrecarga dos operadores matemáticos para ela, sendo necessário usar métodos da classe. Veja, por exemplo, como você faria o programa da listagem 1 com BigDecimal:
BigDecimal d1 = new BigDecimal("1.95");
BigDecimal d2 = new BigDecimal("1.03");
System.out.println(d1.subtract(d2));
Listagem 6: Programa da listagem 1 com BigDecimal
o resultado, ao contrário do primeiro exemplo, é 0.92, ou seja, o valor correto.
Utilizar os primitivos normalmente é mais rápido e mais prático, mas o problema fica por conta da definição das casas decimais. Você pode controlar diretamente as casas decimais, por exemplo, utilizando como unidade para os valores o centavo ao invés de real. Um int ou um long passariam a representar a quantidade de centavos presentes no valor, e não a quatidade de reais. Por exemplo:
long l1 = 195; long l2 = 103; System.out.println(l1 – l2);
Listagem 6: Programa da listagem 1 com long
As variáveis acima dizem que você tem 195 centavos (e não R$ 1,95) e vai gastar 103 centavos, e não R$ 1,03. No final você ficará com 92 centavos (e não R$ 0,92).
Se você trabalha com valores com diferentes números de casas decimais, talvez seja melhor deixar o computador fazer este trabalho e utilizar o BigDecimal. Senão, talvez usar long ou int seja mais fácil. Se os valores não ultrapassarem nove dígitos, você pode usar int; até dezoito dígitos, utilize long; e acima disto será necessário usar o BigDecimal.
Veja como ficaria o programa da máquina do Seu Zé com BigDecimal:
BigDecimal seuSaldo = new BigDecimal("1.50");
int balasRetiradas = 0;
while (seuSaldo.compareTo(BigDecimal.ZERO) > 0) {
balasRetiradas++;
seuSaldo = seuSaldo.subtract(new BigDecimal("0.15"));
System.out.println("Retirou " + balasRetiradas + " bala(s)");
System.out.println("Saldo Atual: " + seuSaldo + "\n");
}
System.out.println("Você retirou " + balasRetiradas + " balas!");
Listagem 7: Programa do Seu Zé com BigDecimal
E a saída seria:
Retirou 1 bala(s) Saldo Atual: 1.35 Retirou 2 bala(s) Saldo Atual: 1.20 Retirou 3 bala(s) Saldo Atual: 1.05 Retirou 4 bala(s) Saldo Atual: 0.90 Retirou 5 bala(s) Saldo Atual: 0.75 Retirou 6 bala(s) Saldo Atual: 0.60 Retirou 7 bala(s) Saldo Atual: 0.45 Retirou 8 bala(s) Saldo Atual: 0.30 Retirou 9 bala(s) Saldo Atual: 0.15 Retirou 10 bala(s) Saldo Atual: 0.00 Você retirou 10 balas!
Listagem 8: Saída do programa anterior
De forma parecida, a versão com long seria:
long seuSaldo = 150;
int balasRetiradas = 0;
while (seuSaldo > 0) {
balasRetiradas++;
seuSaldo -= 15;
System.out.println("Retirou " + balasRetiradas + " bala(s)");
System.out.println("Saldo Atual: " + seuSaldo + "\n");
}
System.out.println("Você retirou " + balasRetiradas + " balas!");
Listagem 9: Programa do Seu Zé com long
E a saída:
Retirou 1 bala(s) Saldo Atual: 135 Retirou 2 bala(s) Saldo Atual: 120 Retirou 3 bala(s) Saldo Atual: 105 Retirou 4 bala(s) Saldo Atual: 90 Retirou 5 bala(s) Saldo Atual: 75 Retirou 6 bala(s) Saldo Atual: 60 Retirou 7 bala(s) Saldo Atual: 45 Retirou 8 bala(s) Saldo Atual: 30 Retirou 9 bala(s) Saldo Atual: 15 Retirou 10 bala(s) Saldo Atual: 0 Você retirou 10 balas!
Listagem 10: Saída do programa anterior
Conclusões
Como pudemos ver nos exemplos mostrados no decorrer do artigo, algumas decisões simples e até óbvias, como usar float ou double para representar números decimais, podem trazer conseqüências sérias para o seu programa se não forem bem pensadas e analisadas. Diferenças imperceptíveis a princípio podem tornar-se grandes dores de cabeça e a mudança da estratégia de armazenamento dos valores após a implantação do sistema pode ser algo bem complicado de se fazer. Para não ter problemas no futuro, não se esqueça: “Evite float e double se você quer respostas exatas”.
Referências
[1] Joshua Bloch, Effective Java Programming Language Guide. Addison-Wesley Professional, 2001.
[2] David Goldberg, What every computer scientist should know about floating-point arithmetic. http://docs.sun.com/source/806-3568/ncg_goldberg.html, acesso em 12 de outubro de 2006.
Comentários (17)
- pelo que aparece java é um excelente esotu atento para apreender em java
- postado por José Olavo da paz Teixeira em 14/11/2006 às 23:21
- E atenção galera. Isto não acontece somente com o Java como dá para perceber
- postado por Excelente em 18/11/2006 às 23:21
- Bom, com referência e tudo. Sugestão: Padrões de Negócio = Que tal uma Classe Moeda?
- postado por Gilmatryx em 21/11/2006 às 23:21
- Olhá só o que achei - o assunto que comentávamos hoje sobre DOUBLE.
- postado por vidalcia em 15/02/2007 às 23:21
- o programa do seu joao está errado... ele deveria ser: "while (seuSaldo > 0.15) {" pois se o salvo for 0.10 ele nao deve entrar no loop... mais realmente tem que tomar cuidado com o problema do double e float
- postado por Leandro Sales em 27/02/2007 às 23:21
- Você tem razão, Leandro. Mas mesmo assim o erro acontece. :)
- postado por David Pereira em 28/02/2007 às 23:21
- E como devem ser feitas as operações de divisão? Existe um método da classe BigDecimal chamado divide que recebe pelo menos dois parametros: o quociente da operação e o tipo de arrendondamento do resultado. Mas eu não quero arredondar, quero simplesmente que sejam consideradas apenas as duas casas decimais depois do ponto. Como isso deve ser feito? Já tentei passar um terceiro parametro para o metodo divide (o parametro scale=2), mas isso não me deu o resultado que eu esperava. Alguém pode ajudar?
- postado por Leonardo em 29/03/2007 às 23:21
- Leonardo, se você tem uma divisão que dá mais de três casas decimais e só quer usar duas, você deve setar a escala e fazer um arredondamento. Um dos tipos de arredondamento da classe java.math.RoundingMode devem servir para o que você quer (creio que o RoundingMode.DOWN). Para maiores informações: http://java.sun.com/j2se/1.5.0/docs/api/java/math/RoundingMode.html
- postado por David Pereira em 30/03/2007 às 23:21
- Esse semana estava resolvendo um problema da faculdade para calcular troco e tive esse problema... Eu resoli acrescentando ao Double uma casa decimal a mais com o valor 1 que eu nao usaria.... Assim: 0,90 centavos, ficava 0,901 centavos. O problema era tipo mostrar o minimo de moedas que precisaria para dar o troco. Quando tirava 50 centavos de 90, ficava 39 centavos ao inves de 40... com isso eu resolvi... Mas creio que quando for preciso mostrar o valor, fica mais dificil...
- postado por Rafael em 31/03/2007 às 23:21
- David, cara mto bom o artigo, é um baita problema mesmo, a tempo li o seu artigo. valew.
- postado por Fábio em 17/04/2007 às 23:21
- Muito bom o artigo David, me salvou (hehe), e mais uma dica, se a BigDecimal for iniciada com um double, o erro acontece também, tem que iniciar com uma String ou seja, use: new BigDecimal("86.66"); e não new BigDecimal(86.66);
- postado por Fábio em 17/04/2007 às 23:21
- Levem isto a sério de verdade... trabalho em um sistema que foi criado sem ter conhecimento deste problema, na hora de gerar o sintegra os erros somavam valores acima de 8 reais por nota fiscal.... começamos a migração de estratégia há mais de 7 meses a ainda hoje encontramos erros de arredondamento.
- postado por Giovane em 17/04/2007 às 23:21
- Certo.. e como resolveria o problema de potênciação?? SE eu precisar pegar um BigDecimal e elevar a 7 potência como faria?? Teria q usar um loop mesmo?Ow existe alguma classe q efetua esse trabalho! Valeu!
- postado por Kadu em 11/05/2007 às 23:21
- Kadu, a classe BigDecimal tem um método pow, que calcula potenciação.
- postado por David Pereira em 13/05/2007 às 23:21
- Muito bom esse artigo! Estou tendo um problema similar, pq no meu banco de dados (paradox) as datas e horas sao armazenadas como um double, e que quando eu leio esse double da tabela ele me retorna um valor truncado em 6 casas decimais e eu preciso pega-lo completo... alguma sugestao?
- postado por Monica Priscila em 28/06/2007 às 23:21
- Mônica, estranho esse seu problema. Com relação ao double, não deveria estar acontecendo o truncamento. Talvez seja algo do Paradox, mas como não o conheço, então não sei o motivo. Espero que alguém consiga lhe ajudar.
- postado por David Pereira em 02/07/2007 às 23:21
- Muito bom seu artigo, eu não tinha conhecimento! muito bom mesmo vc está de parabéns!!!!!!!!!
- postado por Mauricio em 19/09/2007 às 23:21
