"95% of folks out there are completely clueless about floating-point." - James Gosling (o «pai» do Java)
Primeira nota: se puder evitar usar os tipos float e double, use antes os tipos inteiros, mais rápidos e mais fiáveis. Como nem sempre isso é possível, convém conhecermos algumas das características e comportamentos dos tipos ditos reais.
Este tutorial tenta apresentar alguns desses problemas. Este texto é baseado nas referências indicadas abaixo. Para maiores detalhes é aconselhável lê-las com atenção.
O Java usa um subconjunto do padrão IEEE 754 que define uma representação binária de números reais em vírgula flutuante.
Um float é representado em 32 bits e cada combinação desses bits representa um número. A sua definição é a seguinte:
(-1)s . m . 2 (e-127)
onde
s
(o bit 31) representa o sinal que define se o número é positivo ou negativoe
é representado por 8 bits (bits 23 a 30) e é o expoente (a menos de 127)m
é representado por 23 bits (bits 0 a 22) e é a mantissa (sendo normalizada para estar entre 0.5 e 1)Por exemplo, o valor 0.085 é representado por 00111101101011100001010001111011
binário: 0 01111011 01011100001010001111011 decimal: 0 123 3019899 s e m
o que resulta em (o 223 é para normalizar o valor m):
2e-127 (1 + m / 223) = 2-4(1 + 3019899/8388608) = 11408507/134217728
Se fizermos as contas, o resultado desta fração é 0.085000000894069671630859375.
O que concluímos deste pequeno exemplo? Não fomos capazes de armazenar o número 0.085! O que guardamos é a melhor aproximação possível dados os 32 bits disponíveis. Assim, há de imediato um erro associado à representação deste número.
O tipo double tem uma representação similar mas usa 64 bits. Isto implica que há 500 milhões de doubles entre cada dois valores consecutivos de tipo float. O expoente tem 11 bits (a menos de 1023) e uma mantissa de 52 bits. Mas o problema da aproximação não fica resolvido, a única coisa que obtemos é uma melhor aproximação.
Mesmo o número 0.1 não tem uma representação exata em binário. Uma décima em binário é representado pela sequência infinita
0.0001100110011001100...
e qualquer espaço reservado no respectivo tipo real dá-nos apenas uma aproximação. Se guardássemos os primeiros 20 bits, o número realmente descrito seria 0.09999847412109375. Se fossem os primeiros 40 bits, guardaríamos realmente o número 0.0999999999994543031789362430572509765625.
Podem fazer as vossas experiências de conversão nesta página.
O problema da aproximação não ocorre apenas no sistema binário. Por exemplo, o número 1/3 não tem uma representação exacta em base 10 (1/3 = 0.333333333...), enquanto que em base 3 o número 1/3 é representado por 0.1.
Podemos fazer estas experiências no Java:
import java.math.BigDecimal; ... BigDecimal bd1 = new BigDecimal(0.1); System.out.println(bd1);
produz:
0.1000000000000000055511151231257827021181583404541015625
de notar que os valores BigDecimal são imutáveis. Para somar dois BigDecimals fazer, por exemplo:
System.out.println( bd1.add(new BigDecimal(0.05)) ); // 0.15000000000000000832667268468867405317723751068115234375
Para comparar não usar o equals() mas sim o compareTo().
Esta classe tem também um construtor estrito onde fornecemos os números em formato de string para evitar os problemas de aproximação:
System.out.println(new BigDecimal("0.1")); // 0.1
Mais info sobre a classe aqui.
Podem usar este pedaço de código para explorar a representação dos números do Java.
double d = 0.1; System.out.print("IEEE 754 representation of " + d + " = "); System.out.println(Long.toBinaryString(Double.doubleToLongBits(d))); // IEEE 754 representation of 0.1 = 11111110111001100110011001100110011001100110011001100110011010
Os tipos reais ainda possuem valores especiais para representar os infinitos negativo e positivo, bem como NaN que representa uma ausência de número (por exemplo, produzido quando é avaliado 0/0 ou a raiz quadrada de um número negativo). Estes valores existem para fechar as operações do float ou double, i.e., para não haver possibilidade de criar um valor que não seja representado pelo tipo usando apenas as suas operações. O custo deste fecho é um conjunto complicado de regras de excepção para tratar estes valores (cf. classe Double).
Não se deve usar o operador == para comparar floats ou doubles:
double x = 10; double y = Math.sqrt(x); System.out.println( x == y*y ? "igual" : "diferente"); // diferente
ouch!
Temos de dar alguma tolerância:
final double EPSILON = 1e-6; System.out.println( Math.abs(x-y*y)<EPSILON ? "igual" : "diferente"); // igual
Outro ponto importante é que os floats e doubles tem um intervalo bastante pequeno para guardar números:
float f = 16777216; System.out.println("f = " + f + ", f+1 = " + f+1); // f = 1.6777216E7, f+1 = 1.6777216E71
argh!!
Como é possível que o erro seja tão catastrófico? 16777216 é 224 e a mantissa tem apenas 23 bits provocando um overflow.
O mesmo ocorre para os doubles que têm 53 bits para a mantissa:
double dm = 9007199254740992L; // 2^53 System.out.println((dm+1) - dm); // 0.0
Este género de problemas também ocorre com números 'normais' quando multiplicados por quantidades muito pequenas:
System.out.println(1.0 + " : " + (1.0 + 0.5*Double.MIN_VALUE)); // 1.0 : 1.0
Existe uma diferença entre o termo precisão (precision) e correcção (accuracy).
Seja o número 3.14000001. A sua precisão é de oito casas decimais. Mas a sua correcção depende do contexto. Se for uma estimativa de pi, a sua correcção é de apenas duas casas decimais. Se o número for uma estimativa de 31401/10000 a sua correcção é de três casas decimais.
Como vimos nos exemplos anteriores, os valores representados nos tipos float e double têm normalmente muitas casas de precisão, mas as casas decimais correctas é um assunto completamente diferente.
Mas mesmo a grande precisão dos tipos do Java pode não ser suficiente. Pode acontecer que dois números diferentes sejam considerados iguais porque ambos têm a mesma aproximação.
O código
System.out.println("0.9200000000000001 = " + new BigDecimal(0.9200000000000001));
System.out.println("0.9200000000000002 = " + new BigDecimal(0.9200000000000002));
boolean b = (0.9200000000000001 == 0.9200000000000002);
System.out.println("0.9200000000000001 == 0.9200000000000002: " + b);
produz o seguinte resultado:
0.9200000000000001 = 0.9200000000000001509903313490212894976139068603515625
0.9200000000000002 = 0.9200000000000001509903313490212894976139068603515625
0.9200000000000001 == 0.9200000000000002: true
Precisamos de ter muito cuidado quando comparamos dois valores muito próximos.
Mesmo a soma de valores simples traz-nos surpresas desagradáveis:
System.out.println("0.1 = " + 0.1);
System.out.println("0.1 = " + new BigDecimal(0.1));
System.out.println("0.2 = " + new BigDecimal(0.2));
System.out.println("0.3 = " + new BigDecimal(0.3));
System.out.println("0.4 = " + new BigDecimal(0.4));
System.out.println("0.1 + 0.2 = " + new BigDecimal(0.1 + 0.2));
System.out.println("0.1 + 0.3 = " + new BigDecimal(0.1 + 0.3));
boolean b1 = (0.1 + 0.2 == 0.3);
System.out.println("0.1 + 0.2 == 0.3: " + b1);
produz:
0.1 = 0.1
0.1 = 0.1000000000000000055511151231257827021181583404541015625
0.2 = 0.200000000000000011102230246251565404236316680908203125
0.3 = 0.299999999999999988897769753748434595763683319091796875
0.4 = 0.40000000000000002220446049250313080847263336181640625
0.1 + 0.2 = 0.3000000000000000444089209850062616169452667236328125
0.1 + 0.3 = 0.40000000000000002220446049250313080847263336181640625
0.1 + 0.2 == 0.3: false
Lidar correctamente com estes problemas está muito para além do âmbito deste guião, sendo um assunto complexo tratado pela disciplina científica da Análise Numérica.
A soma e a subtração são mais perigosas que o produto e a divisão. Isto porque podemos ter o azar de subtrair dois números muito similares, o que pode implicar a perda total de precisão!
Este próximo exemplo calcula a derivada do seno usando a definição (f(x+h) - f(x)) / h quando h tende para zero, e compara com o valor verdadeiro (a derivada do seno é o coseno):
for (int i = 1; i < 20; ++i) { double h = Math.pow(10.0, -i); System.out.println( String.format("%1.15f", (Math.sin(1.0+h) - Math.sin(1.0))/h)); } System.out.println(Math.cos(1.0) + "[True result]");
Os dígitos corretos estão sublinhados (assumindo arredondamentos):
0,497363752535389
0,536085981011869
0,539881480360327
0,540260231418621
0,540298098505865
0,540301885121330
0,540302264040449
0,540302302898255
0,540302358409406
0,540302247387103
0,540301137164079
0,540345546085064
0,539568389967826
0,544009282066327
0,555111512312578
0,000000000000000
0,000000000000000
0,000000000000000
0,000000000000000
0.5403023058681398 [True Result]
A correção melhora à medida que h fica mais pequeno, como seria de se esperar. Mas a partir de h = 10-8, a correção começa a decair devido à subtração. A partir de h=10-16 a perda de correção é total (dado que 1+h==h da perspectiva do Java).
Se tiver que realizar muitas multiplicações pode correr o risco de perder toda a correção por overflow ou underflow. Eg: calcular 200! / (196! 4!) pelo método usual (o resultado certo é 64684950):
static long factorial(int n) { long result=1; for (int i=1; i<=n; i++) result += i; return result; }long result = factorial(200) / (factorial(196)*factorial(4)); System.out.println(result);
não funciona! Este código produz o valor zero.
A forma tradicional de resolver este problema é transformar os valores em logaritmos e somá-los. No fim, aplica-se o exponencial para obter o resultado:
static double logFactorial(int n) { double result=0.0; for (int i=1; i<=n; i++) result += Math.log((double)i); return result; }double result2 = Math.exp(logFactorial(200) - (logFactorial(196)+logFactorial(4))); System.out.println((long)result2);
produz 64684950. Mais info aqui.
O erro absoluto é a diferença entre o valor exacto e a estimativa ou aproximação. Por exemplo, se o valor exacto é 0.1 e a aproximação é 0.09, o erro absoluto é 0.1 - 0.09 = 0.01.
O erro relativo é o erro absoluto a dividir pelo valor exacto. No exemplo anterior, o erro relativo seria 0.01 / 0.1 = 0.1 ou 10%.
Uma forma de colmatar, embora não resolvendo, os problemas de aproximação -- quando lidamos com valores muito pequenos -- é usar o erro relativo para comparar dois valores em vez de usar o erro absoluto.
Um exemplo de uso. Seja o método de Newton para calcular a raiz quadrada de um número. O seguinte código tenta implementar esse algoritmo:
double c = 4.0; double EPSILON = 0.0; double t = c; while (t*t - c > EPSILON) t = (c/t + t) / 2.0; System.out.print(t + " e' raiz de " + c);
Se experimentarmos com alguns valores, o programa parece estar correcto:
2.0 e' raiz de 4.0
1.414213562373095 e' raiz de 2
3.162277660168379 e' raiz de 10
Mas se tentarmos com c = 20.0, o ciclo não termina! Ocorre um problema de aproximação porque o ciclo começa a lidar com números tão pequenos que a sua soma não produz qualquer efeito (como, por exemplo, 1.0 + e = 1.0). No caso do double esse valor é 2-53.
double d = 1.0 / (1L << 53); // cf. operador << boolean b = (1.0 == (1.0 + d)); System.out.println("1 + 2^-53 == 1: " + b);
produz:
1 + 2^-53 == 1: true
Voltando ao exemplo, se convertermos o código para não comparar o erro absoluto
t*t - c > c*EPSILON
mas sim o erro relativo
Math.abs(t*t - c) > c*EPSILON
podemos resolver este problema em específico.
double c = 20.0; double EPSILON = 1e-15; double t = c; while (Math.abs(t*t - c) > c*EPSILON) t = (c/t + t) / 2.0; System.out.print(t + " e' raiz de " + c);
produz:
4.47213595499958 e' raiz de 20
Se puder, use inteiros e longs em vez de float e doubles.
Se puder, use double em vez de float
Use o erro relativo em vez de erro absoluto para comparar floats ou doubles. Não usar o ==.
Se precisa de representar valores monetários, como cêntimos, use uma variável inteira para o total em cêntimos. Quando for imprimir coloque a vírgula após os dois primeiros dígitos. Pode fazer o mesmo para outras unidades, como a temperatura.
Cuidado quando uma operação sobre doubles é repetida muitas vezes. Se tem de somar, por exemplo, vários 0.01 a uma variável double d, em vez de
d += 0.01
use uma variável auxiliar inteira:
i++;
d = i * 0.01;
As classes BigDecimal e BigInteger fornecem valores fraccionais e inteiros de precisão arbitrária.
Cuidado a subtrair valores muito similares e a somar valores de magnitudes muito diferentes.