Floating point operations

Saudações,

Alguém poderia me trir esta dúvida?

É o seguinte:

Porque algumas operações de ponto flutuante no java geram resultados incorretos? Por exemplo:

public class TestPrintln {
public strictfp static void main (String[] args) {
	  byte b1 = 100;
	  float f1 = 1.2f;
	  System.out.println(f1*b1); //deveria exibir 120.0, mas o resultado 
// que obtive foi 120.00001. 
//isto é um bug? como fazer para que esta operação seja executada 
//corretamente, sem gambiarras? 
  }
}

Obrigado.

Isso acontece porque o objetivo das pessoas que fizeram os tipos de ponto flutuante era ter cálculos rápidos para pessoas que trabalham com coisas não tão precisas assim.
Por exemplo, engenheiros e cientistas que pegam dados empíricos “não tão bons” e querem calculá-los rapidamente.

O jeito de consertar é não usar double e float. Se você considera trabalhar com um int como se fosse double gambiarra pode usar classes como a BigDecimal para fazer suas contas.

O item 31 do livro Effective Java fala sobre isso. Você pode ler meu super-resumido resumo aqui

O problema é o fato que as pessoas não entendem matemática de ponto flutuante e que o computador é uma máquina binaria.

Logo executar operações com decimais exige conversão, que leva a imprecisão.
Existem duas soluções para o seu problema: use BigDecimal e aceite uma perda de performance de duas ordens de magnitude ou aprenda como usar ponto flutuante corretamente junto com formatadores de saida.

Olá

As respostas da Bani e do Louds já dizem tudo. Inclusive a Bani tocou em um ponto que o exemplo da sua pergunta quase deixa escapar. Vou contar um pouco de história para explicar minha resposta.

Quando os computadores digitais apareceram as primeiras aplicações foram com cálculos científicos. Nem todos os números reais podem ser representados de forma exata sob a forma binária mesmo que se use milhares de bits. Então lá nos primórdios da computação todo mundo estudava a questão dos erros. A gente queria saber quanto nossa solução via computador estava distante do valor exato. Como a Bani disse, muitas vezes os dados não eram tao exatos assim e ainda havia a questão de propagação de erros nos cáculos sucessivos. No final a gente tentava acreditar que nossos resultados eram exatos a menos de um valor muito pequeno, geralmente chamado de épsilon, que era a margem de erro.

Já quando se trata de grana ninguém quer seu valor errado a menos de épsilon nenhum. No caso do seu exemplo com 120.00001 tudo bem pois o erro foi para mais e arredondando daria certo. Mas se fosse 119.999999 ao arredondar se perderia pelo menos um centavo. Então já naquela época os cáculos financeiros não eram feitos com números reais. Usam-se os chamados números decimais onde as contas apesar de mais lentas não perdiam dígito nenhum. Os decimais eram representados na época de 2 modos: 1 dígito por byte ou 2 dígitos por byte. Assim, mesmo que a conta envolvesse aqueles valores absurdos do tempo em que convíviamos com inflação e qualquer Zé Ninguém ganhava milhões, nenhum centavo se perdia porque os valores ocupavam muitos bytes. A aritmética neste caso não podia usar as instruções de máquina da CPU para as contas. Se usava uma aritmética toda especial e muito mais lenta.

Com o Java, do mesmo modo que com qualquer linguagem, as contas com números de ponto flutuante podem ser feitas de 2 modos como disse o Louds:
a) Usando aritmética de ponto flutuante mas sempre contando com um pequenino erro. Os números que resultam de contas nunca são comparados por igualdade e sim a menos de um valor muito pequeno. Exemplo:

public class TestPrintln { 
    public strictfp static void main (String[] args) { 
        byte b1 = 100; 
        float f1 = 1.2f;
        System.out.println(f1*b1);
 
        float epsilon = 1.0e-06f;
        if (Math.abs(f1 - 120.0f) < epsilon)
            System.out.println("f1 * b1 = 120.0"); 
    } 
} 

Ou usando aritmética do tipo decimal usando a classe BigDecimal

[]s
Luca

Muito obrigado!
A ajuda de vocês foi preciosa.

Olhem que engraçado

Quando eu rodo o seguinte codigo:

public class Teste
{
    public static void main(String[] args)
    {
        double v1 = 96.050;
        double v2 = 96.000;
        double v3 = (v1-v2)*100 ;
        System.out.println(v3);

    }
}

Ele imprime: 4.999999999999716

Teria que usar o BigDecimal neste caso tb???

Acho que “BigDecimal” é um canhão para matar mosquito. Dificilmente um problema seu requer todo o poder do BigDecimal.

Muitas vezes calcular as coisas com long e centavos já é suficiente. O bom é se o Java tivesse um tipo “Currency”, que é uma forma disfarçada de calcular com long e centavos (no caso do Windows o tipo Currency trabalha com centésimos de centavos).

O que você precisa fazer é saber se virar com as picadas dos mosquitos. No seu caso, como 4.99999… é quase 5.0, e é mais importante visualizar o resultado correto, então basta arredondar na hora de visualizar.

Modifiquei o seu programa (requer Java 5.0):

import java.text.*;

public class TesteArredondamento
{
    /**
    * Deve imprimir:
    * 5,00000
    * 5,00000
    * (depende das configurações regionais de sua máquina.)
    */
    public static void main(String[] args)
    {
        double v1 = 96.050;
        double v2 = 96.000;
        double v3 = (v1-v2)*100 ;
        // printf é novo no Java 5.0. Estou mostrando apenas 5 casas depois da virgula.
        System.out.printf ("%.5f%n", v3);
        // DecimalFormat existe a partir do 1.3 - mostramos 5 casas depois da vírgula.
        System.out.println (new DecimalFormat ("#.00000").format (v3));
    }
}

Caro thingol

Deu certo aqui…eu to usando a 1.5
Imprimiu 5.00000 e 5.00000

Mas o problema eh prever que ia dar essas BOMBAS. Vc faz uma parada na fé que vai dar certinho, no caso 5.000… e a verdade dá 4.99997…eh problema isso…

No JAVA nao tem tipo um ROUND como no PL/SQL ou um FLOOR ou CEILING …sei la…alguma forma de arredondamento numerico???

Mas de qqer forma, valeu pela atenção

Abraço 8)

Bom, dê uma olhada no javadoc de java.lang.Math. Tem todas essas funções que você citou, e algumas mais.
Mas lembre-se: não adianta você arredondar dentro do seu programa (por mais que você tente, 0.1 não pode ser representado exatamente em binário). Você tem de fazer duas coisas:

  • Visualizar os resultados arredondados (como eu fiz);
  • E usar uma tolerância ao fazer comparações (como o Luca mostrou com essa história de “epsilon”).

Poutz…
Foi mal a vergonha que eu passei… :oops:
M
e esqueci completamente de java.lang.Math

Abração 8)