Автор оригинала: Nguyen Nam Thai.
1. Обзор
С самого зарождения Java все числовые типы данных подписываются. Однако во многих ситуациях требуется использовать значения без знака. Например, если мы подсчитаем количество вхождений события, мы не хотим столкнуться с отрицательным значением.
Поддержка арифметики без знака, наконец, стала частью JDK начиная с версии 8. Эта поддержка появилась в виде API Unsigned Integer, в основном содержащего статические методы в классах Integer и Long .
В этом уроке мы рассмотрим этот API и дадим инструкции о том, как правильно использовать беззнаковые числа.
2. Представления На Уровне Битов
Чтобы понять, как обрабатывать числа со знаком и без знака, давайте сначала рассмотрим их представление на битовом уровне.
В Java числа кодируются с помощью системы two, дополняющей . Эта кодировка реализует множество основных арифметических операций, включая сложение, вычитание и умножение, одинаково, независимо от того, являются ли операнды знаковыми или беззнаковыми.
Все должно быть яснее с примером кода. Для простоты мы будем использовать переменные типа данных byte primitive. Операции аналогичны для других интегральных числовых типов, таких как short , int или long .
Предположим, что у нас есть некоторый тип byte со значением 100 . Это число имеет двоичное представление 0110_0100 .
Давайте удвоим это значение:
byte b1 = 100; byte b2 = (byte) (b1 << 1);
Оператор сдвига влево в данном коде перемещает все биты в переменной b1 a влево, технически делая ее значение вдвое большим. Тогда двоичное представление переменной b2 будет 1100_1000 .
В системе типов без знака это значение представляет десятичное число, эквивалентное 2^7 + 2^6 + 2^3 , или 200 . Тем не менее, в знаковой системе самый левый бит работает как знаковый бит. Таким образом, результат -2^7 + 2^6 + 2^3 , или -56 .
Быстрый тест может проверить результат:
assertEquals(-56, b2);
Мы видим, что вычисления знаковых и беззнаковых чисел одинаковы. Различия появляются только тогда, когда JVM интерпретирует двоичное представление как десятичное число.
Операции сложения, вычитания и умножения могут работать с беззнаковыми числами, не требуя каких-либо изменений в JDK. Другие операции, такие как сравнение или деление, обрабатывают числа со знаком и без знака по-разному.
Именно здесь в игру вступает целочисленный API без знака.
3. Целочисленный API Без Знака
API Unsigned Integer обеспечивает поддержку целочисленной арифметики без знака в Java 8. Большинство членов этого API являются статическими методами в классах Integer и Long .
Методы в этих классах работают аналогично. Таким образом, мы сосредоточимся только на классе Integer , оставив для краткости класс Long .
3.1. Сравнение
Класс Integer определяет метод с именем compare Unsigned для сравнения беззнаковых чисел. Этот метод рассматривает все двоичные значения без знака, игнорируя понятие бита знака.
Давайте начнем с двух чисел на границах типа данных 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);
Это должно быть яснее, если мы посмотрим на двоичное представление этих чисел:
- MAX_VALUE -> 0111_1111_…_1111
- MIN_VALUE -> 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. Синтаксический анализ
При разборе String с помощью метода 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 .