Java 8 беззнаковая арифметическая поддержка

Поддержка арифметики в Java 8 без знака

1. обзор

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

Поддержка неподписанной арифметики, наконец, стала частью JDK начиная с версии 8. This support came in the form of the Unsigned Integer API, primarily containing static methods in the Integer and Long classes.с

В этом руководстве мы рассмотрим этот API и дадим инструкции о том, как правильно использовать числа без знака.

2. Представления битового уровня

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

In Java, numbers are encoded using the two’s complement system. Эта кодировка реализует множество основных арифметических операций, включая сложение, вычитание и умножение, одинаково независимо от того, являются ли операнды знаковыми или беззнаковыми.

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

Предположим, у нас есть некий типbyte со значением100. Это число имеет двоичное представление0110_0100.

Давайте удвоим это значение:

byte b1 = 100;
byte b2 = (byte) (b1 << 1);

Оператор сдвига влево в данном коде перемещает все биты в переменнойb1 на позицию влево, технически увеличивая ее значение в два раза. Тогда двоичное представление переменнойb2 будет1100_1000.

В системе беззнакового типа это значение представляет собой десятичное число, эквивалентное2^7 + 2^6 + 2^3 или200. Тем не менее,in a signed system, the left-most bit works as the sign bit. Следовательно, результат будет -2^7 + 2^6 + 2^3 или-56.

Быстрый тест может проверить результат:

assertEquals(-56, b2);

Мы видим, что вычисления чисел со знаком и без знака одинаковы. Differences only appear when the JVM interprets a binary representation as a decimal number.с

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

Здесь вступает в игру Unsigned Integer API.

3. API беззнаковых целых чисел

Целочисленный API без знака обеспечивает поддержку целочисленной арифметики без знака в Java 8. Большинство членов этого API - статические методы в классахInteger иLong.

Методы в этих классах работают аналогично. Таким образом, мы сосредоточимся только на классеInteger, оставив для краткости классLong.

3.1. сравнение

КлассInteger определяет метод с именемcompareUnsigned для сравнения чисел без знака. This method considers all binary values unsigned, ignoring the notion of the sign bit.

Начнем с двух чисел на границах типа данныхint:

int positive = Integer.MAX_VALUE;
int negative = Integer.MIN_VALUE;

Если мы сравним эти числа как значения со знаком,positive, очевидно, больше, чемnegative:

int signedComparison = Integer.compare(positive, negative);
assertEquals(1, signedComparison);

При сравнении чисел как значений без знака крайний левый бит считается старшим значащим битом, а не знаковым битом. Таким образом, результат будет другим,positive меньше, чемnegative:

int unsignedComparison = Integer.compareUnsigned(positive, negative);
assertEquals(-1, unsignedComparison);

Это должно быть понятнее, если мы посмотрим на двоичное представление этих чисел:

  • MAX_VALUE0111_11111111

  • MIN_VALUE1000_00000000

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

assertEquals(negative, positive + 1);

3.2. Деление и модуль

Так же, как и операция сравнения,the unsigned division and modulo operations process all bits as value bits. Следовательно, частные и остатки различаются, когда мы выполняем эти операции над числами со знаком и без знака:

int positive = Integer.MAX_VALUE;
int negative = Integer.MIN_VALUE;

assertEquals(-1, negative / positive);
assertEquals(1, Integer.divideUnsigned(negative, positive));

assertEquals(-1, negative % positive);
assertEquals(1, Integer.remainderUnsigned(negative, positive));

3.3. анализ

При разбореString с использованием методаparseUnsignedIntthe text argument can represent a number greater than MAX_VALUE.

Такое большое значение нельзя проанализировать с помощью методаparseInt, который может обрабатывать только текстовое представление чисел отMIN_VALUE доMAX_VALUE.

Следующий тестовый пример проверяет результаты анализа:

Throwable thrown = catchThrowable(() -> Integer.parseInt("2147483648"));
assertThat(thrown).isInstanceOf(NumberFormatException.class);

assertEquals(Integer.MAX_VALUE + 1, Integer.parseUnsignedInt("2147483648"));

Обратите внимание, что методparseUnsignedInt может анализировать строку, указывающую число больше, чемMAX_VALUE, но не сможет проанализировать любое отрицательное представление.

3.4. форматирование

Подобно синтаксическому анализу, при форматировании числа операция без знака рассматривает все биты как биты значения. Следовательно,we can produce the textual representation of a number about twice as large as MAX_VALUE.

Следующий тестовый пример подтверждает результат форматированияMIN_VALUE в обоих случаях - со знаком и без знака:

String signedString = Integer.toString(Integer.MIN_VALUE);
assertEquals("-2147483648", signedString);

String unsignedString = Integer.toUnsignedString(Integer.MIN_VALUE);
assertEquals("2147483648", unsignedString);

4. Плюсы и минусы

Многие разработчики, особенно разработчики языка, поддерживающего типы данных без знака, такие как C, приветствуют введение арифметических операций без знака. Однакоthis isn’t necessarily a good thing.

Есть две основные причины спроса на номера без знака.

Во-первых, существуют случаи, когда отрицательное значение никогда не может возникнуть, и использование типа без знака может предотвратить такое значение в первую очередь. Во-вторых, с беззнаковым типом мы можемdouble the range of usable positive values по сравнению со своим подписанным аналогом.

Давайте проанализируем причины обращения к беззнаковым числам.

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

Например, методString.indexOf возвращает позицию первого появления определенного символа в строке. Индекс -1 может легко обозначать отсутствие такого символа.

Другая причина чисел без знака - расширение пространства значений. Однакоif the range of a signed type isn’t enough, it’s unlikely that a doubled range would suffice.

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

Другая проблема API беззнакового целого числа состоит в том, что двоичная форма числа одинакова независимо от того, подписано оно или нет. Следовательно, этоeasy to mix signed and unsigned values, which may lead to unexpected results.

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

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

Как всегда, доступен исходный код этой статьиover on GitHub.