Як Java обробляє цілі цілі підтоки та переливи та як би ви це перевірили?


226

Як Java обробляє цілі цілі підтоки та переливи?

На основі цього, як би ви перевірили / перевірили, що це відбувається?


28
Дуже погано, що Java не надає опосередкований доступ до прапора переповнення процесора , як це робиться в C # .
Дрю Ноакс

@DrewNoakes І це дуже погано, що C # не за замовчуванням checked, наскільки я знаю. Я не бачу, що він використовується багато, а введення тексту checked { code; }- це стільки ж роботи, як і виклик методу.
Maarten Bodewes

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

@DrewNoakes ОК, цікаво. Трохи дивно, що це налаштування поза кодом. Взагалі я хотів би мати таку ж поведінку програми незалежно від таких налаштувань (можливо, за винятком тверджень).
Maarten Bodewes

@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 - це умова в комп'ютерній програмі, де результатом обчислення є кількість менших абсолютних значень, ніж комп’ютер може насправді представляти в пам'яті на своєму процесорі. Таким чином, underflow не поширюється на Java Integers. @BalusC
Jingguo Yao

66

Що ж стосується примітивних цілочисельних типів, Java взагалі не обробляє Over / Underflow (для плаваючого та подвійного поведінка відрізняється, воно буде перетворюватися на +/- нескінченність так само, як IEEE-754 мандати).

Додавши два int, ви не отримаєте вказівки, коли відбудеться переповнення. Простий спосіб перевірити наявність переповнення - використовувати наступний більший тип, щоб фактично виконати операцію і перевірити, чи є результат все ще в діапазоні для типу джерела:

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

Що ви робите замість застережень про кидок, залежить від ваших вимог програми (кидати, вмикати мінімум / макс. Або просто входити в журнал). Якщо ви хочете виявити переповнення при тривалих операціях, вам не пощастило з примітивами, скористайтеся натомість 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 (це ефективно перевірить бітовий знак). Принцип працює точно так само для всіх цілих примитивних типів , зміна всіх декларацій у вищевказаному методі на довгий змушує його працювати довго.

Для менших типів, внаслідок неявного перетворення в 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
Ви можете перевірити це за допомогою побітових
rogerdpack

1
Це спрацює, але я припускаю, що це матиме неприємний удар.
chessofnerd

33

За замовчуванням, інта та довга математика Java мовчки обертаються над переливом та переливом. (Операції з цілими числами для інших цілих типів виконуються шляхом попереднього просування операндів до int або long, згідно JLS 4.2.2 .)

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

Що стосується Java 8, java.lang.Math також передбачає toIntExactвіддати довгу до 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, так і для довгих примітивних типів і ігнорує переповнення позитивними та негативними цілими числами.

Ця відповідь спочатку описує цілочисельне переповнення, наводить приклад того, як це може статися, навіть із проміжними значеннями в оцінці виразів, а потім дає посилання на ресурси, які дають детальні методи запобігання та виявлення цілочислового переповнення.

Арифметика цілого числа і вирази, що виникають у результаті несподіваного або невиявленого переповнення, є поширеною помилкою програмування. Несподіване або невиявлене переповнення цілих чисел також є добре відомою проблемою безпеки, особливо це стосується об'єктів масиву, стека та списку.

Переповнення може відбуватися у позитивному чи негативному напрямку, коли позитивне чи негативне значення буде перевищувати максимальне або мінімальне значення для відповідного примітивного типу. Переповнення може виникати в проміжному значенні під час вираження або оцінки операції і впливати на результат виразу або операції, коли очікується, що кінцеве значення буде в межах діапазону.

Іноді негативний перелив помилково називається підливом. Underflow - це те, що відбувається, коли значення буде ближче до нуля, ніж дозволяє представлення. Підвищення відбувається в цілій арифметиці і очікується. Підниження цілого числа відбувається, коли ціле оцінювання буде від -1 до 0 або 0 і 1. Який буде дробовий результат скорочується до 0. Це нормально і очікується з арифметикою з цілим числом і не вважається помилкою. Однак це може призвести до викиду коду. Одним із прикладів є виняток "ArithmeticException: / за нулем", якщо результат вираження цілого числа використовується як дільник у виразі.

Розглянемо наступний код:

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

в результаті чого x присвоюється 0, а наступна оцінка bigValue / x кидає виняток, "ArithmeticException: / по нулю" (тобто ділиться на нуль), а не y присвоюється значення 2.

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

Переставляючи вираз для обчислення x на вираз, який при оцінці ділиться перед множенням наступного коду:

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

приводить до того, що x присвоюється 858,993,458, а y присвоюється 2, що очікується.

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

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

Існують методи уникнення ненавмисного цілого переповнення. Техніка може бути класифікована як тестування перед умовами, оновлення та BigInteger.

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

Оновлення включає використання більшого примітивного типу для виконання арифметичної операції або виразу, а потім визначення, чи отримане значення перевищує максимальне або мінімальне значення для цілого числа. Навіть при оновленнях все одно можливо, що значення або якесь проміжне значення в операції чи виразі буде перевищувати максимальні або мінімальні значення для типу оновлення та спричинити переповнення, яке також не буде виявлено і спричинить несподівані та небажані результати. За допомогою аналізу або попередніх умов можна запобігти переповненню моменту, коли запобігання без оновлення неможливе або практичне. Якщо цілі числа, про які йдеться, вже є довгими примітивними типами, то оновлення неможливо з примітивними типами на Java.

Метод BigInteger включає використання BigInteger для арифметичної операції або вираження за допомогою бібліотечних методів, які використовують BigInteger. BigInteger не переповнює. Він буде використовувати всю наявну пам'ять, якщо необхідно. Його арифметичні методи, як правило, лише дещо менш ефективні, ніж цілі операції. Досі можливо, що результат, що використовує BigInteger, може перевищувати максимальне або мінімальне значення для цілого числа, однак переповнення не буде відбуватися в арифметиці, що веде до результату. Програмування та проектування все одно потрібно буде визначати, що робити, якщо результат BigInteger перевищує максимальні або мінімальні значення для бажаного примітивного типу результату, наприклад, int або long.

Програма CERT Carnegie Mellon Software Engineering Institute та Oracle створили набір стандартів для безпечного програмування Java. До стандартів входять методики запобігання та виявлення цілого переповнення. Стандарт опублікований як вільно доступний інтернет-ресурс тут: Стандарт безпечного кодування CERT Oracle для Java

Розділ стандарту, який описує і містить практичні приклади методів кодування для запобігання або виявлення цілого числа переповнення, знаходиться тут: NUM00-J. Виявити або запобігти цілому переповненню

Форма книги та форма PDF у форматі стандарту безпечного кодування CERT Oracle для 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-бітні половинки, просувати кожну половину на довгу (будьте уважні щодо розширення знаків!), А потім обчислити два часткові добутки [одна з верхніх половинок повинна бути нулем].
supercat

Коли ви говорите "Довго * довго, якщо нормалізує операнди, щоб бути позитивними ...", як би ви почали нормалізувати Long.MIN_VALUE?
fragorl

Ці методи можуть бути цікавими, якщо потрібно перевірити, чи щось переповнюється, перш ніж реально виконати обчислення. Це може бути приємно для тестування, наприклад, введення користувача, яке використовується для таких обчислень, замість того, щоб виловлювати виняток, коли це відбувається.
Maarten Bodewes

8

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


Так, це гарна ідея, якщо ви не є Java 8 або вище, і в цьому випадку Mathклас містить подібний код.
Maarten Bodewes

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

Ну! А тепер, чи можете ви відповісти, як виявити це на складне обчислення?
Aubin

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", отриманого в результаті будь-якої іншої інформації. Таким чином, ви не можете сказати через якийсь статус або перевіривши лише значення, чи воно переповнене.

Але ви можете бути розумними щодо своїх обчислень, щоб уникнути переповнення, якщо це має значення, або принаймні знати, коли це станеться. Яка у вас ситуація?


Це насправді не ситуація, просто щось, про що мені цікаво, і що змусило мене задуматися. Якщо вам потрібен приклад використання case, ось такий: у мене є клас із власною внутрішньою змінною під назвою "секунди". У мене є два методи, які беруть ціле число як параметр і збільшать або зменшать (відповідно) "секунди" на стільки ж. Як би ви перевірили те, що відбувається перелив / перелив, і як би ви запобігли його виникненню?
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 обробляє цілі цілі підтоки та переповнення (додайте текст для пояснення).
Spencer Wieczorek

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.