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

Руководство по Java BigInteger

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

1. Введение

Java предоставляет некоторые примитивы, такие как int или long , для выполнения целочисленных операций. Но иногда нам нужно хранить числа, которые превышают доступные ограничения для этих типов данных.

В этом руководстве мы более подробно рассмотрим класс BigInteger . Мы проверим его структуру, заглянув в исходный код, и ответим на вопрос – как можно хранить большие числа за пределами доступных примитивных типов данных ?

2. Класс BigInteger

Как мы знаем, класс BigInteger используется для математических операций, включающих вычисления очень больших целых чисел, больших, чем примитивный тип long . Он представляет неизменяемые целые числа произвольной точности .

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

  • 1000 0000 представляет -128
  • 0111 1111 представляет 127
  • 1111 1111 представляет -1

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

2.1. инт сигнум

Свойство signum определяет знак `` BigInteger . Три целых числа представляют знак значения: -1 для отрицательных чисел, 0 для нуля, 1 для положительных чисел:

assertEquals(1, BigInteger.TEN.signum());
assertEquals(-1, BigInteger.TEN.negate().signum());
assertEquals(0, BigInteger.ZERO.signum());

Следует помнить, что BigInteger.ZERO должен иметь сигнум 0 из -за массива величин. Это значение гарантирует, что для каждого значения BigInteger существует ровно одно представление `` .

2.2. int[] маг

Вся магия класса BigInteger начинается со свойства mag . Он хранит заданное значение в массиве, используя двоичное представление , что позволяет опустить ограничения примитивных типов данных.

Более того, BigInteger группирует их в 32-битные порции — набор из четырех байтов. Из-за этого величина внутри определения класса объявляется как массив int :

int[] mag;

Этот массив содержит величину заданного значения в записи с обратным порядком байтов . Нулевой элемент этого массива является самым значительным целым числом величины. Давайте проверим это с помощью BigInteger(byte[] bytes) :

assertEquals(new BigInteger("1"), new BigInteger(new byte[]{0b1}))
assertEquals(new BigInteger("2"), new BigInteger(new byte[]{0b10}))
assertEquals(new BigInteger("4"), new BigInteger(new byte[]{0b100}))

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

Поскольку существует переменная величины знака ( signum ), мы не используем первый бит в качестве бита знака значения . Давайте быстро проверим это:

byte[] bytes = { -128 }; // 1000 0000
assertEquals(new BigInteger("128"), new BigInteger(1, bytes));
assertEquals(new BigInteger("-128"), new BigInteger(-1, bytes));

Мы создали два разных значения, используя конструктор BigInteger(int signum, byte[] величина) . Он переводит представление величины знака в BigInteger. Мы повторно использовали тот же массив байтов, изменив только значение знака.

Мы также можем напечатать величину, используя метод toString(int radix) :

assertEquals("10000000", new BigInteger(1, bytes));
assertEquals("-10000000", new BigInteger(-1, bytes));

Обратите внимание, что к отрицательным значениям добавляется знак минус.

Наконец, наиболее значимое значение int должно быть ненулевым . Это означает, что BigInteger.ZERO имеет массив mag нулевой длины:

assertEquals(0, BigInteger.ZERO.bitCount()); 
assertEquals(BigInteger.ZERO, new BigInteger(0, new byte[]{}));

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

Давайте теперь перейдем к более сложным примерам и проверим, как BigInteger хранит числа в примитивных типах данных.

3. BigInteger больше, чем Long.MAX_VALUE.

Как мы уже знаем, тип данных long представляет собой 64-битное целое число с дополнением до двух . Signed long имеет минимальное значение -2 ^63 (1000 0000 … 0000) и максимальное значение 2 ^63 -1 (0111 1111 … 1111). Чтобы создать число, превышающее эти пределы, нам нужно использовать класс BigInteger .

Давайте теперь создадим значение на единицу больше, чем Long.MAX_VALUE , равное 2 ^63 . Согласно информации из предыдущей главы, он должен иметь:

  • свойство signum установлено в 1,
  • массив mag , всего 64 бита, где установлен только старший бит (1000 0000 … 0000).

Во-первых, давайте создадим BigInteger с помощью функции setBit(int n) :

BigInteger bi1 = BigInteger.ZERO.setBit(63);
String str = bi1.toString(2);
assertEquals(64, bi1.bitLength());
assertEquals(1, bi1.signum());
assertEquals("9223372036854775808", bi1.toString());
assertEquals(BigInteger.ONE, bi1.substract(BigInteger.valueOf(Long.MAX_VALUE)));

assertEquals(64, str.length());
assertTrue(str.matches("^10{63}$")); // 1000 0000 ... 0000

Помните, что в двоичной системе представления биты упорядочены справа налево, начиная с 0. В то время как BigInteger.ZERO имеет пустой массив величин, установка 63-го бита делает его одновременно и самым значащим — нулевым элементом массива. Массив 64 длины. Знак автоматически устанавливается равным единице .

С другой стороны, та же последовательность битов представлена Long.MIN_VALUE . Преобразуем эту константу в массив byte[] и создадим конструкцию BigInteger:

byte[] bytes = ByteBuffer.allocate(Long.BYTES).putLong(Long.MIN_VALUE).array();
BigInteger bi2 = new BigInteger(1, bytes);
assertEquals(bi1, bi2);
...

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

Наконец, мы можем проверить внутреннюю переменную int[] mag . В настоящее время Java не предоставляет API для получения этого значения, но мы можем сделать это с помощью инструмента оценки в нашем отладчике:

./bf654d084ad54ce4fc1243312be13331.png

Мы сохраняем наше значение в массиве, используя два целых числа, два пакета по 32 бита. Нулевой элемент равен Integer.MIN_VALUE, а другой равен нулю.

4. Вывод

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

Затем мы проверили исходный код BigInteger. Мы проверили свойства signum и mag . Мы также узнали, как BigInteger хранит данное значение, что позволяет нам предоставлять большие числа, чем доступные примитивные типы данных.

Как всегда, мы можем найти все фрагменты кода и тесты на GitHub .