Overflow e underflow in Java

1. Introduzione

In questo tutorial, esamineremo l'overflow e l'underflow dei tipi di dati numerici in Java.

Non approfondiremo gli aspetti più teorici, ci concentreremo solo su quando accadrà in Java.

Per prima cosa, esamineremo i tipi di dati interi, quindi i tipi di dati a virgola mobile. Per entrambi, vedremo anche come possiamo rilevare quando si verifica un overflow o un underflow.

2. Overflow e Underflow

In poche parole, l'overflow e l'underflow si verificano quando si assegna un valore che non rientra nell'intervallo del tipo di dati dichiarato della variabile.

Se il valore (assoluto) è troppo grande, lo chiamiamo overflow, se il valore è troppo piccolo, lo chiamiamo underflow.

Diamo un'occhiata a un esempio in cui tentiamo di assegnare il valore 101000 (un 1 con 1000 zeri) a una variabile di tipo int o double . Il valore è troppo grande per una variabile int o double in Java e si verificherà un overflow.

Come secondo esempio, supponiamo di provare ad assegnare il valore 10-1000 (che è molto vicino a 0) a una variabile di tipo double . Questo valore è troppo piccolo per una doppia variabile in Java e ci sarà un underflow.

Vediamo più nel dettaglio cosa succede in Java in questi casi.

3. Tipi di dati interi

I tipi di dati interi in Java sono byte (8 bit), short (16 bit), int (32 bit) e long (64 bit).

Qui ci concentreremo sul tipo di dati int . Lo stesso comportamento si applica agli altri tipi di dati, tranne per il fatto che i valori minimo e massimo differiscono.

Un numero intero di tipo int in Java può essere negativo o positivo, il che significa che con i suoi 32 bit possiamo assegnare valori compresi tra -231 ( -2147483648 ) e 231-1 ( 2147483647 ).

La classe wrapper Integer definisce due costanti che contengono questi valori: Integer.MIN_VALUE e Integer.MAX_VALUE .

3.1. Esempio

Cosa succederà se definiamo una variabile m di tipo int e tentiamo di assegnare un valore troppo grande (ad esempio, 21474836478 = MAX_VALUE + 1)?

Un possibile risultato di questa assegnazione è che il valore di m non sarà definito o che ci sarà un errore.

Entrambi sono risultati validi; tuttavia, in Java, il valore di m sarà -2147483648 (il valore minimo). D'altra parte, se tentiamo di assegnare un valore di -2147483649 ( = MIN_VALUE - 1 ), m sarà 2147483647 (il valore massimo). Questo comportamento è chiamato integer-wraparound.

Consideriamo il seguente frammento di codice per illustrare meglio questo comportamento:

int value = Integer.MAX_VALUE-1; for(int i = 0; i < 4; i++, value++) { System.out.println(value); }

Otterremo il seguente output, che dimostra l'overflow:

2147483646 2147483647 -2147483648 -2147483647 

4. Gestione dell'underflow e dell'overflow di tipi di dati interi

Java non genera un'eccezione quando si verifica un overflow; ecco perché può essere difficile trovare errori derivanti da un overflow. Né possiamo accedere direttamente al flag di overflow, disponibile nella maggior parte delle CPU.

Tuttavia, esistono vari modi per gestire un possibile overflow. Diamo un'occhiata a molte di queste possibilità.

4.1. Usa un diverso tipo di dati

Se vogliamo consentire valori maggiori di 2147483647 (o minori di -2147483648 ), possiamo semplicemente utilizzare il tipo di dati lungo o un BigInteger .

Sebbene anche le variabili di tipo long possano traboccare, i valori minimo e massimo sono molto maggiori e sono probabilmente sufficienti nella maggior parte delle situazioni.

L'intervallo di valori di BigInteger non è limitato, ad eccezione della quantità di memoria disponibile per JVM.

Vediamo come riscrivere il nostro esempio sopra 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); }

Vedremo il seguente output:

2147483647 2147483648 2147483649 2147483650

Come possiamo vedere nell'output, non c'è overflow qui. Il nostro articolo BigDecimal e BigInteger in Java copre BigInteger in modo più dettagliato.

4.2. Lancia un'eccezione

Ci sono situazioni in cui non vogliamo consentire valori più grandi, né vogliamo che si verifichi un overflow e vogliamo invece lanciare un'eccezione.

A partire da Java 8, possiamo utilizzare i metodi per operazioni aritmetiche esatte. Diamo prima un'occhiata a un esempio:

int value = Integer.MAX_VALUE-1; for(int i = 0; i < 4; i++) { System.out.println(value); value = Math.addExact(value, 1); }

Il metodo statico addExact () esegue un'aggiunta normale, ma genera un'eccezione se l'operazione risulta in un overflow o underflow:

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)

Oltre a addExact () , il pacchetto Math in Java 8 fornisce i metodi esatti corrispondenti per tutte le operazioni aritmetiche. Vedere la documentazione Java per un elenco di tutti questi metodi.

Inoltre, esistono metodi di conversione esatti, che generano un'eccezione se si verifica un overflow durante la conversione in un altro tipo di dati.

Per la conversione da long a int :

public static int toIntExact(long a)

And for the conversion from BigInteger to an int or long:

BigInteger largeValue = BigInteger.TEN; long longValue = largeValue.longValueExact(); int intValue = largeValue.intValueExact();

4.3. Before Java 8

The exact arithmetic methods were added to Java 8. If we use an earlier version, we can simply create these methods ourselves. One option to do so is to implement the same method as in 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. Non-Integer Data Types

The non-integer types float and double do not behave in the same way as the integer data types when it comes to arithmetic operations.

One difference is that arithmetic operations on floating-point numbers can result in a NaN. We have a dedicated article on NaN in Java, so we won't look further into that in this article. Furthermore, there are no exact arithmetic methods such as addExact or multiplyExact for non-integer types in the Math package.

Java follows the IEEE Standard for Floating-Point Arithmetic (IEEE 754) for its float and double data types. This standard is the basis for the way that Java handles over- and underflow of floating-point numbers.

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);

Il che sembra essere in contraddizione con la nostra prima affermazione.

8. Conclusione

In questo articolo, abbiamo visto cos'è l'overflow e l'underflow, come può verificarsi in Java e qual è la differenza tra i tipi di dati integer e floating-point.

Abbiamo anche visto come rilevare l'overflow e l'underflow durante l'esecuzione del programma.

Come al solito, il codice sorgente completo è disponibile su Github.