Преливане и преливане в Java

1. Въведение

В този урок ще разгледаме преливането и преливането на числови типове данни в Java.

Няма да се впускаме по-дълбоко в по-теоретичните аспекти - просто ще се съсредоточим върху това кога това се случва в Java.

Първо ще разгледаме целочислените типове данни, след това типовете данни с плаваща запетая. И за двамата ще видим и как можем да открием кога се случва превишаване или преливане.

2. Преливане и недоливане

Най-просто казано, препълване и преливане се случват, когато присвоим стойност, която е извън обхвата на декларирания тип данни на променливата.

Ако (абсолютната) стойност е твърде голяма, ние я наричаме препълнена, ако стойността е твърде малка, ние я наричаме underflow.

Нека разгледаме пример, при който се опитваме да присвоим стойността 101000 ( 1 с 1000 нули) на променлива от тип int или double . Стойността е твърде голяма за int или double променлива в Java и ще има препълване.

Като втори пример, да речем, че се опитваме да присвоим стойността 10-1000 (което е много близо до 0) на променлива от тип double . Тази стойност е твърде малка за двойна променлива в Java и ще има недостиг.

Нека да видим какво се случва в Java в тези случаи по-подробно.

3. Цялостни типове данни

Целочислените типове данни в Java са байт (8 бита), къс (16 бита), int (32 бита) и дълъг (64 бита).

Тук ще се съсредоточим върху типа данни int . Същото поведение се отнася и за другите типове данни, с изключение на това, че минималните и максималните стойности се различават.

Цяло число от тип int в Java може да бъде отрицателно или положително, което означава, че със своите 32 бита можем да присвоим стойности между -231 ( -2147483648 ) и 231-1 ( 2147483647 ).

Класът на обвивката Integer дефинира две константи, които съдържат тези стойности: Integer.MIN_VALUE и Integer.MAX_VALUE .

3.1. Пример

Какво ще се случи, ако дефинираме променлива m от тип int и се опитаме да присвоим твърде голяма стойност (напр. 21474836478 = MAX_VALUE + 1)?

Възможен резултат от това задание е, че стойността на m ще бъде неопределена или че ще има грешка.

И двете са валидни резултати; в Java обаче стойността на m ще бъде -2147483648 (минималната стойност). От друга страна, ако се опитаме да присвоим стойност -2147483649 ( = MIN_VALUE - 1 ), m ще бъде 2147483647 (максималната стойност). Това поведение се нарича integer-wraparound.

Нека разгледаме следния кодов фрагмент, за да илюстрираме по-добре това поведение:

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

Ще получим следния изход, който демонстрира препълване:

2147483646 2147483647 -2147483648 -2147483647 

4. Обработка на недопълване и преливане на целочислени типове данни

Java не създава изключение, когато възникне преливане; Ето защо може да е трудно да се намерят грешки, произтичащи от преливане. Нито можем да получим директен достъп до знамето за препълване, което се предлага в повечето процесори.

Съществуват обаче различни начини за справяне с евентуално преливане. Нека разгледаме няколко от тези възможности.

4.1. Използвайте различен тип данни

Ако искаме да разрешим стойности, по-големи от 2147483647 (или по-малки от -2147483648 ), можем просто да използваме дългия тип данни или BigInteger вместо това.

Въпреки че променливите от тип long също могат да преливат, минималните и максималните стойности са много по-големи и вероятно са достатъчни в повечето ситуации.

Обхватът на стойностите на BigInteger не е ограничен, освен от размера на паметта, достъпна за JVM.

Нека да видим как да пренапишем горния ни пример с BigInteger :

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

Ще видим следния изход:

2147483647 2147483648 2147483649 2147483650

Както можем да видим в изхода, тук няма преливане. Нашата статия BigDecimal и BigInteger в Java обхваща по-подробно BigInteger .

4.2. Хвърлете изключение

Има ситуации, в които не искаме да позволим по-големи стойности, нито искаме да се получи преливане и вместо това искаме да хвърлим изключение.

As of Java 8, we can use the methods for exact arithmetic operations. Let's look at an example first:

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

The static method addExact() performs a normal addition, but throws an exception if the operation results in an overflow or 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)

In addition to addExact(), the Math package in Java 8 provides corresponding exact methods for all arithmetic operations. See the Java documentation for a list of all these methods.

Furthermore, there are exact conversion methods, which throw an exception if there is an overflow during the conversion to another data type.

For the conversion from a long to an 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);

Което изглежда противоречи на първото ни твърдение.

8. Заключение

В тази статия видяхме какво се прелива и прелива, как това може да се случи в Java и каква е разликата между типовете данни с цяло число и с плаваща запетая

Видяхме също така как можем да открием преизпълнение и преливане по време на изпълнение на програмата.

Както обикновено, пълният изходен код е достъпен в Github.