JavaRush /Blog Java /Random-ES /El dispositivo de los números reales.

El dispositivo de los números reales.

Publicado en el grupo Random-ES
¡Hola! En la conferencia de hoy hablaremos de números en Java, y concretamente de números reales. El dispositivo de los números reales - 1¡No entrar en pánico! :) No habrá dificultades matemáticas en la conferencia. Hablaremos de números reales exclusivamente desde nuestro punto de vista de "programador". Entonces, ¿qué son los “números reales”? Los números reales son números que tienen parte fraccionaria (que puede ser cero). Pueden ser positivos o negativos. A continuación se muestran algunos ejemplos: 15 56,22 0,0 1242342343445246 -232336,11 ¿Cómo funciona un número real? Muy sencillo: consta de una parte entera, una parte fraccionaria y un signo. Para los números positivos el signo normalmente no se indica explícitamente, pero para los números negativos sí se indica. Anteriormente, examinamos en detalle qué operaciones con números se pueden realizar en Java. Entre ellas había muchas operaciones matemáticas estándar: suma, resta, etc. También había algunas nuevas para usted: por ejemplo, el resto de la división. Pero, ¿cómo funciona exactamente trabajar con números dentro de una computadora? ¿De qué forma se almacenan en la memoria?

Almacenar números reales en la memoria

Creo que no será un descubrimiento para ti que los números pueden ser grandes y pequeños :) Se pueden comparar entre sí. Por ejemplo, el número 100 es menor que el número 423324. ¿Esto afecta el funcionamiento de la computadora y de nuestro programa? En realidad, . Cada número está representado en Java por un rango específico de valores :
Tipo Tamaño de la memoria (bits) Rango de valores
byte 8 bits -128 a 127
short 16 bits -32768 a 32767
char 16 bits entero sin signo que representa un carácter UTF-16 (letras y 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
Hoy hablaremos de los dos últimos tipos floaty double. Ambos realizan la misma tarea: representar números fraccionarios. También se les suele llamar “ números de coma flotante” . Recuerde este término para el futuro :) Por ejemplo, el número 2.3333 o 134.1212121212. Bastante extraño. Después de todo, ¿resulta que no hay diferencia entre estos dos tipos, ya que realizan la misma tarea? Pero hay una diferencia. Preste atención a la columna "tamaño en memoria" en la tabla anterior. Todos los números (y no solo los números, toda la información en general) se almacenan en la memoria de la computadora en forma de bits. Un bit es la unidad más pequeña de información. Es bastante simple. Cualquier bit es igual a 0 o 1. Y la palabra " bit " proviene del inglés " binary digit ", un número binario. Creo que probablemente hayas oído hablar de la existencia del sistema numérico binario en matemáticas. Cualquier número decimal con el que estemos familiarizados se puede representar como un conjunto de unos y ceros. Por ejemplo, el número 584,32 en binario se vería así: 100100100001010001111 . Cada uno y cero en este número es un bit separado. Ahora deberías tener más clara la diferencia entre tipos de datos. Por ejemplo, si creamos un número de tipo float, sólo tenemos 32 bits a nuestra disposición. Al crear un número, floatesta es exactamente la cantidad de espacio que se le asignará en la memoria de la computadora. Si queremos crear el número 123456789.65656565656565, en binario se verá así: 11101011011110011010001010110101000000 . Consta de 38 unos y ceros, es decir, se necesitan 38 bits para almacenarlo en la memoria. float¡Este número simplemente no “encajará” en el tipo ! Por tanto, el número 123456789 se puede representar como un tipo double. Para almacenarlo se asignan hasta 64 bits: ¡esto nos conviene! Por supuesto, el rango de valores también será adecuado. Para mayor comodidad, puedes pensar en un número como un pequeño cuadro con celdas. Si hay suficientes celdas para almacenar cada bit, entonces el tipo de datos se elige correctamente :) El dispositivo de los números reales - 2Por supuesto, las diferentes cantidades de memoria asignada también afectan el número en sí. Tenga en cuenta que los tipos floattienen doublediferentes rangos de valores. ¿Qué significa esto en la práctica? Un número doublepuede expresar mayor precisión que un número float. Los números de coma flotante de 32 bits (en Java este es exactamente el tipo float) tienen una precisión de aproximadamente 24 bits, es decir, unos 7 decimales. Y los números de 64 bits (en Java este es el tipo double) tienen una precisión de aproximadamente 53 bits, es decir, aproximadamente 16 decimales. Aquí hay un ejemplo que demuestra bien esta diferencia:
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);
   }
}
¿Qué deberíamos obtener aquí como resultado? Parecería que todo es bastante sencillo. Tenemos el número 0.0 y le sumamos 0.1111111111111111 7 veces seguidas. El resultado debería ser 0,7777777777777777. Pero creamos un número float. Su tamaño está limitado a 32 bits y, como dijimos anteriormente, es capaz de mostrar un número hasta aproximadamente el séptimo decimal. Por tanto, al final el resultado que obtengamos en la consola será diferente al que esperábamos:

0.7777778
El número parecía “cortado”. Ya sabes cómo se almacenan los datos en la memoria, en forma de bits, por lo que esto no debería sorprenderte. Está claro por qué sucedió esto: el resultado 0.7777777777777777 simplemente no cabía en los 32 bits que se nos asignaron, por lo que se truncó para caber en una variable de tipo float:) Podemos cambiar el tipo de variable doubleen nuestro ejemplo, y luego la final el resultado no se truncará:
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
Ya hay 16 decimales, el resultado “cabe” en 64 bits. Por cierto, ¿quizás has notado que en ambos casos los resultados no fueron del todo correctos? El cálculo se realizó con errores menores. Hablaremos de las razones de esto a continuación :) Ahora digamos algunas palabras sobre cómo se pueden comparar números entre sí.

Comparación de números reales

Ya tocamos parcialmente este tema en la última conferencia, cuando hablamos de operaciones de comparación. No volveremos a analizar operaciones como >, <, >=. <=En su lugar, veamos un ejemplo más interesante:
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);
   }
}
¿Qué número crees que se mostrará en la pantalla? La respuesta lógica sería la respuesta: el número 1. Empezamos a contar desde el número 0,0 y sucesivamente le sumamos 0,1 diez veces seguidas. Todo parece estar correcto, debería serlo. Intente ejecutar este código y la respuesta le sorprenderá enormemente :) Salida de la consola:

0.9999999999999999
Pero ¿por qué ocurrió un error en un ejemplo tan simple? O_o Aquí incluso un alumno de quinto grado podría responder correctamente fácilmente, pero el programa Java produjo un resultado inexacto. "Inexacto" es una palabra mejor aquí que "incorrecto". Todavía obtuvimos un número muy cercano a uno, y no solo un valor aleatorio :) Difiere del correcto literalmente en un milímetro. ¿Pero por qué? Quizás esto sea sólo un error puntual. ¿Quizás la computadora falló? Intentemos escribir otro ejemplo.
public class Main {

   public static void main(String[] args)  {

       //sumar 0.1 a cero once veces seguidas
       double f1 = 0.0;
       for (int i = 1; i <= 11; i++) {
           f1 += .1;
       }

       // Multiplica 0.1 por 11
       double f2 = 0.1 * 11;

       //debería ser el mismo - 1.1 en ambos casos
       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       // ¡Vamos a revisar!
       if (f1 == f2)
           System.out.println("¡f1 y f2 son iguales!");
       else
           System.out.println("¡f1 y f2 no ​​son iguales!");
   }
}
Salida de consola:

f1 = 1.0999999999999999
f2 = 1.1
f1 и f2 не равны!
Entonces, claramente no se trata de fallas en la computadora :) ¿Qué está pasando? Errores como estos están relacionados con la forma en que se representan los números en forma binaria en la memoria de la computadora. El hecho es que en el sistema binario es imposible representar con precisión el número 0,1 . Por cierto, el sistema decimal también tiene un problema similar: es imposible representar fracciones correctamente (y en lugar de ⅓ obtenemos 0,33333333333333..., que tampoco es el resultado correcto). Parecería una nimiedad: con tales cálculos la diferencia puede ser de cien milésima parte (0,00001) o incluso menos. Pero ¿qué pasa si todo el resultado de su Programa Muy Serio depende de esta comparación?
if (f1 == f2)
   System.out.println("Cohete vuela al espacio");
else
   System.out.println("Se cancela el lanzamiento, todos se van a casa");
Claramente esperábamos que los dos números fueran iguales, pero debido al diseño de la memoria interna, cancelamos el lanzamiento del cohete. El dispositivo de los números reales - 3Si es así, debemos decidir cómo comparar dos números de punto flotante para que el resultado de la comparación sea más... ummm... predecible. Entonces, ya hemos aprendido la regla número 1 al comparar números reales: nunca use ==números de punto flotante al comparar números reales. Ok, creo que ya son suficientes malos ejemplos :) ¡Veamos un buen ejemplo!
public class Main {

   public static void main(String[] args)  {

       final double threshold = 0.0001;

       //sumar 0.1 a cero once veces seguidas
       double f1 = .0;
       for (int i = 1; i <= 11; i++) {
           f1 += .1;
       }

       // Multiplica 0.1 por 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 y f2 son iguales");
       else
           System.out.println("f1 y f2 no ​​son iguales");
   }
}
Aquí esencialmente estamos haciendo lo mismo, pero cambiando la forma en que comparamos los números. Tenemos un número de "umbral" especial: 0,0001, una diezmilésima. Puede que sea diferente. Depende de qué tan precisa sea la comparación que necesite en un caso particular. Puedes hacerlo más grande o más pequeño. Usando el método, Math.abs()obtenemos el módulo de un número. El módulo es el valor de un número independientemente del signo. Por ejemplo, los números -5 y 5 tendrán el mismo módulo y serán iguales a 5. Restamos el segundo número del primero, y si el resultado resultante, independientemente del signo, es menor que el umbral que establecimos, entonces Nuestros números son iguales. En cualquier caso, son iguales al grado de precisión que establecimos usando nuestro “número umbral” , es decir, como mínimo son iguales hasta una diezmilésima. Este método de comparación le evitará el comportamiento inesperado que vimos en el caso de ==. Otra buena forma de comparar números reales es utilizar una clase especial BigDecimal. Esta clase fue creada específicamente para almacenar números muy grandes con una parte fraccionaria. A diferencia de doubley float, cuando se usa BigDecimalla suma, la resta y otras operaciones matemáticas se realizan no usando operadores ( +-, etc.), sino usando métodos. Así es como se verá en nuestro caso:
import java.math.BigDecimal;

public class Main {

   public static void main(String[] args)  {

       /*Crear dos objetos BigDecimal - cero y 0.1.
       Hacemos lo mismo que antes: sumamos 0,1 a cero 11 veces seguidas.
       En la clase BigDecimal, la suma se realiza mediante el método add () */
       BigDecimal f1 = new BigDecimal(0.0);
       BigDecimal pointOne = new BigDecimal(0.1);
       for (int i = 1; i <= 11; i++) {
           f1 = f1.add(pointOne);
       }

       /*Aquí tampoco ha cambiado nada: crear dos objetos BigDecimal
       y multiplicar 0,1 por 11
       En la clase BigDecimal, la multiplicación se realiza mediante el método multiplicar()*/
       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);

       /*Otra característica de BigDecimal es que los objetos numéricos deben compararse entre sí
       mediante el método especial compareTo()*/
       if (f1.compareTo(f2) == 0)
           System.out.println("f1 y f2 son iguales");
       else
           System.out.println("f1 y f2 no ​​son iguales");
   }
}
¿Qué tipo de salida de consola obtendremos?

f1 = 1.1000000000000000610622663543836097232997417449951171875
f2 = 1.1000000000000000610622663543836097232997417449951171875
f1 и f2 равны
Obtuvimos exactamente el resultado que esperábamos. ¡Y preste atención a qué tan precisos resultaron nuestros números y cuántos decimales caben en ellos! ¡Mucho más que en floate incluso en double! Recuerda la clase BigDecimalpara el futuro, definitivamente la necesitarás :) ¡Uf! La conferencia fue bastante larga, pero lo hiciste: ¡bien hecho! :) ¡Nos vemos en la próxima lección, futuro programador!
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION