Рубрики
Без рубрики

Переполнение и недостаточный поток в Java

Краткий и практический обзор переполнения и недостаточного потока в Java.

Автор оригинала: Michael Krimgen.

1. введение

В этом уроке мы рассмотрим переполнение и недостаточный поток числовых типов данных в Java.

Мы не будем углубляться в более теоретические аспекты — мы просто сосредоточимся на том, когда это произойдет на Java.

Сначала мы рассмотрим целочисленные типы данных, затем типы данных с плавающей запятой. Для обоих случаев мы также увидим, как мы можем определить, когда происходит избыточный или недостаточный поток.

2. Переполнение и недостаточный поток

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

Если (абсолютное) значение слишком велико, мы называем это переполнением, если значение слишком мало, мы называем это недостаточным потоком.

Давайте рассмотрим пример, в котором мы пытаемся присвоить значение 10 10001 с 1000 нули) в переменную типа int или double . Значение слишком велико для переменной int или double в Java, и произойдет переполнение.

В качестве второго примера предположим, что мы пытаемся присвоить значение 10 -1000 (что очень близко к 0) к переменной типа double . Это значение слишком мало для переменной double в Java, и возникнет недостаточный поток.

Давайте посмотрим, что происходит в Java в этих случаях более подробно.

3. Целочисленные Типы Данных

Целочисленными типами данных в Java являются байт (8 бит), короткий (16 бит), int (32 бита) и длинный (64 бита).

Здесь мы сосредоточимся на инт тип данных. То же самое поведение применимо и к другим типам данных, за исключением того, что минимальные и максимальные значения различаются.

Целое число типа int в Java может быть отрицательным или положительным, что означает, что с его 32 битами мы можем присваивать значения между -2 31 ( -2147483648 ) и 2 31 -1 ( 2147483647 ).

Класс-оболочка Integer определяет две константы, которые содержат эти значения: Integer.МИНИМАЛЬНОЕ ЗНАЧЕНИЕ и Целое число.МАКСИМАЛЬНОЕ ЗНАЧЕНИЕ .

3.1. Пример

Что произойдет, если мы определим переменную m типа int и попытаемся присвоить слишком большое значение (например, 21474836478 + 1)?

Возможным результатом этого назначения является то, что значение m будет неопределенным или возникнет ошибка.

Оба результата являются допустимыми; однако в Java значение m будет равно -2147483648 (минимальное значение). С другой стороны, если мы попытаемся присвоить значение -2147483649 ( = MIN_VALUE – 1 ), m будет 2147483647 (максимальное значение). Такое поведение называется целочисленным охватом.

Давайте рассмотрим следующий фрагмент кода, чтобы лучше проиллюстрировать это поведение:

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 ), мы можем просто использовать тип данных long или 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. Создать исключение

Бывают ситуации, когда мы не хотим допускать больших значений, а также не хотим, чтобы происходило переполнение, и вместо этого мы хотим создать исключение.

Начиная с Java 8, мы можем использовать методы для точных арифметических операций. Давайте сначала рассмотрим пример:

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

Статический метод addExact() выполняет обычное добавление, но выдает исключение, если операция приводит к переполнению или недостаточному потоку:

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)

В дополнение к add Exact () пакет Math в Java 8 предоставляет соответствующие точные методы для всех арифметических операций. Список всех этих методов см. в документации Java .

Кроме того, существуют точные методы преобразования, которые создают исключение, если во время преобразования в другой тип данных происходит переполнение.

Для преобразования из long в int :

public static int toIntExact(long a)

И для преобразования из BigInteger в int или long :

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

4.3. До Java 8

Точные арифметические методы были добавлены в Java 8. Если мы используем более раннюю версию, мы можем просто создать эти методы сами. Один из вариантов сделать это-реализовать тот же метод, что и в 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. Нецелочисленные Типы Данных

Нецелочисленные типы float и double ведут себя не так, как целочисленные типы данных, когда дело доходит до арифметических операций.

Одно из отличий заключается в том, что арифметические операции с числами с плавающей запятой могут привести к NaN . У нас есть специальная статья о NaN в Java , поэтому мы не будем вдаваться в подробности в этой статье. Кроме того, в пакете Math нет точных арифметических методов, таких как addExact или multiplyExact для нецелочисленных типов.

Java следует стандарту IEEE для арифметики с плавающей запятой (IEEE 754) для своих типов данных float и double|/. Этот стандарт является основой для того, как Java обрабатывает избыточный и недостаточный поток чисел с плавающей запятой.

В нижеприведенных разделах мы сосредоточимся на избыточном и недостаточном потоке двойной тип данных и что мы можем сделать, чтобы справиться с ситуациями, в которых они возникают.

5.1. Переполнение

Что касается целочисленных типов данных, мы могли бы ожидать, что:

assertTrue(Double.MAX_VALUE + 1 == Double.MIN_VALUE);

Однако это не относится к переменным с плавающей запятой. Верно следующее:

assertTrue(Double.MAX_VALUE + 1 == Double.MAX_VALUE);

Это происходит потому, что двойной значение имеет только ограниченное количество значащие биты . Если мы увеличим значение большого двойной значение только на один, мы не меняем ни один из значимых битов. Следовательно, значение остается прежним.

Если мы увеличим значение нашей переменной таким образом, что увеличим один из значимых битов переменной, переменная будет иметь значение БЕСКОНЕЧНОСТЬ :

assertTrue(Double.MAX_VALUE * 2 == Double.POSITIVE_INFINITY);

и NEGATIVE_INFINITY для отрицательных значений:

assertTrue(Double.MAX_VALUE * -2 == Double.NEGATIVE_INFINITY);

Мы видим, что, в отличие от целых чисел, нет обтекания, но есть два разных возможных результата переполнения: значение остается неизменным, или мы получаем одно из специальных значений, POSITIVE_INFINITY или НЕГАТИВНАЯ БЛИЗОСТЬ .

5.2. Недостаточный поток

Существуют две константы, определенные для минимальных значений двойного значения: MIN_VALUE (4,9 e-324) и MIN_NORMAL (2,2250738585072014 E-308).

Стандарт IEEE для арифметики с плавающей запятой (IEEE 754) более подробно объясняет разницу между ними.

Давайте сосредоточимся на том, зачем нам вообще нужно минимальное значение для чисел с плавающей запятой.

A двойной значение не может быть произвольно малым, так как у нас есть только ограниченное количество битов для представления значения.

Глава о Типах, значениях и переменных в спецификации языка Java SE описывает, как представлены типы с плавающей запятой. Минимальный показатель степени для двоичного представления double задается в виде -1074 . Это означает, что наименьшее положительное значение , которое может иметь двойник, равно Math.pow(2, -1074) , что равно 4,9 e-324 .

Как следствие, точность double в Java не поддерживает значения между 0 и 4,9 e-324, или между -4,9 e-324 и 0 для отрицательных значений.

Итак, что произойдет, если мы попытаемся присвоить слишком маленькое значение переменной типа double ? Давайте рассмотрим пример:

for(int i = 1073; i <= 1076; i++) {
    System.out.println("2^" + i + " = " + Math.pow(2, -i));
}

С выходом:

2^1073 = 1.0E-323
2^1074 = 4.9E-324
2^1075 = 0.0
2^1076 = 0.0

Мы видим, что если мы назначим слишком малое значение, мы получим недостаточный поток, и результирующее значение будет 0.0 (положительный ноль).
Аналогично, при отрицательных значениях недостаточный поток приведет к значению -0.0
(отрицательный ноль).

6. Обнаружение недостаточного потока и переполнения типов данных с плавающей запятой

Поскольку переполнение приведет либо к положительной, либо к отрицательной бесконечности, а переполнение приведет к положительному или отрицательному нулю, нам не нужны точные арифметические методы, такие как для целочисленных типов данных. Вместо этого мы можем проверить наличие этих специальных констант для обнаружения избыточного и недостаточного потока.

Если мы хотим создать исключение в этой ситуации, мы можем реализовать вспомогательный метод. Давайте посмотрим, как это может выглядеть для возведения в степень:

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

В этом методе нам нужно использовать метод Дважды.сравните() . Обычные операторы сравнения ( < и > ) не проводите различия между положительным и отрицательным нулем.

7. Положительный и отрицательный ноль

Наконец, давайте рассмотрим пример, который показывает, почему нам нужно быть осторожными при работе с положительным и отрицательным нулем и бесконечностью.

Давайте определим пару переменных, чтобы продемонстрировать:

double a = +0f;
double b = -0f;

Потому что положительное и отрицательное 0 считаются равными:

assertTrue(a == b);

В то время как положительная и отрицательная бесконечность считаются разными:

assertTrue(1/a == Double.POSITIVE_INFINITY);
assertTrue(1/b == Double.NEGATIVE_INFINITY);

Однако следующее утверждение верно:

assertTrue(1/a != 1/b);

Что, по-видимому, противоречит нашему первому утверждению.

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

В этой статье мы рассмотрели, что такое избыточный и недостаточный поток, как это может происходить в Java и в чем разница между целочисленными типами данных и типами данных с плавающей запятой.

Мы также увидели, как мы можем обнаруживать избыточный и недостаточный поток во время выполнения программы.

Как обычно, полный исходный код доступен на Github .