Überlauf und Unterlauf in Java

1. Einleitung

In diesem Tutorial werden wir uns mit dem Über- und Unterlauf numerischer Datentypen in Java befassen.

Wir werden nicht tiefer in die theoretischeren Aspekte eintauchen - wir werden uns nur darauf konzentrieren, wann dies in Java geschieht.

Zuerst betrachten wir ganzzahlige Datentypen, dann Gleitkomma-Datentypen. Für beide werden wir auch sehen, wie wir erkennen können, wenn ein Über- oder Unterlauf auftritt.

2. Überlauf und Unterlauf

Einfach ausgedrückt, Überlauf und Unterlauf treten auf, wenn wir einen Wert zuweisen, der außerhalb des Bereichs des deklarierten Datentyps der Variablen liegt.

Wenn der (absolute) Wert zu groß ist, nennen wir ihn Überlauf, wenn der Wert zu klein ist, nennen wir ihn Unterlauf.

Schauen wir uns ein Beispiel an, in dem wir versuchen , einer Variablen vom Typ int oder double den Wert 101000 (eine 1 mit 1000 Nullen) zuzuweisen . Der Wert ist in Java für eine int- oder double- Variable zu groß , und es kommt zu einem Überlauf.

Nehmen wir als zweites Beispiel an, wir versuchen , einer Variablen vom Typ double den Wert 10-1000 (der sehr nahe bei 0 liegt) zuzuweisen . Dieser Wert ist zu klein für eine Doppelvariable in Java, und es kommt zu einem Unterlauf.

Lassen Sie uns genauer sehen, was in Java in diesen Fällen passiert.

3. Ganzzahlige Datentypen

Die ganzzahligen Datentypen in Java sind Byte (8 Bit), Short (16 Bit), Int (32 Bit) und Long (64 Bit).

Hier konzentrieren wir uns auf den Datentyp int . Das gleiche Verhalten gilt für die anderen Datentypen, außer dass sich die Minimal- und Maximalwerte unterscheiden.

Eine Ganzzahl vom Typ int in Java kann negativ oder positiv sein, was bedeutet, dass wir mit ihren 32 Bit Werte zwischen -231 ( -2147483648 ) und 231-1 ( 2147483647 ) zuweisen können .

Die Wrapper-Klasse Integer definiert zwei Konstanten, die diese Werte enthalten: Integer.MIN_VALUE und Integer.MAX_VALUE .

3.1. Beispiel

Was passiert, wenn wir eine Variable m vom Typ int definieren und versuchen, einen zu großen Wert zuzuweisen (z. B. 21474836478 = MAX_VALUE + 1)?

Ein mögliches Ergebnis dieser Zuweisung ist, dass der Wert von m undefiniert ist oder dass ein Fehler auftritt.

Beides sind gültige Ergebnisse; In Java ist der Wert von m jedoch -2147483648 (der Mindestwert). Auf der anderen Seite, wenn wir einen Wert von -2147483649 (zuweisen versuchen = MIN_VALUE - 1 ), m wird 2147483647 (der Maximalwert). Dieses Verhalten wird als Integer-Wraparound bezeichnet.

Betrachten wir das folgende Codefragment, um dieses Verhalten besser zu veranschaulichen:

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

Wir erhalten die folgende Ausgabe, die den Überlauf demonstriert:

2147483646 2147483647 -2147483648 -2147483647 

4. Umgang mit Unter- und Überlauf ganzzahliger Datentypen

Java löst keine Ausnahme aus, wenn ein Überlauf auftritt. Aus diesem Grund kann es schwierig sein, Fehler zu finden, die sich aus einem Überlauf ergeben. Wir können auch nicht direkt auf das Überlauf-Flag zugreifen, das in den meisten CPUs verfügbar ist.

Es gibt jedoch verschiedene Möglichkeiten, um mit einem möglichen Überlauf umzugehen. Schauen wir uns einige dieser Möglichkeiten an.

4.1. Verwenden Sie einen anderen Datentyp

Wenn wir Werte zulassen möchten, die größer als 2147483647 (oder kleiner als -2147483648 ) sind, können wir stattdessen einfach den langen Datentyp oder eine BigInteger verwenden.

Obwohl Variablen vom Typ long ebenfalls überlaufen können, sind die Minimal- und Maximalwerte viel größer und in den meisten Situationen wahrscheinlich ausreichend.

Der Wertebereich von BigInteger ist nur durch die für die JVM verfügbare Speichermenge eingeschränkt.

Mal sehen, wie wir unser obiges Beispiel mit BigInteger umschreiben :

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

Wir werden die folgende Ausgabe sehen:

2147483647 2147483648 2147483649 2147483650

Wie wir in der Ausgabe sehen können, gibt es hier keinen Überlauf. Unser Artikel BigDecimal und BigInteger in Java behandelt BigInteger ausführlicher.

4.2. Wirf eine Ausnahme

Es gibt Situationen, in denen wir keine größeren Werte zulassen möchten oder einen Überlauf möchten und stattdessen eine Ausnahme auslösen möchten.

Ab Java 8 können wir die Methoden für exakte arithmetische Operationen verwenden. Schauen wir uns zuerst ein Beispiel an:

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

Die statische Methode addExact () führt eine normale Addition durch, löst jedoch eine Ausnahme aus, wenn die Operation zu einem Überlauf oder Unterlauf führt:

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)

Zusätzlich zu addExact () bietet das Math- Paket in Java 8 entsprechende genaue Methoden für alle arithmetischen Operationen. In der Java-Dokumentation finden Sie eine Liste aller dieser Methoden.

Darüber hinaus gibt es genaue Konvertierungsmethoden, die eine Ausnahme auslösen, wenn während der Konvertierung in einen anderen Datentyp ein Überlauf auftritt.

Für die Konvertierung von einem Long zu einem Int :

public static int toIntExact(long a)

Und für die Konvertierung von BigInteger zu einem int oder long :

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

4.3. Vor Java 8

Die genauen arithmetischen Methoden wurden zu Java 8 hinzugefügt. Wenn wir eine frühere Version verwenden, können wir diese Methoden einfach selbst erstellen. Eine Möglichkeit besteht darin, dieselbe Methode wie in Java 8 zu implementieren:

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. Nicht ganzzahlige Datentypen

Die nicht ganzzahligen Typen float und double verhalten sich bei arithmetischen Operationen nicht wie die ganzzahligen Datentypen.

Ein Unterschied besteht darin, dass arithmetische Operationen mit Gleitkommazahlen zu einem NaN führen können . Wir haben einen speziellen Artikel über NaN in Java, daher werden wir in diesem Artikel nicht weiter darauf eingehen. Darüber hinaus gibt es im Math- Paket keine genauen arithmetischen Methoden wie addExact oder multiplyExact für nicht ganzzahlige Typen .

Java folgt den IEEE - Standard für Gleitkomma-Arithmetik (IEEE 754) für seinen Schwimmer und doppelte Datentypen. Dieser Standard ist die Grundlage für die Art und Weise, wie Java mit Über- und Unterlauf von Gleitkommazahlen umgeht.

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

Das scheint ein Widerspruch zu unserer ersten Behauptung zu sein.

8. Fazit

In diesem Artikel haben wir gesehen, was Über- und Unterlauf ist, wie er in Java auftreten kann und was der Unterschied zwischen dem Datentyp Ganzzahl und Gleitkomma ist.

Wir haben auch gesehen, wie wir während der Programmausführung einen Über- und Unterlauf erkennen können.

Wie üblich ist der vollständige Quellcode auf Github verfügbar.