JavaRush /Blogue Java /Random-PT /O dispositivo dos números reais

O dispositivo dos números reais

Publicado no grupo Random-PT
Olá! Na palestra de hoje falaremos sobre números em Java, e especificamente sobre números reais. O dispositivo dos números reais - 1Não entrar em pânico! :) Não haverá dificuldades matemáticas na aula. Falaremos sobre números reais exclusivamente do ponto de vista do nosso “programador”. Então, o que são “números reais”? Números reais são números que possuem uma parte fracionária (que pode ser zero). Eles podem ser positivos ou negativos. Aqui estão alguns exemplos: 15 56,22 0,0 1242342343445246 -232336,11 Como funciona um número real? Muito simples: consiste em uma parte inteira, uma parte fracionária e um sinal. Para números positivos o sinal geralmente não é indicado explicitamente, mas para números negativos é indicado. Anteriormente, examinamos detalhadamente quais operações com números podem ser executadas em Java. Entre elas estavam muitas operações matemáticas padrão - adição, subtração, etc. Também havia algumas novas para você: por exemplo, o resto da divisão. Mas como exatamente funciona o trabalho com números dentro de um computador? De que forma eles são armazenados na memória?

Armazenando números reais na memória

Acho que não será uma descoberta para você que os números podem ser grandes e pequenos :) Eles podem ser comparados entre si. Por exemplo, o número 100 é menor que o número 423324. Isso afeta o funcionamento do computador e do nosso programa? Na verdade sim . Cada número é representado em Java por um intervalo específico de valores :
Tipo Tamanho da memória (bits) Faixa de valores
byte 8 bits -128 a 127
short 16 bits -32768 a 32767
char 16 bits inteiro sem sinal que representa um caractere UTF-16 (letras e números)
int 32 bits de -2147483648 a 2147483647
long 64 bits de -9223372036854775808 a 9223372036854775807
float 32 bits de 2 -149 a (2-2 -23 )*2 127
double 64 bits de 2 -1074 a (2-2 -52 )*2 1023
Hoje falaremos sobre os dois últimos tipos - floate double. Ambos realizam a mesma tarefa – representar números fracionários. Eles também são frequentemente chamados de “ números de ponto flutuante” . Lembre-se deste termo para o futuro :) Por exemplo, o número 2.3333 ou 134.1212121212. Muito estranho. Afinal, acontece que não há diferença entre esses dois tipos, já que realizam a mesma tarefa? Mas há uma diferença. Preste atenção na coluna “tamanho na memória” na tabela acima. Todos os números (e não apenas números - todas as informações em geral) são armazenados na memória do computador na forma de bits. Um bit é a menor unidade de informação. É muito simples. Qualquer bit é igual a 0 ou 1. E a própria palavra “ bit ” vem do inglês “ dígito binário ” - um número binário. Acho que você provavelmente já ouviu falar da existência do sistema numérico binário na matemática. Qualquer número decimal com o qual estamos familiarizados pode ser representado como um conjunto de uns e zeros. Por exemplo, o número 584,32 em binário ficaria assim: 100100100001010001111 . Cada um e zero neste número são um bit separado. Agora você deve estar mais claro sobre a diferença entre os tipos de dados. Por exemplo, se criarmos um número do tipo float, teremos apenas 32 bits à nossa disposição. Ao criar um número, floaté exatamente quanto espaço será alocado para ele na memória do computador. Se quisermos criar o número 123456789.65656565656565, em binário ficará assim: 11101011011110011010001010110101000000 . É composto por 38 uns e zeros, ou seja, são necessários 38 bits para armazená-lo na memória. floatEste número simplesmente não “caberá” no tipo ! Portanto, o número 123456789 pode ser representado como um tipo double. Até 64 bits são alocados para armazená-lo: isso nos convém! Claro, a faixa de valores também será adequada. Por conveniência, você pode pensar em um número como uma pequena caixa com células. Se houver células suficientes para armazenar cada bit, o tipo de dados será escolhido corretamente :) O dispositivo dos números reais - 2É claro que diferentes quantidades de memória alocada também afetam o próprio número. Observe que os tipos floattêm doublediferentes intervalos de valores. O que isso significa na prática? Um número doublepode expressar maior precisão do que um número float. Números de ponto flutuante de 32 bits (em Java esse é exatamente o tipo float) possuem precisão de aproximadamente 24 bits, ou seja, cerca de 7 casas decimais. E os números de 64 bits (em Java esse é o tipo double) possuem precisão de aproximadamente 53 bits, ou seja, aproximadamente 16 casas decimais. Aqui está um exemplo que demonstra bem essa diferença:
public class Main {

   public static void main(String[] args)  {

       float f = 0.0f;
       for (int i=1; i <= 7; i++) {
           f += 0.1111111111111111;
       }

       System.out.println(f);
   }
}
O que devemos chegar aqui como resultado? Parece que tudo é bastante simples. Temos o número 0,0 e adicionamos 0,1111111111111111 a ele 7 vezes seguidas. O resultado deve ser 0,7777777777777777. Mas criamos um número float. Seu tamanho é limitado a 32 bits e, como dissemos anteriormente, é capaz de exibir um número até aproximadamente a 7ª casa decimal. Portanto, no final, o resultado que obteremos no console será diferente do que esperávamos:

0.7777778
O número parecia estar “cortado”. Você já sabe como os dados são armazenados na memória - na forma de bits, então isso não deve surpreendê-lo. Está claro por que isso aconteceu: o resultado 0,7777777777777777 simplesmente não cabia nos 32 bits alocados para nós, então foi truncado para caber em uma variável de tipo float:) Podemos alterar o tipo da variável para doubleem nosso exemplo, e então o final o resultado não será truncado:
public class Main {

   public static void main(String[] args)  {

       double f = 0.0;
       for (int i=1; i <= 7; i++) {
           f += 0.1111111111111111;
       }

       System.out.println(f);
   }
}

0.7777777777777779
Já são 16 casas decimais, o resultado “cabe” em 64 bits. Aliás, talvez você tenha notado que em ambos os casos os resultados não foram totalmente corretos? O cálculo foi feito com pequenos erros. Falaremos sobre as razões para isso abaixo :) Agora vamos dizer algumas palavras sobre como você pode comparar números entre si.

Comparação de números reais

Já tocamos parcialmente nesse assunto na palestra passada, quando falamos sobre operações de comparação. Não reanalisaremos operações como >, <, >=. <=Vejamos um exemplo mais interessante:
public class Main {

   public static void main(String[] args)  {

       double f = 0.0;
       for (int i=1; i <= 10; i++) {
           f += 0.1;
       }

       System.out.println(f);
   }
}
Qual número você acha que será exibido na tela? A resposta lógica seria a resposta: o número 1. Começamos a contar a partir do número 0,0 e adicionamos sucessivamente 0,1 a ele dez vezes seguidas. Tudo parece estar correto, deveria ser um. Tente executar este código e a resposta irá surpreendê-lo :) Saída do console:

0.9999999999999999
Mas por que ocorreu um erro em um exemplo tão simples? O_o Aqui até mesmo um aluno da quinta série poderia facilmente responder corretamente, mas o programa Java produziu um resultado impreciso. “Inexato” é uma palavra melhor aqui do que “incorreto”. Ainda obtivemos um número muito próximo de um, e não apenas um valor aleatório :) Ele difere do correto literalmente em um milímetro. Mas por que? Talvez este seja apenas um erro único. Talvez o computador tenha travado? Vamos tentar escrever outro exemplo.
public class Main {

   public static void main(String[] args)  {

       //add 0.1 to zero eleven times in a row
       double f1 = 0.0;
       for (int i = 1; i <= 11; i++) {
           f1 += .1;
       }

       // Multiply 0.1 by 11
       double f2 = 0.1 * 11;

       //should be the same - 1.1 in both cases
       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       // Let's check!
       if (f1 == f2)
           System.out.println("f1 and f2 are equal!");
       else
           System.out.println("f1 and f2 are not equal!");
   }
}
Saída do console:

f1 = 1.0999999999999999
f2 = 1.1
f1 и f2 не равны!
Então, claramente não se trata de falhas no computador :) O que está acontecendo? Esses tipos de erros estão relacionados à forma como os números são representados em formato binário na memória do computador. O fato é que no sistema binário é impossível representar com precisão o número 0,1 . Aliás, o sistema decimal também tem um problema semelhante: é impossível representar frações corretamente (e em vez de ⅓ obtemos 0,33333333333333..., o que também não é exatamente o resultado correto). Pareceria uma bagatela: com esses cálculos, a diferença pode ser de cem milésimas partes (0,00001) ou até menos. Mas e se todo o resultado do seu Programa Muito Sério depender dessa comparação?
if (f1 == f2)
   System.out.println("Rocket flies into space");
else
   System.out.println("The launch is canceled, everyone goes home");
Obviamente esperávamos que os dois números fossem iguais, mas devido ao design da memória interna, cancelamos o lançamento do foguete. O dispositivo dos números reais - 3Nesse caso, precisamos decidir como comparar dois números de ponto flutuante para que o resultado da comparação seja mais... ummm... previsível. Portanto, já aprendemos a regra nº 1 ao comparar números reais: nunca use ==números de ponto flutuante ao comparar números reais. Ok, acho que já bastam de maus exemplos :) Vejamos um bom exemplo!
public class Main {

   public static void main(String[] args)  {

       final double threshold = 0.0001;

       //add 0.1 to zero eleven times in a row
       double f1 = .0;
       for (int i = 1; i <= 11; i++) {
           f1 += .1;
       }

       // Multiply 0.1 by 11
       double f2 = .1 * 11;

       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       if (Math.abs(f1 - f2) < threshold)
           System.out.println("f1 and f2 are equal");
       else
           System.out.println("f1 and f2 are not equal");
   }
}
Aqui estamos essencialmente fazendo a mesma coisa, mas mudando a forma como comparamos os números. Temos um número de “limiar” especial - 0,0001, um décimo de milésimo. Pode ser diferente. Depende de quão precisa é a comparação necessária em um caso específico. Você pode torná-lo maior ou menor. Usando o método, Math.abs()obtemos o módulo de um número. O módulo é o valor de um número independentemente do sinal. Por exemplo, os números -5 e 5 terão o mesmo módulo e serão iguais a 5. Subtraímos o segundo número do primeiro, e se o resultado resultante, independentemente do sinal, for menor que o limite que definimos, então nossos números são iguais. Em qualquer caso, são iguais ao grau de precisão que estabelecemos através do nosso “número limite” , ou seja, no mínimo são iguais a um décimo de milésimo. Este método de comparação irá salvá-lo do comportamento inesperado que vimos no caso do ==. Outra boa maneira de comparar números reais é usar uma classe especial BigDecimal. Esta classe foi criada especificamente para armazenar números muito grandes com uma parte fracionária. Ao contrário de doublee float, ao usar BigDecimaladição, subtração e outras operações matemáticas são realizadas não usando operadores ( +-, etc.), mas usando métodos. Será assim no nosso caso:
import java.math.BigDecimal;

public class Main {

   public static void main(String[] args)  {

       /*Create two BigDecimal objects - zero and 0.1.
       We do the same thing as before - add 0.1 to zero 11 times in a row
       In the BigDecimal class, addition is done using the add () method */
       BigDecimal f1 = new BigDecimal(0.0);
       BigDecimal pointOne = new BigDecimal(0.1);
       for (int i = 1; i <= 11; i++) {
           f1 = f1.add(pointOne);
       }

       /*Nothing has changed here either: create two BigDecimal objects
       and multiply 0.1 by 11
       In the BigDecimal class, multiplication is done using the multiply() method*/
       BigDecimal f2 = new BigDecimal(0.1);
       BigDecimal eleven = new BigDecimal(11);
       f2 = f2.multiply(eleven);

       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       /*Another feature of BigDecimal is that number objects need to be compared with each other
       using the special compareTo() method*/
       if (f1.compareTo(f2) == 0)
           System.out.println("f1 and f2 are equal");
       else
           System.out.println("f1 and f2 are not equal");
   }
}
Que tipo de saída do console obteremos?

f1 = 1.1000000000000000610622663543836097232997417449951171875
f2 = 1.1000000000000000610622663543836097232997417449951171875
f1 и f2 равны
Obtivemos exatamente o resultado que esperávamos. E preste atenção na precisão dos nossos números e em quantas casas decimais cabem neles! Muito mais do que em floate até em double! Lembre-se da aula BigDecimalpara o futuro, com certeza você vai precisar :) Ufa! A palestra foi bem longa, mas você conseguiu: muito bem! :) Até a próxima lição, futuro programador!
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION