Перейти к основному содержимому

Поддержка беззнаковых арифметических операций в Java 8

· 6 мин. чтения

Упражнение: Сложение двух чисел

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

ANDROMEDA

1. Обзор

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

Поддержка беззнаковой арифметики, наконец, стала частью JDK начиная с версии 8. Эта поддержка появилась в виде API Unsigned Integer, в основном содержащего статические методы в классах Integer и Long .

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

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

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

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

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

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

Удвоим это значение:

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

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

В системе беззнакового типа это значение представляет собой десятичное число, эквивалентное 2^7 + 2^6 + 2^3 или 200 . Тем не менее, в системе со знаком крайний левый бит работает как бит знака. Следовательно, результат равен -2^7 + 2^6 + 2^3 или -56 .

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

assertEquals(-56, b2);

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

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

Именно здесь в игру вступает Unsigned Integer API.

3. Целочисленный API без знака

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

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

3.1. Сравнение

Класс Integer определяет метод с именем compareUnsigned для сравнения чисел без знака. Этот метод считает все двоичные значения беззнаковыми, игнорируя понятие бита знака.

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

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

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

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

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

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

Будет яснее, если мы посмотрим на двоичное представление этих чисел:

  • МАКС_ЗНАЧЕНИЕ -> 0111_1111_…_1111
  • МИН_ЗНАЧ -> 1000_0000_…_0000

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

assertEquals(negative, positive + 1);

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

Как и операция сравнения, беззнаковое деление и операции по модулю обрабатывают все биты как биты значения. Таким образом, частные и остатки различны, когда мы выполняем эти операции над знаковыми и беззнаковыми числами:

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. Разбор

При синтаксическом анализе строки с использованием метода parseUnsignedInt текстовый аргумент может представлять число больше 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. Форматирование

Подобно синтаксическому анализу, при форматировании числа беззнаковая операция рассматривает все биты как биты значения. Следовательно, мы можем создать текстовое представление числа примерно в два раза больше, чем 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, приветствуют введение беззнаковых арифметических операций. Однако это не обязательно хорошо.

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

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

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

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

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

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

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

Еще одна проблема с API Unsigned Integer заключается в том, что двоичная форма числа одинакова независимо от того, знаковое оно или беззнаковое. Поэтому легко смешивать знаковые и беззнаковые значения, что может привести к неожиданным результатам .

5. Вывод

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

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