Mistério flutuante

8 respostas
J

Galera,

preciso de ajuda para um problema estranho em um sistema.

-> Ambiente:

Compilador: 1.4.2_01
JVM versão 1.4.2_01
Eclipse - versão: 3.0.1

-> Testei também em outra máquina com a mesma configuração e com a JVM 1.5.0_02

-> Programa de teste para reproduzir o problema:

public class Unica {

    public static void main(String[] args) {
        byte contI;
        float[] valorCategoria = {12.7319f,18.252f,0,0};
        int[] economias = {35,15,0,0};
		for (contI = 0; contI < 4; contI++) {
			if (economias[contI] != 0) {
			    arredondadorMagico = new BigDecimal(valorCategoria[contI]);
				valorCategoria[contI] = arredondadorMagico.setScale(5, BigDecimal.ROUND_DOWN).floatValue();
				valorCategoria[contI] *= economias[contI];
			}
			System.out.println("valorCategoria[contI]:" + valorCategoria[contI]);
		}
    }
}

-> Durante o debug, o eclipse não acusa valores nas demais casas decimais além das definidas na inicialização

-> Resultados (com ou sem a utilização do arredondadorMagico):

valorCategoria[0]:445.61652
valorCategoria[1]:273.78
valorCategoria[2]:0.0
valorCategoria[3]:0.0

-> Minha Pergunta: De onde vem o 2 na quinta casa decimal do valorCategoria[0]? :shock:

Muito obrigado.

8 Respostas

Luca

Olá

Resposta: da dificuldade de representar um número real como um binário. Por este motivo quando a gente usa float ou double na hora de imprimir a gente arredonda para o número de casa decimais desejado.

Também é preciso cautela quando se compara números reais. Eles nunca vão ser iguais até a última casa. Assim a gente testa a igualdade a menos de uma valor muito pequeno.

Por fim uma recomendação. Ao representar números reais, prefira sempre usar doubles ao invés de floats.

[]s
Luca

T

Pegando carona na explicação do Luca:

Luca:
Olá

Resposta: da dificuldade de representar um número real como um binário. Por este motivo quando a gente usa float ou double na hora de imprimir a gente arredonda para o número de casas decimais desejado.

Por exemplo: quando você divide 1 por 3, o resultado é 0,3333333…
mas não dá para representar isso porque é uma “dízima periódica” (valor com infinitos dígitos).
Então você precisa truncar ou arredondar o número.
Se você arredondar para 3 casas, por exemplo, o resultado seria 0,333.
Quanto é 3 * (1 / 3) se você arredondar para 3 casas?
O resultado é 0,999, não 1.
Internamente o computador arredonda para mais casas, mas não consegue guardar uma quantidade infinita delas. (Não adianta comprar mais memória…)
Além disso ele tem mais um problema. No caso da base 10, se os fatores do divisor forem apenas 2 e/ou 5, a divisão não dá uma dízima periódica. Mas no caso da base 2, que é o que o computador usa internamente, apenas os divisores que são potências de dois não vão dar problemas de “dízimas periódicas”. Por isso, no computador, 10 * (1 / 10) não é exatamente igual a 1.

Portanto se você tiver alguma conta que precisa saber se 3 * (1 / 3) == 1, isso não funciona porque você não deu nenhuma tolerância.
Esse valor muito pequeno (às vezes chamado “epsilon”) de tolerância é o que você deve usar para evitar problemas. Por exemplo:

double um = 1.0;
double tres_tercos = 3.0 * (1.0 / 3.0);
if (Math.abs (um - tres_tercos) <= 1E-9) { // estou usando o "epsilon" de 0,000000001
     System.out.println ("um é numericamente igual a tres_tercos");
}


Por fim uma recomendação. Ao representar números reais, prefira sempre usar doubles ao invés de floats.

[]s
Luca

E se seu inglês é bom e você tem alguma noção de matemática, você pode tentar ler isto: What Every Computer Scientist Should Know About Floating-Point Arithmetic (
http://docs.sun.com/source/806-3568/ncg_goldberg.html )

J

Em primeiro lugar, muito obrigado pelas respostas.
Vou tentar entender aquele documento da Sun, thingol. Valeu.

Entendo o q vcs explicaram, mas ainda resta uma dúvida:

Inicializei a variável float com quantidade específica de casas decimais, ou seja, o seu valor não é resultado de uma operação matemática.

float[] valorCategoria = {12.7319f,18.252f,0,0};

Se, internamente, o compilador representou o valor de uma forma tal que provocou o aumento de casas decimais, esse novo valor não deveria ser exibido durante o processo de debug no eclipse? Por que isso não acontece?

Depois, multipliquei por um inteiro.

valorCategoria[contI] *= economias[contI];

Penso que não seria possível aumentar o número de casas decimais em uma operação desse tipo. E, para o resultado obtido, volto ao caso acima de por que o valor não foi exibido pelo eclipse.

Obrigado.

T

Tomei a liberdade de corrigir o seu programa. Ainda não está do jeito correto (que seria usar MathContext), mas pelo menos dá o resultado que você espera.

import java.util.*;
import java.math.*;

public class Unica {

    public static void main(String[] args) {
        byte contI;
        //         float[] valorCategoria = {12.7319f,18.252f,0,0};
        //         double[] valorCategoria = {12.7319,18.252,0,0};
        BigDecimal[] valorCategoria = {
            new BigDecimal ("12.7319"),
            new BigDecimal ("18.252"),
            new BigDecimal ("0"),
            new BigDecimal ("0"), 
            new BigDecimal ("0")
        };
        
        int[] economias = {
            35,
            15,
            0,
            0
        };
        for (contI = 0; contI < economias.length; contI++) {
            if (economias[contI] != 0) {
                valorCategoria[contI].setScale(5, BigDecimal.ROUND_DOWN);
                valorCategoria[contI] = valorCategoria[contI].multiply (BigDecimal.valueOf(economias[contI]));
            }
            System.out.println("valorCategoria[contI]:" + valorCategoria[contI]);
        }
    }
}
J

Valeu demais thingol e Luca. Aprendi bastante.

O meu problema é que eu estou participando da construção de um sistema para rodar no Pocket PC com uma JVM que se chama CrEmE, que não é J2ME e sim Java 1.1.8. Por isso, tenho q manter a compatibilidade do código com essa versão e evitar a criação de objetos, preferindo utilizar os tipos primitivos para otimizar o consumo de memória e o processamento.

thingol, aquele documento é muito bom. Estou tentando entendê-lo.

Um grande abraço a todos.

T

Para evitar problemas, mr. Jubas:

Que tal fazer o seguinte: eu sei que BigDecimal é super-pesado, nem sei se está disponível no Creme. E você começou a usar métodos que só estão disponíveis no JDK 5.0, por isso é que “fui na onda” e mostrei uma correção do seu programa com

Em vez disso, trabalhe com long (não sei se isso está disponível no Creme), e escale os números com 5 casas. Por exemplo: 12.0 = 1200000L
12.7319 = 1273190L

35 * 12.7319 = 35 * 1273190 = 44561650 = 445.61650
35 * 445.61650 = 35 * 44561650 = 222808250 = 2228.08250

Só que isso é meio chato e talvez seja meio lento (e os números não podem ser muito grandes), mas acho que números de ponto flutuante são ainda mais lentos no PocketPC (não são por hardware, como é o caso do Pentium).

matheuscechito

eu comecei a aprender java essa semana, e hj realizei a seguinte conta:

0.1 + 0.1 + 0.1 = 0.3000000000004

esse 4 na última casa é pelo mesmo motivo explicado anteriormente??? msm fazendo uma soma???

David

Sim… 0.1 na base 10 é uma dízima periódica em binário, por isso esse problema de exatidão.

Criado 13 de maio de 2005
Ultima resposta 10 de mai. de 2006
Respostas 8
Participantes 5