Overflow dan Underflow di Java

1. Perkenalan

Dalam tutorial ini, kita akan melihat overflow dan underflow tipe data numerik di Java.

Kami tidak akan mendalami lebih dalam aspek yang lebih teoretis - kami hanya akan fokus pada saat itu terjadi di Java.

Pertama, kita akan melihat tipe data integer, lalu tipe data floating-point. Untuk keduanya, kita juga akan melihat bagaimana kita dapat mendeteksi ketika over- atau underflow terjadi.

2. Overflow dan Underflow

Sederhananya, overflow dan underflow terjadi ketika kita menetapkan nilai yang berada di luar rentang tipe data variabel yang dideklarasikan.

Jika nilai (absolut) terlalu besar, kami menyebutnya overflow, jika nilainya terlalu kecil, kami menyebutnya underflow.

Mari kita lihat contoh di mana kami mencoba untuk menetapkan nilai 101000 (a 1 dengan 1000 nol) ke variabel bertipe int atau double . Nilainya terlalu besar untuk variabel int atau double di Java, dan akan terjadi overflow.

Sebagai contoh kedua, katakanlah kita mencoba untuk menetapkan nilai 10-1000 (yang sangat dekat dengan 0) ke variabel berjenis double . Nilai ini terlalu kecil untuk variabel ganda di Jawa, dan akan ada aliran bawah.

Mari kita lihat apa yang terjadi di Java dalam kasus ini secara lebih detail.

3. Tipe Data Integer

Tipe data integer di Java adalah byte (8 bits), short (16 bits), int (32 bits), dan long (64 bits).

Di sini, kita akan fokus pada tipe data int . Perilaku yang sama berlaku untuk tipe data lainnya, kecuali nilai minimum dan maksimumnya berbeda.

Integer bertipe int di Java bisa negatif atau positif, yang artinya dengan 32 bit-nya, kita dapat memberikan nilai antara -231 ( -2147483648 ) dan 231-1 ( 2147483647 ).

Kelas pembungkus Integer mendefinisikan dua konstanta yang menyimpan nilai-nilai ini: Integer.MIN_VALUE dan Integer.MAX_VALUE .

3.1. Contoh

Apa yang akan terjadi jika kita mendefinisikan variabel m dengan tipe int dan mencoba untuk menetapkan nilai yang terlalu besar (misalnya, 21474836478 = MAX_VALUE + 1)?

Hasil yang mungkin dari tugas ini adalah nilai m tidak akan ditentukan atau akan ada kesalahan.

Keduanya adalah hasil yang valid; namun, di Jawa, nilai m adalah -2147483648 (nilai minimum). Di sisi lain, jika kita mencoba untuk menetapkan nilai -2147483649 ( = MIN_VALUE - 1 ), m akan menjadi 2147483647 (nilai maksimum). Perilaku ini disebut integer-wraparound.

Mari pertimbangkan cuplikan kode berikut untuk menggambarkan perilaku ini dengan lebih baik:

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

Kami akan mendapatkan output berikut, yang menunjukkan overflow:

2147483646 2147483647 -2147483648 -2147483647 

4. Penanganan Underflow dan Overflow Tipe Data Integer

Java tidak memunculkan pengecualian saat terjadi overflow; itulah mengapa sulit untuk menemukan kesalahan yang disebabkan oleh luapan. Kami juga tidak dapat langsung mengakses flag overflow, yang tersedia di sebagian besar CPU.

Namun, ada berbagai cara untuk menangani kemungkinan luapan. Mari kita lihat beberapa kemungkinan ini.

4.1. Gunakan Tipe Data Berbeda

Jika kita ingin mengizinkan nilai yang lebih besar dari 2147483647 (atau lebih kecil dari -2147483648 ), kita cukup menggunakan tipe data panjang atau BigInteger sebagai gantinya.

Meskipun variabel berjenis panjang juga dapat melimpah, nilai minimum dan maksimum jauh lebih besar dan mungkin cukup di sebagian besar situasi.

Rentang nilai BigInteger tidak dibatasi, kecuali dengan jumlah memori yang tersedia untuk JVM.

Mari kita lihat cara menulis ulang contoh di atas dengan BigInteger :

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

Kami akan melihat output berikut:

2147483647 2147483648 2147483649 2147483650

Seperti yang bisa kita lihat di output, tidak ada overflow di sini. Artikel kami BigDecimal dan BigInteger di Java mencakup BigInteger secara lebih rinci.

4.2. Lempar Pengecualian

Ada situasi di mana kami tidak ingin mengizinkan nilai yang lebih besar, kami juga tidak ingin terjadi luapan, dan sebagai gantinya kami ingin membuat pengecualian.

Pada Java 8, kita dapat menggunakan metode untuk operasi aritmatika yang tepat. Mari kita lihat contoh dulu:

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

Metode statis addExact () melakukan penambahan normal, tetapi memunculkan pengecualian jika operasi menghasilkan overflow atau 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)

Selain addExact () , paket Matematika di Java 8 menyediakan metode yang tepat untuk semua operasi aritmatika. Lihat dokumentasi Java untuk daftar semua metode ini.

Selain itu, ada metode konversi yang tepat, yang memberikan pengecualian jika ada luapan selama konversi ke tipe data lain.

Untuk konversi dari long menjadi 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);

Yang tampaknya merupakan kontradiksi dengan pernyataan pertama kami.

8. Kesimpulan

Dalam artikel ini, kita melihat apa yang over- dan underflow, bagaimana hal itu bisa terjadi di Java, dan apa perbedaan antara tipe data integer dan floating-point.

Kami juga melihat bagaimana kami dapat mendeteksi over- dan underflow selama eksekusi program.

Seperti biasa, kode sumber lengkap tersedia di Github.