Débordement et sous-débordement en Java

1. Introduction

Dans ce didacticiel, nous examinerons le débordement et le sous-débordement des types de données numériques en Java.

Nous n'allons pas approfondir les aspects plus théoriques - nous nous concentrerons uniquement sur le moment où cela se produit en Java.

Tout d'abord, nous examinerons les types de données entiers, puis les types de données à virgule flottante. Pour les deux, nous verrons également comment nous pouvons détecter les débordements excessifs ou insuffisants.

2. Débordement et sous-débordement

En termes simples, un dépassement de capacité et un dépassement inférieur se produisent lorsque nous attribuons une valeur qui est hors de portée du type de données déclaré de la variable.

Si la valeur (absolue) est trop grande, nous l'appelons overflow, si la valeur est trop petite, nous l'appelons underflow.

Regardons un exemple où nous essayons d'attribuer la valeur 101000 (un 1 avec 1000 zéros) à une variable de type int ou double . La valeur est trop grande pour une variable int ou double en Java, et il y aura un débordement.

Comme deuxième exemple, disons que nous essayons d'attribuer la valeur 10-1000 (qui est très proche de 0) à une variable de type double . Cette valeur est trop petite pour une variable double en Java, et il y aura un dépassement inférieur.

Voyons ce qui se passe en Java dans ces cas plus en détail.

3. Types de données entiers

Les types de données entiers en Java sont octets (8 bits), courts (16 bits), int (32 bits) et longs (64 bits).

Ici, nous allons nous concentrer sur le type de données int . Le même comportement s'applique aux autres types de données, sauf que les valeurs minimale et maximale diffèrent.

Un entier de type int en Java peut être négatif ou positif, ce qui signifie qu'avec ses 32 bits, nous pouvons attribuer des valeurs comprises entre -231 ( -2147483648 ) et 231-1 ( 2147483647 ).

La classe wrapper Integer définit deux constantes contenant ces valeurs: Integer.MIN_VALUE et Integer.MAX_VALUE .

3.1. Exemple

Que se passera-t-il si nous définissons une variable m de type int et tentons d'attribuer une valeur trop grande (par exemple, 21474836478 = MAX_VALUE + 1)?

Un résultat possible de cette affectation est que la valeur de m sera indéfinie ou qu'il y aura une erreur.

Les deux sont des résultats valables; cependant, en Java, la valeur de m sera -2147483648 (la valeur minimale). En revanche, si nous essayons d'attribuer une valeur de -2147483649 ( = MIN_VALUE - 1 ), m sera 2147483647 (la valeur maximale). Ce comportement est appelé enveloppement d'entier.

Examinons l'extrait de code suivant pour mieux illustrer ce comportement:

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

Nous obtiendrons la sortie suivante, qui démontre le débordement:

2147483646 2147483647 -2147483648 -2147483647 

4. Gestion du dépassement inférieur et du dépassement des types de données entiers

Java ne lève pas d'exception lorsqu'un débordement se produit; c'est pourquoi il peut être difficile de trouver des erreurs résultant d'un débordement. Nous ne pouvons pas non plus accéder directement à l'indicateur de débordement, qui est disponible dans la plupart des processeurs.

Cependant, il existe différentes manières de gérer un éventuel débordement. Examinons plusieurs de ces possibilités.

4.1. Utiliser un type de données différent

Si nous voulons autoriser des valeurs supérieures à 2147483647 (ou inférieures à -2147483648 ), nous pouvons simplement utiliser le type de données long ou un BigInteger à la place.

Bien que les variables de type long puissent également déborder, les valeurs minimum et maximum sont beaucoup plus grandes et sont probablement suffisantes dans la plupart des situations.

La plage de valeurs de BigInteger n'est pas limitée, sauf par la quantité de mémoire disponible pour la JVM.

Voyons comment réécrire notre exemple ci-dessus avec BigInteger :

BigInteger largeValue = new BigInteger(Integer.MAX_VALUE + ""); for(int i = 0; i < 4; i++) { System.out.println(largeValue); largeValue = largeValue.add(BigInteger.ONE); }

Nous verrons la sortie suivante:

2147483647 2147483648 2147483649 2147483650

Comme nous pouvons le voir dans la sortie, il n'y a pas de débordement ici. Notre article BigDecimal et BigInteger en Java couvre BigInteger plus en détail.

4.2. Lancer une exception

Il y a des situations où nous ne voulons pas autoriser des valeurs plus grandes, ni ne voulons qu'un débordement se produise, et nous voulons plutôt lever une exception.

Depuis Java 8, nous pouvons utiliser les méthodes pour des opérations arithmétiques exactes. Regardons d'abord un exemple:

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

La méthode statique addExact () effectue un ajout normal, mais lève une exception si l'opération entraîne un débordement ou un dépassement inférieur:

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)

En plus de addExact () , le package Math de Java 8 fournit les méthodes exactes correspondantes pour toutes les opérations arithmétiques. Consultez la documentation Java pour une liste de toutes ces méthodes.

De plus, il existe des méthodes de conversion exactes, qui lèvent une exception en cas de dépassement lors de la conversion vers un autre type de données.

Pour la conversion d'un long en un entier :

public static int toIntExact(long a)

Et pour la conversion de BigInteger en un int ou long :

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

4.3. Avant Java 8

Les méthodes arithmétiques exactes ont été ajoutées à Java 8. Si nous utilisons une version antérieure, nous pouvons simplement créer ces méthodes nous-mêmes. Une option pour ce faire est d'implémenter la même méthode que dans 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. Types de données non entiers

Les types non entiers float et double ne se comportent pas de la même manière que les types de données entiers lorsqu'il s'agit d'opérations arithmétiques.

Une différence est que les opérations arithmétiques sur les nombres à virgule flottante peuvent aboutir à un NaN . Nous avons un article dédié sur NaN en Java, nous n'allons donc pas approfondir cela dans cet article. De plus, il n'y a pas de méthodes arithmétiques exactes telles que addExact ou multiplyExact pour les types non entiers dans le package Math .

Java suit la norme IEEE pour l'arithmétique à virgule flottante (IEEE 754) pour ses types de données float et double . Cette norme est à la base de la manière dont Java gère les débordements et débordements de nombres à virgule flottante.

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

Ce qui semble être une contradiction avec notre première affirmation.

8. Conclusion

Dans cet article, nous avons vu ce qu'il y a de débordement et de sous-dépassement, comment cela peut se produire en Java et quelle est la différence entre les types de données entier et flottant.

Nous avons également vu comment nous pouvions détecter les débordements et sous-débordements pendant l'exécution du programme.

Comme d'habitude, le code source complet est disponible sur Github.