Os Javeiros


Evite float e double se você quer respostas exatas!

Posted in Java por javeiros em junho 25, 2008

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.

Retirado de: http://www.jeebrasil.com.br/mostrar/47

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s


%d blogueiros gostam disto: