Как Java обрабатывает целочисленные переполнения и переполнения и как бы вы это проверили?


226

Как Java обрабатывает целочисленные переполнения и переполнения?

Исходя из этого, как бы вы проверить / проверить, что это происходит?


28
Жаль, что Java не обеспечивает косвенный доступ к флагу переполнения ЦП , как это делается в C # .
Дрю Ноакс

@DrewNoakes И очень плохо, что C # по умолчанию не checkedнасколько я знаю. Я не вижу, чтобы он использовался так часто, и набор текста checked { code; }- это примерно такая же работа, как и вызов метода.
Мартен Бодьюз

2
@MaartenBodewes, вы можете установить его по умолчанию во время компиляции ассемблера. csc /checked ...или установите свойство на панели свойств проекта в Visual Studio.
Дрю Ноакс

@DrewNoakes ОК, интересно. Немного странно, что это настройка вне кода. В общем, я хотел бы иметь одинаковое поведение программы независимо от таких настроек (возможно, за исключением утверждений).
Мартен Бодьюз

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

Ответы:


217

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

Вы можете проверить это заранее следующим образом:

public static boolean willAdditionOverflow(int left, int right) {
    if (right < 0 && right != Integer.MIN_VALUE) {
        return willSubtractionOverflow(left, -right);
    } else {
        return (~(left ^ right) & (left ^ (left + right))) < 0;
    }
}

public static boolean willSubtractionOverflow(int left, int right) {
    if (right < 0) {
        return willAdditionOverflow(left, -right);
    } else {
        return ((left ^ right) & (left ^ (left - right))) < 0;
    }
}

(вы можете заменить int, longчтобы выполнить те же проверки для long)

Если вы думаете, что это может происходить более часто, подумайте об использовании типа данных или объекта, который может хранить большие значения, например, longили, может быть java.math.BigInteger. Последний не переполняется, практически, доступная память JVM является пределом.


Если вы уже находитесь на Java8, то вы можете использовать новые методы Math#addExact()и Math#subtractExact()методы, которые вызовут ArithmeticExceptionпереполнение.

public static boolean willAdditionOverflow(int left, int right) {
    try {
        Math.addExact(left, right);
        return false;
    } catch (ArithmeticException e) {
        return true;
    }
}

public static boolean willSubtractionOverflow(int left, int right) {
    try {
        Math.subtractExact(left, right);
        return false;
    } catch (ArithmeticException e) {
        return true;
    }
}

Исходный код можно найти здесь и здесь соответственно.

Конечно, вы также можете просто использовать их сразу, а не скрывать в booleanслужебном методе.


13
@dhblah, скажем, максимальные и минимальные значения, которые Java допускает для int +100, -100, соответственно. Если вы добавляете единицу к целому числу Java, процесс будет выглядеть так, как он переполнен. 98, 99, 100, -100, -99, -98, ..., Это имеет больше смысла?
Остин

6
Я рекомендую использовать служебные методы вместо использования кода сразу. Методы утилит являются внутренними и будут заменены машинным кодом. Быстрый тест показал, что Math.addExact на 30% быстрее, чем скопированный метод (Java 1.8.0_40).
TilmannZ

1
@ErikE Math#addExact- это синтаксис, который обычно используется при написании javadocs - хотя обычно он конвертируется Math.addExact, иногда просто остается другая форма
Pokechu22

1
If it underflows, it goes back to the maximum value and continues from there.- вы, кажется, перепутали недополнение с отрицательным переполнением. недостаточное количество целых чисел происходит все время (когда результат является дробной).
неф

1
en.wikipedia.org/wiki/Arithmetic_underflow говорит, что Underflow - это условие в компьютерной программе, где результатом вычисления является число, меньшее абсолютного значения, чем компьютер может фактически представить в памяти своего ЦП. Таким образом, недополнение не относится к Java Integers. @BalusC
Цзинго Яо,

66

Что касается примитивных целочисленных типов, Java вообще не обрабатывает Over / Underflow (для float и double поведение отличается, оно сбрасывается в бесконечность +/-, как и в IEEE-754).

При добавлении двух целых чисел вы не получите индикации, когда произойдет переполнение. Простой способ проверить наличие переполнения - использовать следующий больший тип для фактического выполнения операции и проверить, находится ли результат в диапазоне для исходного типа:

public int addWithOverflowCheck(int a, int b) {
    // the cast of a is required, to make the + work with long precision,
    // if we just added (a + b) the addition would use int precision and
    // the result would be cast to long afterwards!
    long result = ((long) a) + b;
    if (result > Integer.MAX_VALUE) {
         throw new RuntimeException("Overflow occured");
    } else if (result < Integer.MIN_VALUE) {
         throw new RuntimeException("Underflow occured");
    }
    // at this point we can safely cast back to int, we checked before
    // that the value will be withing int's limits
    return (int) result;
}

Что вы будете делать вместо пунктов throw, зависит от требований вашего приложения (throw, flush to min / max или просто записывать что угодно). Если вы хотите обнаружить переполнение при длительных операциях, вам не повезло с примитивами, используйте вместо этого BigInteger.


Редактирование (2014-05-21): Поскольку этот вопрос, по-видимому, упоминается довольно часто, и мне пришлось самому решать ту же проблему, довольно просто оценить условие переполнения тем же методом, которым ЦП вычислял бы свой V-флаг.

Это в основном логическое выражение, которое включает знак обоих операндов, а также результат:

/**
 * Add two int's with overflow detection (r = s + d)
 */
public static int add(final int s, final int d) throws ArithmeticException {
    int r = s + d;
    if (((s & d & ~r) | (~s & ~d & r)) < 0)
        throw new ArithmeticException("int overflow add(" + s + ", " + d + ")");    
    return r;
}

В Java проще применить выражение (в if) ко всем 32 битам и проверить результат, используя <0 (это эффективно проверит знаковый бит). Этот принцип работает одинаково для всех целочисленных примитивных типов , если изменить все объявления в вышеприведенном методе на long, это заставит его работать долго.

Для меньших типов из-за неявного преобразования в int (подробности смотрите в JLS для побитовых операций), вместо проверки <0, проверка должна явно маскировать бит знака (0x8000 для коротких операндов, 0x80 для байтовых операндов, настроить приведение и декларация параметров соответственно):

/**
 * Subtract two short's with overflow detection (r = d - s)
 */
public static short sub(final short d, final short s) throws ArithmeticException {
    int r = d - s;
    if ((((~s & d & ~r) | (s & ~d & r)) & 0x8000) != 0)
        throw new ArithmeticException("short overflow sub(" + s + ", " + d + ")");
    return (short) r;
}

(Обратите внимание, что в приведенном выше примере используется выражение, необходимое для обнаружения вычитания переполнения)


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

Так что же происходит, если оба аргумента имеют одинаковый знак? Давайте посмотрим на случай, когда оба положительны: добавление двух аргументов, которые создают сумму, большую, чем типы MAX_VALUE, всегда будет давать отрицательное значение, поэтому переполнение происходит, если arg1 + arg2> MAX_VALUE. Теперь максимальное значение, которое может быть получено, будет MAX_VALUE + MAX_VALUE (в крайнем случае оба аргумента - MAX_VALUE). Для байта (пример), который будет означать 127 + 127 = 254. Рассматривая битовые представления всех значений, которые могут возникнуть в результате добавления двух положительных значений, можно обнаружить, что для тех, которые переполняют (от 128 до 254), установлен бит 7, тогда как все, которые не переполняются (от 0 до 127), очищают бит 7 (самый верхний, знак). Это именно то, что проверяет первая (правая) часть выражения:

if (((s & d & ~r) | (~s & ~d & r)) < 0)

(~ s & ~ d & r) становится истинным, только если оба операнда (s, d) положительны, а результат (r) отрицателен (выражение работает на всех 32 битах, но единственный бит, который нас интересует является самым верхним (знаковым) битом, который проверяется <0).

Теперь, если оба аргумента отрицательны, их сумма никогда не может быть ближе к нулю, чем любой из аргументов, сумма должна быть ближе к минус бесконечность. Самое экстремальное значение, которое мы можем получить, это MIN_VALUE + MIN_VALUE, которое (опять же для примера байта) показывает, что для любого значения в диапазоне (от -1 до -128) бит знака установлен, а любое возможное значение переполнения (от -129 до -256) ) имеет очищенный бит знака. Таким образом, знак результата снова показывает состояние переполнения. Вот что проверяет левая половина (s & d & ~ r) для случая, когда оба аргумента (s, d) отрицательны, а результат - положителен. Логика во многом эквивалентна положительному случаю; все битовые комбинации, которые могут возникнуть в результате добавления двух отрицательных значений, будут очищать знаковый бит в том и только в том случае, если произошла потеря значения.


1
Вы можете проверить это с поразрядными операторами, а также betterlogic.com/roger/2011/05/...
rogerdpack

1
Это будет работать, но я предполагаю, что это будет иметь неприятный удар по производительности.
chessofnerd

33

По умолчанию int и long math в Java молча переносятся при переполнении и переполнении. (Целочисленные операции над другими целочисленными типами выполняются, сначала переводя операнды в int или long, согласно JLS 4.2.2 .)

На Java 8, java.lang.Mathобеспечивает addExact, subtractExact, multiplyExact, incrementExact, decrementExactи negateExactстатические методы для обоих междунара и длинных аргументов , которые выполняют операцию по имени, отбрасывая ArithmeticException на переполнении. (Метода divExact нет - вам придется проверить один особый случай ( MIN_VALUE / -1) самостоятельно.)

Начиная с Java 8, java.lang.Math также обеспечивает toIntExactприведение long к int, вызывая ArithmeticException, если значение long не помещается в int. Это может быть полезно, например, для вычисления суммы целых, используя непроверенную длинную математику, а затем toIntExactв конце приводить к типу int (но будьте осторожны, чтобы не допустить переполнения вашей суммы).

Если вы все еще используете более старую версию Java, Google Guava предоставляет статические методы IntMath и LongMath для проверенного сложения, вычитания, умножения и возведения в степень (исключая переполнение). Эти классы также предоставляют методы для вычисления факториалов и биномиальных коэффициентов, которые возвращаются MAX_VALUEпри переполнении (что менее удобно проверять). Примитивные классы полезности гуавы, в SignedBytes, UnsignedBytes, Shortsи Ints, обеспечивают checkedCastметоды для сужения больших типов (бросание на IllegalArgumentException под / переливом, не ArithmeticException), а также saturatingCastметоды , которые возвращают MIN_VALUEили MAX_VALUEна переполнение.


32

Java ничего не делает с целочисленным переполнением ни для int, ни для длинных примитивных типов, и игнорирует переполнение положительными и отрицательными целыми числами.

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

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

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

Иногда отрицательное переполнение ошибочно называют недостаточным. Недостаток - это то, что происходит, когда значение будет ближе к нулю, чем позволяет представление. Недостаток происходит в целочисленной арифметике и ожидается. Потеря целочисленного значения происходит, когда целочисленное вычисление будет между -1 и 0 или 0 и 1. То, что будет дробным результатом, усекается до 0. Это нормально и ожидается с целочисленной арифметикой и не считается ошибкой. Однако это может привести к исключению кода. Одним из примеров является исключение «ArithmeticException: / by zero», если результат целочисленного занижения используется в качестве делителя в выражении.

Рассмотрим следующий код:

int bigValue = Integer.MAX_VALUE;
int x = bigValue * 2 / 5;
int y = bigValue / x;

что приводит к тому, что x присваивается 0, а последующая оценка bigValue / x вызывает исключение «ArithmeticException: / by zero» (т. е. делится на ноль) вместо y, которому присваивается значение 2.

Ожидаемый результат для x будет 858,993,458, что меньше, чем максимальное значение int 2,147,483,647. Однако промежуточный результат от оценки Integer.MAX_Value * 2 будет равен 4 294 967 294, что превышает максимальное значение int и равно -2 в соответствии с представлениями целых чисел дополнения 2s. Последующая оценка -2 / 5 оценивается в 0, что присваивается х.

Переставляем выражение для вычисления x в выражение, которое при вычислении делит перед умножением следующий код:

int bigValue = Integer.MAX_VALUE;
int x = bigValue / 5 * 2;
int y = bigValue / x;

в результате х присваивается 858,993,458, а у назначается 2, что ожидается.

Промежуточный результат от bigValue / 5 составляет 429 496 729, что не превышает максимальное значение для типа int. Последующая оценка 429 496 729 * 2 не превышает максимальное значение для int, и ожидаемый результат присваивается x. Оценка для y тогда не делится на ноль. Оценки для x и y работают как ожидалось.

Целочисленные значения Java хранятся как и ведут себя в соответствии с дополнением 2s со знаком целочисленных представлений. Когда результирующее значение будет больше или меньше, чем максимальное или минимальное целочисленные значения, вместо этого получается целочисленное значение дополнения 2. В ситуациях, специально не предназначенных для использования поведения дополнения 2s, которое является наиболее обычными целочисленными арифметическими ситуациями, результирующее значение дополнения 2s вызовет программную логику или ошибку вычисления, как было показано в примере выше. Отличная статья в Википедии описывает двоичные числа с комплиментами 2s здесь: Дополнение к двум - Википедия

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

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

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

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

Программа CERT Института разработки программного обеспечения Карнеги-Меллона и Oracle создали набор стандартов для безопасного программирования на Java. В стандарты включены методы предотвращения и обнаружения целочисленного переполнения. Стандарт опубликован в виде свободно доступного онлайн-ресурса здесь: CERT Oracle Secure Coding Standard для Java

Раздел стандарта, который описывает и содержит практические примеры методов кодирования для предотвращения или обнаружения целочисленного переполнения, находится здесь: NUM00-J. Обнаружение или предотвращение целочисленного переполнения

Книжная форма и PDF-форма CERT Oracle Secure Coding Standard для Java также доступны.


это лучший ответ здесь, поскольку в нем четко указано, что такое недостаточное значение (принятого ответа нет), а также перечислены методы борьбы с переполнением / недостаточным объемом
неф

12

Просто сам разбираюсь с этой проблемой, вот мое решение (как для умножения, так и для сложения):

static boolean wouldOverflowOccurwhenMultiplying(int a, int b) {
    // If either a or b are Integer.MIN_VALUE, then multiplying by anything other than 0 or 1 will result in overflow
    if (a == 0 || b == 0) {
        return false;
    } else if (a > 0 && b > 0) { // both positive, non zero
        return a > Integer.MAX_VALUE / b;
    } else if (b < 0 && a < 0) { // both negative, non zero
        return a < Integer.MAX_VALUE / b;
    } else { // exactly one of a,b is negative and one is positive, neither are zero
        if (b > 0) { // this last if statements protects against Integer.MIN_VALUE / -1, which in itself causes overflow.
            return a < Integer.MIN_VALUE / b;
        } else { // a > 0
            return b < Integer.MIN_VALUE / a;
        }
    }
}

boolean wouldOverflowOccurWhenAdding(int a, int b) {
    if (a > 0 && b > 0) {
        return a > Integer.MAX_VALUE - b;
    } else if (a < 0 && b < 0) {
        return a < Integer.MIN_VALUE - b;
    }
    return false;
}

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


Деление может быть медленным относительно умножения. Для int*int, я думаю , просто кастинг , чтобы longи видеть , если результат умещается в intбудет самый быстрый подход. Поскольку long*long, если кто-то нормализует операнды, чтобы они были положительными, можно разделить каждую на верхнюю и нижнюю 32-битные половины, продвигать каждую половину к длинной (будьте осторожны с расширениями знака!), А затем вычислять два частичных произведения [одна из верхних половин должна быть ноль].
суперкат

Когда вы говорите «долго * долго, если один нормализует операнды, чтобы они были положительными ...», как бы вы пошли о нормализации Long.MIN_VALUE?
Фрагор

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

8

Существуют библиотеки, которые обеспечивают безопасные арифметические операции, которые проверяют целочисленное переполнение / недополнение. Например, Guava IntMath.checkedAdd (int a, int b) возвращает сумму aи b, при условии, что он не переполняется, и выбрасывает, ArithmeticExceptionесли a + bпереполняется в знаковой intарифметике.


Да, это хорошая идея, если вы не Java 8 или выше, в этом случае Mathкласс содержит похожий код.
Мартен Бодьюз

6

Это оборачивается.

например:

public class Test {

    public static void main(String[] args) {
        int i = Integer.MAX_VALUE;
        int j = Integer.MIN_VALUE;

        System.out.println(i+1);
        System.out.println(j-1);
    }
}

печать

-2147483648
2147483647

Хорошо! А теперь, можете ли вы ответить, как определить это в сложное исчисление?
Обин

5

Я думаю, что вы должны использовать что-то вроде этого, и это называется Upcasting:

public int multiplyBy2(int x) throws ArithmeticException {
    long result = 2 * (long) x;    
    if (result > Integer.MAX_VALUE || result < Integer.MIN_VALUE){
        throw new ArithmeticException("Integer overflow");
    }
    return (int) result;
}

Вы можете прочитать далее здесь: Обнаружение или предотвращение целочисленного переполнения

Это довольно надежный источник.


3

Это ничего не делает - просто происходит переполнение.

«-1», являющееся результатом переполнения вычислений, ничем не отличается от «-1», полученного из любой другой информации. Таким образом, вы не можете определить через какое-либо состояние или просто проверить значение, переполнено ли оно.

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


Это не совсем ситуация, просто то, о чем мне интересно, и это заставляет меня задуматься. Если вам нужен пример использования, вот один: у меня есть класс с собственной внутренней переменной с именем 'seconds'. У меня есть два метода, которые принимают целое число в качестве параметра и на столько увеличивают или уменьшают (соответственно) «секунды». Как бы вы проверили модульное тестирование того, что происходит переполнение / переполнение, и как бы вы предотвратили это?
KushalP

1
static final int safeAdd(int left, int right)
                 throws ArithmeticException {
  if (right > 0 ? left > Integer.MAX_VALUE - right
                : left < Integer.MIN_VALUE - right) {
    throw new ArithmeticException("Integer overflow");
  }
  return left + right;
}

static final int safeSubtract(int left, int right)
                 throws ArithmeticException {
  if (right > 0 ? left < Integer.MIN_VALUE + right
                : left > Integer.MAX_VALUE + right) {
    throw new ArithmeticException("Integer overflow");
  }
  return left - right;
}

static final int safeMultiply(int left, int right)
                 throws ArithmeticException {
  if (right > 0 ? left > Integer.MAX_VALUE/right
                  || left < Integer.MIN_VALUE/right
                : (right < -1 ? left > Integer.MIN_VALUE/right
                                || left < Integer.MAX_VALUE/right
                              : right == -1
                                && left == Integer.MIN_VALUE) ) {
    throw new ArithmeticException("Integer overflow");
  }
  return left * right;
}

static final int safeDivide(int left, int right)
                 throws ArithmeticException {
  if ((left == Integer.MIN_VALUE) && (right == -1)) {
    throw new ArithmeticException("Integer overflow");
  }
  return left / right;
}

static final int safeNegate(int a) throws ArithmeticException {
  if (a == Integer.MIN_VALUE) {
    throw new ArithmeticException("Integer overflow");
  }
  return -a;
}
static final int safeAbs(int a) throws ArithmeticException {
  if (a == Integer.MIN_VALUE) {
    throw new ArithmeticException("Integer overflow");
  }
  return Math.abs(a);
}

2
Это обрабатывает тестирование. Хотя и не объясняется, как Java обрабатывает целочисленные переполнения и переполнения (добавьте текст, чтобы объяснить).
Спенсер Вечорек

1

Я думаю, что это должно быть хорошо.

static boolean addWillOverFlow(int a, int b) {
    return (Integer.signum(a) == Integer.signum(b)) && 
            (Integer.signum(a) != Integer.signum(a+b)); 
}

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.