Overflow dan Underflow di Java

1. Pengenalan

Dalam tutorial ini, kita akan melihat limpahan dan aliran masuk jenis data berangka di Java.

Kami tidak akan menyelami lebih mendalam aspek yang lebih teoritis - kami hanya akan menumpukan perhatian ketika ia berlaku di Jawa.

Pertama, kita akan melihat jenis data integer, kemudian pada jenis data floating-point. Untuk kedua-duanya, kami juga akan melihat bagaimana kami dapat mengesan bila berlaku overflow atau underflow.

2. Overflow dan Underflow

Ringkasnya, overflow dan underflow berlaku apabila kita memberikan nilai yang berada di luar jangkauan jenis data yang diisytiharkan dari pemboleh ubah.

Sekiranya nilai (mutlak) terlalu besar, kami memanggilnya limpahan, jika nilainya terlalu kecil, kami memanggilnya underflow.

Mari kita lihat contoh di mana kita cuba menetapkan nilai 101000 ( 1 dengan 1000 sifar) kepada pemboleh ubah jenis int atau ganda . Nilainya terlalu besar untuk pemboleh ubah int atau ganda di Java, dan akan ada limpahan.

Sebagai contoh kedua, katakan kita cuba memberikan nilai 10-1000 (yang sangat dekat dengan 0) kepada pemboleh ubah jenis ganda . Nilai ini terlalu kecil untuk pemboleh ubah berganda di Java, dan akan ada aliran masuk bawah.

Mari lihat apa yang berlaku di Java dalam kes-kes ini dengan lebih terperinci.

3. Jenis Data Integer

Jenis data integer di Java ialah bait (8 bit), pendek (16 bit), int (32 bit), dan panjang (64 bit).

Di sini, kita akan memberi tumpuan kepada jenis data int . Tingkah laku yang sama berlaku untuk jenis data lain, kecuali bahawa nilai minimum dan maksimum berbeza.

Integer jenis int di Jawa boleh menjadi negatif atau positif, yang bermaksud dengan 32 bit, kita boleh menetapkan nilai antara -231 ( -2147483648 ) dan 231-1 ( 2147483647 ).

Kelas pembungkus Integer menentukan dua pemalar yang memegang nilai-nilai ini: Integer.MIN_VALUE dan Integer.MAX_VALUE .

3.1. Contohnya

Apa yang akan berlaku sekiranya kita menentukan pemboleh ubah m jenis int dan berusaha memberikan nilai yang terlalu besar (mis., 21474836478 = MAX_VALUE + 1)?

Kemungkinan hasil tugasan ini adalah bahawa nilai m tidak akan ditentukan atau akan berlaku ralat.

Kedua-duanya adalah hasil yang sah; namun, di Java, nilai m akan menjadi -2147483648 (nilai minimum). Sebaliknya, jika kita berusaha memberikan nilai -2147483649 ( = MIN_VALUE - 1 ), m adalah 2147483647 (nilai maksimum). Tingkah laku ini disebut sebagai integer-wraparound.

Mari pertimbangkan coretan kod berikut untuk menggambarkan tingkah laku ini dengan lebih baik:

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

Kami akan mendapat output berikut, yang menunjukkan limpahan:

2147483646 2147483647 -2147483648 -2147483647 

4. Mengendalikan Underflow dan Overflow of Integer Data Type

Java tidak membuang pengecualian apabila berlaku limpahan; sebab itulah sukar untuk mencari ralat yang disebabkan oleh limpahan. Kita juga tidak dapat mengakses bendera limpahan secara langsung, yang terdapat di kebanyakan CPU.

Walau bagaimanapun, terdapat pelbagai cara untuk menangani kemungkinan limpahan. Mari lihat beberapa kemungkinan ini.

4.1. Gunakan Jenis Data yang berbeza

Sekiranya kita ingin membenarkan nilai lebih besar dari 2147483647 (atau lebih kecil dari -2147483648 ), kita hanya boleh menggunakan jenis data panjang atau BigInteger sebagai gantinya.

Walaupun pemboleh ubah jenis panjang juga dapat meluap, nilai minimum dan maksimum jauh lebih besar dan mungkin mencukupi dalam kebanyakan situasi.

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

Mari lihat bagaimana menulis semula 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 kita lihat dalam output, tidak ada limpahan di sini. Artikel kami BigDecimal dan BigInteger di Java merangkumi BigInteger dengan lebih terperinci.

4.2. Lontarkan Pengecualian

Terdapat situasi di mana kita tidak ingin membiarkan nilai yang lebih besar, kita juga tidak mahu limpahan berlaku, dan kita ingin membuang pengecualian.

Pada Java 8, kita dapat menggunakan kaedah untuk operasi aritmetik yang tepat. Mari lihat contohnya terlebih dahulu:

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

Kaedah statik addExact () melakukan penambahan normal, tetapi membuang pengecualian jika operasi menghasilkan limpahan atau aliran bawah:

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 Math di Java 8 menyediakan kaedah tepat yang sesuai untuk semua operasi aritmetik. Lihat dokumentasi Java untuk senarai semua kaedah ini.

Selain itu, terdapat kaedah penukaran yang tepat, yang memberikan pengecualian jika terdapat limpahan semasa penukaran ke jenis data lain.

Untuk penukaran dari panjang ke int :

public static int toIntExact(long a)

Dan untuk penukaran dari BigInteger ke int atau panjang :

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

4.3. Sebelum Java 8

Kaedah aritmetik yang tepat ditambahkan ke Java 8. Sekiranya kita menggunakan versi sebelumnya, kita boleh membuat kaedah ini sendiri. Salah satu pilihan untuk melakukannya adalah dengan menerapkan metode yang sama seperti di 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. Jenis Data Tidak Bersepadu

Jenis bukan integer terapung dan berganda tidak berkelakuan sama dengan jenis data integer ketika datang ke operasi aritmetik.

Satu perbezaan adalah bahawa operasi aritmetik pada nombor titik terapung dapat menghasilkan NaN . Kami mempunyai artikel khusus untuk NaN di Jawa, jadi kami tidak akan melihat lebih jauh lagi dalam artikel ini. Tambahan pula, tidak ada kaedah aritmetik yang tepat seperti addExact atau multiplyExact untuk jenis tidak integer dalam pakej Math .

Java mengikuti Piawai IEEE untuk Aritmetik Titik Terapung (IEEE 754) untuk jenis data terapung dan berganda . Piawaian ini adalah asas untuk cara Java menangani over-dan underflow nombor apungan.

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 nampaknya bertentangan dengan penegasan pertama kita.

8. Kesimpulannya

Dalam artikel ini, kita melihat apa yang terjadi over-and underflow, bagaimana ia dapat terjadi di Java, dan apa perbezaan antara jenis data bilangan bulat dan titik terapung.

Kami juga melihat bagaimana kami dapat mengesan over- dan underflow semasa pelaksanaan program.

Seperti biasa, kod sumber lengkap boleh didapati di Github.