1. Introducción
En este tutorial, veremos el desbordamiento y el subdesbordamiento de los tipos de datos numéricos en Java.
No profundizaremos en los aspectos más teóricos, solo nos centraremos en cuándo sucede en Java.
Primero, veremos los tipos de datos enteros, luego los tipos de datos de punto flotante. Para ambos, también veremos cómo podemos detectar cuándo ocurre un desbordamiento o un desbordamiento.
2. Desbordamiento y desbordamiento
En pocas palabras, el desbordamiento y el subdesbordamiento ocurren cuando asignamos un valor que está fuera del rango del tipo de datos declarado de la variable.
Si el valor (absoluto) es demasiado grande, lo llamamos desbordamiento, si el valor es demasiado pequeño, lo llamamos subdesbordamiento.
Veamos un ejemplo en el que intentamos asignar el valor 101000 (un 1 con 1000 ceros) a una variable de tipo int o double . El valor es demasiado grande para una variable int o double en Java, y habrá un desbordamiento.
Como segundo ejemplo, digamos que intentamos asignar el valor 10-1000 (que es muy cercano a 0) a una variable de tipo double . Este valor es demasiado pequeño para una variable doble en Java y habrá un desbordamiento.
Veamos qué sucede en Java en estos casos con más detalle.
3. Tipos de datos enteros
Los tipos de datos enteros en Java son byte (8 bits), corto (16 bits), int (32 bits) y largo (64 bits).
Aquí, nos centraremos en el tipo de datos int . El mismo comportamiento se aplica a los otros tipos de datos, excepto que los valores mínimo y máximo difieren.
Un entero de tipo int en Java puede ser negativo o positivo, lo que significa que con sus 32 bits podemos asignar valores entre -231 ( -2147483648 ) y 231-1 ( 2147483647 ).
La clase contenedora Integer define dos constantes que contienen estos valores: Integer.MIN_VALUE e Integer.MAX_VALUE .
3.1. Ejemplo
¿Qué pasará si definimos una variable m de tipo int e intentamos asignar un valor que es demasiado grande (por ejemplo, 21474836478 = MAX_VALUE + 1)?
Un posible resultado de esta asignación es que el valor de m no estará definido o que habrá un error.
Ambos son resultados válidos; sin embargo, en Java, el valor de m será -2147483648 (el valor mínimo). Por otro lado, si intentamos asignar un valor de -2147483649 ( = MIN_VALUE - 1 ), m será 2147483647 (el valor máximo). Este comportamiento se llama entero-envolvente.
Consideremos el siguiente fragmento de código para ilustrar mejor este comportamiento:
int value = Integer.MAX_VALUE-1; for(int i = 0; i < 4; i++, value++) { System.out.println(value); }
Obtendremos el siguiente resultado, que demuestra el desbordamiento:
2147483646 2147483647 -2147483648 -2147483647
4. Manejo de subdesbordamiento y desbordamiento de tipos de datos enteros
Java no lanza una excepción cuando ocurre un desbordamiento; es por eso que puede ser difícil encontrar errores resultantes de un desbordamiento. Tampoco podemos acceder directamente al indicador de desbordamiento, que está disponible en la mayoría de las CPU.
Sin embargo, hay varias formas de manejar un posible desbordamiento. Veamos varias de estas posibilidades.
4.1. Utilice un tipo de datos diferente
Si queremos permitir valores mayores que 2147483647 (o menores que -2147483648 ), simplemente podemos usar el tipo de datos largo o un BigInteger en su lugar.
Aunque las variables de tipo long también pueden desbordarse, los valores mínimo y máximo son mucho mayores y probablemente sean suficientes en la mayoría de situaciones.
El rango de valores de BigInteger no está restringido, excepto por la cantidad de memoria disponible para la JVM.
Veamos cómo reescribir nuestro ejemplo anterior con BigInteger :
BigInteger largeValue = new BigInteger(Integer.MAX_VALUE + ""); for(int i = 0; i < 4; i++) { System.out.println(largeValue); largeValue = largeValue.add(BigInteger.ONE); }
Veremos el siguiente resultado:
2147483647 2147483648 2147483649 2147483650
Como podemos ver en la salida, no hay desbordamiento aquí. Nuestro artículo BigDecimal y BigInteger en Java cubre BigInteger con más detalle.
4.2. Lanzar una excepción
Hay situaciones en las que no queremos permitir valores más grandes, ni queremos que se produzca un desbordamiento, y queremos lanzar una excepción.
A partir de Java 8, podemos usar los métodos para operaciones aritméticas exactas. Primero veamos un ejemplo:
int value = Integer.MAX_VALUE-1; for(int i = 0; i < 4; i++) { System.out.println(value); value = Math.addExact(value, 1); }
El método estático addExact () realiza una adición normal, pero genera una excepción si la operación da como resultado un desbordamiento o subdesbordamiento:
2147483646 2147483647 Exception in thread "main" java.lang.ArithmeticException: integer overflow at java.lang.Math.addExact(Math.java:790) at baeldung.underoverflow.OverUnderflow.main(OverUnderflow.java:115)
Además de addExact () , el paquete Math en Java 8 proporciona los métodos exactos correspondientes para todas las operaciones aritméticas. Consulte la documentación de Java para obtener una lista de todos estos métodos.
Además, existen métodos de conversión exactos, que generan una excepción si hay un desbordamiento durante la conversión a otro tipo de datos.
Para la conversión de long a int :
public static int toIntExact(long a)
Y para la conversión de BigInteger a int o long :
BigInteger largeValue = BigInteger.TEN; long longValue = largeValue.longValueExact(); int intValue = largeValue.intValueExact();
4.3. Antes de Java 8
Los métodos aritméticos exactos se agregaron a Java 8. Si usamos una versión anterior, simplemente podemos crear estos métodos nosotros mismos. Una opción para hacerlo es implementar el mismo método que en Java 8:
public static int addExact(int x, int y) { int r = x + y; if (((x ^ r) & (y ^ r)) < 0) { throw new ArithmeticException("int overflow"); } return r; }
5. Tipos de datos no enteros
Los tipos no enteros float y double no se comportan de la misma forma que los tipos de datos enteros cuando se trata de operaciones aritméticas.
Una diferencia es que las operaciones aritméticas en números de punto flotante pueden resultar en un NaN . Tenemos un artículo dedicado a NaN en Java, por lo que no profundizaremos en eso en este artículo. Además, no existen métodos aritméticos exactos como addExact o multiplyExact para tipos no enteros en el paquete Math .
Java sigue el estándar IEEE para aritmética de punto flotante (IEEE 754) para sus tipos de datos flotantes y dobles . Este estándar es la base de la forma en que Java maneja el desbordamiento y el desbordamiento de números de punto flotante.
In the below sections, we'll focus on the over- and underflow of the double data type and what we can do to handle the situations in which they occur.
5.1. Overflow
As for the integer data types, we might expect that:
assertTrue(Double.MAX_VALUE + 1 == Double.MIN_VALUE);
However, that is not the case for floating-point variables. The following is true:
assertTrue(Double.MAX_VALUE + 1 == Double.MAX_VALUE);
This is because a double value has only a limited number of significant bits. If we increase the value of a large double value by only one, we do not change any of the significant bits. Therefore, the value stays the same.
If we increase the value of our variable such that we increase one of the significant bits of the variable, the variable will have the value INFINITY:
assertTrue(Double.MAX_VALUE * 2 == Double.POSITIVE_INFINITY);
and NEGATIVE_INFINITY for negative values:
assertTrue(Double.MAX_VALUE * -2 == Double.NEGATIVE_INFINITY);
We can see that, unlike for integers, there's no wraparound, but two different possible outcomes of the overflow: the value stays the same, or we get one of the special values, POSITIVE_INFINITY or NEGATIVE_INFINITY.
5.2. Underflow
There are two constants defined for the minimum values of a double value: MIN_VALUE (4.9e-324) and MIN_NORMAL (2.2250738585072014E-308).
IEEE Standard for Floating-Point Arithmetic (IEEE 754) explains the details for the difference between those in more detail.
Let's focus on why we need a minimum value for floating-point numbers at all.
A double value cannot be arbitrarily small as we only have a limited number of bits to represent the value.
The chapter about Types, Values, and Variables in the Java SE language specification describes how floating-point types are represented. The minimum exponent for the binary representation of a double is given as -1074. That means the smallest positive value a double can have is Math.pow(2, -1074), which is equal to 4.9e-324.
As a consequence, the precision of a double in Java does not support values between 0 and 4.9e-324, or between -4.9e-324 and 0 for negative values.
So what happens if we attempt to assign a too-small value to a variable of type double? Let's look at an example:
for(int i = 1073; i <= 1076; i++) { System.out.println("2^" + i + " = " + Math.pow(2, -i)); }
With output:
2^1073 = 1.0E-323 2^1074 = 4.9E-324 2^1075 = 0.0 2^1076 = 0.0
We see that if we assign a value that's too small, we get an underflow, and the resulting value is 0.0 (positive zero).
Similarly, for negative values, an underflow will result in a value of -0.0 (negative zero).
6. Detecting Underflow and Overflow of Floating-Point Data Types
As overflow will result in either positive or negative infinity, and underflow in a positive or negative zero, we do not need exact arithmetic methods like for the integer data types. Instead, we can check for these special constants to detect over- and underflow.
If we want to throw an exception in this situation, we can implement a helper method. Let's look at how that can look for the exponentiation:
public static double powExact(double base, double exponent) { if(base == 0.0) { return 0.0; } double result = Math.pow(base, exponent); if(result == Double.POSITIVE_INFINITY ) { throw new ArithmeticException("Double overflow resulting in POSITIVE_INFINITY"); } else if(result == Double.NEGATIVE_INFINITY) { throw new ArithmeticException("Double overflow resulting in NEGATIVE_INFINITY"); } else if(Double.compare(-0.0f, result) == 0) { throw new ArithmeticException("Double overflow resulting in negative zero"); } else if(Double.compare(+0.0f, result) == 0) { throw new ArithmeticException("Double overflow resulting in positive zero"); } return result; }
In this method, we need to use the method Double.compare(). The normal comparison operators (< and >) do not distinguish between positive and negative zero.
7. Positive and Negative Zero
Finally, let's look at an example that shows why we need to be careful when working with positive and negative zero and infinity.
Let's define a couple of variables to demonstrate:
double a = +0f; double b = -0f;
Because positive and negative 0 are considered equal:
assertTrue(a == b);
Whereas positive and negative infinity are considered different:
assertTrue(1/a == Double.POSITIVE_INFINITY); assertTrue(1/b == Double.NEGATIVE_INFINITY);
However, the following assertion is correct:
assertTrue(1/a != 1/b);
Lo que parece ser una contradicción con nuestra primera afirmación.
8. Conclusión
En este artículo, vimos qué es el desbordamiento y el desbordamiento, cómo puede ocurrir en Java y cuál es la diferencia entre los tipos de datos enteros y de punto flotante.
También vimos cómo podíamos detectar el exceso y el defecto durante la ejecución del programa.
Como de costumbre, el código fuente completo está disponible en Github.