Девочка открывает высокий резной шкаф, из которого льется яркий свет: отсылка к миру, который прячется за обычной дверью
Если коротко: бит это одна двоичная цифра, 0 или 1, а байт это восемь бит, самый маленький кусок памяти, к которому можно обратиться по адресу. В Java на байте построен тип byte со значениями от -128 до 127, а все остальные целые типы просто кратны ему по размеру. Ниже разбираем, откуда берется двоичная система, как в восьми битах умещаются отрицательные числа, что делают сдвиги и побитовые операции, и сколько на самом деле занимает каждый примитив.

Кратко

  • Бит это один двоичный разряд, 0 или 1. Байт это восемь бит, наименьшая адресуемая единица памяти; восемь бит дают 256 разных значений.
  • В Java byte знаковый: старший бит отвечает за знак, поэтому диапазон от -128 до 127, а 127 + 1 дает -128.
  • Отрицательные числа хранятся в дополнительном коде: инвертировать все биты и прибавить 1. Поэтому ~5 это -6, а не -5.
  • Сдвиг влево на N это умножение на 2 в степени N, сдвиг вправо это деление: 5 << 2 дает 20.
  • Побитовое И проверяет отдельные биты: x & 1 равно 0 у четных чисел, а вместе с ИЛИ из битов собирают маски флагов.
  • Целые типы растут вдвое: byte 8 бит, short 16, int 32, long 64. Для boolean у JVM нет своих инструкций: в вычислениях он обрабатывается как int, а в массивах на элемент отводится байт.

Вступление

Если люди считают в десятичной системе счисления, то компьютеры в двоичной. А программист должен понимать, как говорить и с людьми, и с компьютерами. Данный обзор должен помочь в этом деле. Порой за очевидными вещами кроется целый мир. Предлагаю об этом мире поговорить. Например, в неделе 7 дней. А теперь, ответим на вопрос: Что такое число "7"? ) Во-первых, это целое (integer) положительное (positive) натуральное (natural) число. А еще это десятичное число (decimal number). Десятичное число это число в десятичной системе счисления (Decimal System). Когда мы говорим "десятичная система счисления", это означает, что у системы счисления основание (base) равно 10. Основание показывает, сколько цифр может быть использовано в данной системе счисления для представления числа. Отсчет ведется от нуля. Соответственно, для представления чисел в десятичной системе счисления мы используем цифры от 0 до 9. Это хорошо, но считать нужно не только до 9, но и дальше. Как же быть? Например, число 10. Для записи данного числа мы используем уже целых 2 цифры. Позиция каждой цифры в десятичной системе называется десятичным разрядом (decimal place). Разряды отсчитываются справа налево:
Число 148 951 784 296 разбито на классы миллиардов, миллионов, тысяч и единиц, внутри каждого класса подписаны сотни, десятки и единицы
Кроме того, десятичное число можно разложить следующим образом: 103 = 1*10^2 + 0*10^1 + 3*10^0
Пример из учебника: число сто три в десятичной системе представляется как 1 умножить на 10 в квадрате плюс 0 умножить на 10 плюс 3 умножить на 10 в нулевой степени
Число ведь по сути растет справа на лево. То есть сначала было 7, а потом стало 10. Поэтому и разряды считаются справа, начиная с нуля. А для чего все это? Все потому, что мы не компьютеры. И если мы считаем в десятичной системе (то есть, по основанию 10), компьютеры считают в двоичной системе счисления (то есть, по основанию 2). Но правила, которые действуют в этих системах счисления, одинаковы.
Поток зеленых нулей и единиц на черном фоне, как в фильме «Матрица»

Двоичная система

Двоичная система очень похожа на десятичную с тем лишь отличием, что ограничение здесь не 10, а 2.

Перевод из десятичной системы в двоичную

Давайте сравним на примере. Как нам представить 11 в двоичной системе? Очень просто: надо лишь разделить десятичное число на основание 2, то есть посчитать 11/2 в столбик. Пример:
Деление 11 на 2 в столбик: остатки 1, 1, 0, 1 стрелками собираются в результат 1011
Или вот пример с WikiHow:
Иллюстрация wikiHow: 156 делится на 2 столбиком (156, 78, 39, 19, 9, 4, 2, 1), остатки выписаны снизу вверх, и получается 156 в десятичной = 10011100 в двоичной
Интересно, что мы можем число представить в двоичной системе так же, как и в десятичной: 111 в двоичной системе = 1*2^2 + 1*2^1 + 1*2^0 = 4 + 2 + 1
Формула: 111 в двоичной системе равно 1 умножить на 2 в квадрате плюс 1 умножить на 2 плюс 1, то есть 4 плюс 2 плюс 1, получилось 7
Переводить туда и обратно умеет и сама Java: Integer.toBinaryString(11) вернет строку "1011", а Integer.parseInt("1011", 2) прочитает ее как число 11 (второй аргумент это основание системы счисления). Говоря про то, что правила работы в системах счисления одинаковы, давайте посмотрим на сложение в двоичной системе:
Фрагмент учебника про сложение в двоичной системе с четырьмя примерами в столбик: 1001 плюс 1010 равно 10011, 1101 плюс 1011 равно 11000, 11111 плюс 1 равно 100000 и пример с дробной частью
Как видно, мы точно так же переносим разряды при сложении, как и в десятичной системе. Разбор сложения можно посмотреть, например, здесь:

Разряд, бит и байт

Кстати, периодически упоминается какое-то слово "разряд". А что это такое? Разряд это всего лишь "структурный элемент" представления числа. То есть число 10 состоит из двух разрядов: нам надо 2 разряда, 2 места, 2 элемента, чтобы записать это число. Нам это важно понимать потому, что в двоичной системе счисления разряд это бит (bit). Слово Bit произошло от английского "binary digit", то есть двоичная цифра. Она может быть или 0, или 1. Но так же, как мы читаем с вами цифры и слова целиком, а не по буквам, компьютеры читают не по одному биту. Минимальным "куском" обрабатываемой информации в оперативной памяти (так называемая наименьшая адресуемая единица информации) считается последовательность из 8 бит. Так как их 8, это называют "октет". А еще более известным нам словом Байт (Byte). Чтобы запомнить октет, можно запомнить, что слово осьминог (восемь ног) переводится на английский как octopus. То есть тут как раз тот самый "окто" в названии:
Мультяшный осьминог с подписью Octopus держит в восьми щупальцах нули и единицы, из которых складывается байт 00000111
Давайте подумаем, какое максимальное число мы можем представить в виде 8 бит?
Формула: 11111111 в двоичной системе равно сумме степеней двойки от 2 в седьмой до 2 в нулевой, то есть 128 плюс 64 плюс 32 плюс 16 плюс 8 плюс 4 плюс 2 плюс 1, получилось 255
И тут возникает вопрос: а как же быть с отрицательными числами? Чтобы понять это, поговорим о том, как представлены байты в Java.
Крупный план зеленой печатной платы с дорожками и подписью Java Byte type

Java и Byte

Как же так получается, что в Java мы можем использовать отрицательные числа? Сделано это просто. В Java байты знаковые. Крайний левый разряд/бит (его еще называют "старший бит") сделан своего рода "маркером", отвечающим на вопрос: "Это число отрицательное?". Если ответ да, значит маркер имеет значение 1. А иначе 0. Давайте посмотрим на примере, как превратить число 5 в отрицательное число 5:
Схема получения отрицательного числа: 5 в прямом коде 00000101, после инвертирования 11111010 (первое дополнение), после прибавления единицы 11111011 (второе дополнение)
Руководствуясь этой картинкой можно понять, в каких пределах лежит значение типа Byte:
Схема диапазона байта: 01111111 равно 127, 00000001 равно 1, 00000000 равно 0, 10000001 равно минус 127, 10000000 равно минус 128; стрелки показывают, как 127 плюс 1 превращается в 10000000
Так же видно, что:
  • если прибавить единицу к 127, мы получим уже -128.
  • если вычесть единицу из -128, мы получим 127.
Таким образом, Byte в Java может принимать значение от -128 до 127. Как мы помним, байт это октет. А максимальный разряд/старший бит имеет порядковый номер 7, так как мы считаем от нуля. В этом случае легко запомнить, что байт равен от -2 в степени 7 (нижняя граница) до 2 в степени 7 минус 1 (верхняя граница).

Байт в коде

Работа с самим типом данных проста. Используем в качестве "песочницы" для данной статьи онлайн-компилятор Replit (раньше он назывался repl.it): https://replit.com. Для примера выполним код, который переменную типа байт представит в двоичном виде в виде строки (метод Integer.toBinaryString отдает число без ведущих нулей, поэтому до восьми знаков его дополняет String.format):

class Main {
  public static void main(String[] args) {
    byte octet = 5;
    String bin = String.format("%8s", Integer.toBinaryString(octet)).replace(' ', '0');
    System.out.println(bin);
  }
}
Работа с байтами активно используется при работе с I/O Streams. Подробнее можно прочитать в tutorial от Oracle: "I/O Streams". Кроме того, в Java можно использовать особый литерал, чтобы значение указывать в виде битов:

class Main {
  public static void main(String[] args) {
    byte data = 0b101; 
    System.out.println(data);
  }
}
Надпись Bit Manipulation на зеленом фоне из символов в стиле «Матрицы»

Bit Manipulation

Затрагивая байты и биты, нельзя не упомянуть о различных манипуляциях с битами. Операторов для этого в Java семь, и все они работают с целыми числами побитово:
Оператор Что делает Пример
<< сдвиг влево: умножение на 2 в степени N 5 << 2 дает 20
>> сдвиг вправо с сохранением знака: деление на 2 в степени N 20 >> 2 дает 5, -8 >> 1 дает -4
>>> сдвиг вправо с заполнением нулями, знак не сохраняется -8 >>> 1 дает 2147483644
~ НЕ: инвертирует все биты ~5 дает -6
& И: 1 только там, где 1 в обоих операндах 5 & 3 дает 1
| ИЛИ: 1 там, где 1 хотя бы в одном операнде 5 | 3 дает 7
^ исключающее ИЛИ: 1 там, где биты различаются 5 ^ 3 дает 6
Ниже разберем те, что встречаются чаще всего.

Сдвиги

Наверно, самая распространенная операция это сдвиги (bitwise shift или bit-shift). А все потому, что их результат имеет явную практическую пользу. Какая польза? Сдвиг влево на N позиций эквивалентен умножению числа на 2 в степени N. А сдвиг вправо аналогичен такому же делению. Таким образом, 5<<2 == 5*Math.pow(2,2) А чтобы понять, почему так, давайте посмотрим на этот пример подробнее:
Таблица степеней двойки от 2 в нулевой до 2 в восьмой и схема сдвига влево: A равно 00000101 (5), A сдвинутое на 2 равно 00010100 (16 плюс 4, то есть 20, то есть 5 умножить на 2 в квадрате)

Побитовое отрицание NOT

Побитовое отрицание NOT (Unary bitwise), которое обозначается как тильда, инвертирует биты. Записывается, например, так: ~5.

public static void main(String[] args) {
	System.out.println(~5); //-6
 	System.out.println(~-5);//4
}
Это лишний раз показывает, что когда Java меняет знак у числа, кроме инверсии значений битов в самом конце выполняем +1. А без этого, как мы видим, наше число 5 изменяется. И чтобы оно осталось тем же числом, что и до смены знака, надо делать +1. Если хочется разобраться, почему все устроено именно так, у нас есть отдельная статья про побитовое отрицание.

Побитовое И и маски

Побитовое И (Bitwise AND) позволяет из двух разных чисел оставить значение 1 для бита только тогда, когда в обоих битах стоит единица. Интересно это может быть тем, что у этого есть польза для применения:

int x=4;
System.out.println((x&1) != 1);
Данный код проверяет число x на четность. Давайте посмотрим на примере:
Числа от 1 до 5 в двоичном виде в верхнем ряду и результат побитового И с 00000001 в нижнем ряду: младший бит выделен, у четных чисел он равен 0
При помощи совместного использования побитового И (Bitwise AND) и побитового ИЛИ (Bitwise OR) можно использовать маски:

public static void main(String[] args) {
    byte optionA=0b0100;
    byte optionB=0b0010;
    byte optionC=0b0001;
    byte value = (byte)(optionB | optionC);
    // Check for optionC
    if ((optionC & value) != 0b0000) {
      System.out.println("Yes");
    } else {
      System.out.println("No");
    }
  }
Подробнее см. "Masking options with bitwise operators in Java". Манипуляции с битами это занятная тема, по которой пишутся отдельные обзоры, статьи и книги. И отсюда начинается длинный путь в криптографию. В рамках данного обзора стоит понимать, почему это работает и как. Все семь операторов с примерами разобраны в статье «Побитовые операции в Java».

Примитивные типы

Итак, байт это октет, то есть 8 бит. Легко запомнить, что в Java существует тоже 8 примитивных типов, так уж совпало. Примитивным типом называется тип данных, который встроен в язык программирования, то есть доступен по умолчанию. Обзор всех восьми с примерами есть в отдельной статье. byte это минимальный по объему занимаемой памяти примитивный тип данных, с которым может работать Java. Как мы ранее говорили, байт занимает 8 бит. Следовательно, старший разряд имеет номер 7. Поэтому, byte содержит значения от -2 в 7 степени до 2 в 7 степени минус 1. Какие же еще есть примитивные типы? Границы целых типов взяты из JLS 4.2.1, размер boolean спецификация не задает (об этом ниже):
Тип Размер Минимум Максимум Класс-обертка
byte 8 бит -128 127 Byte
short 16 бит -32 768 32 767 Short
int 32 бита -2 147 483 648 2 147 483 647 Integer
long 64 бита -9 223 372 036 854 775 808 9 223 372 036 854 775 807 Long
char 16 бит 0 (\u0000) 65 535 (\uffff) Character
float 32 бита число с плавающей точкой по IEEE 754, одинарная точность Float
double 64 бита число с плавающей точкой по IEEE 754, двойная точность Double
boolean спецификацией не задан false true Boolean
Как мы видим по таблице, целые типы данных по объему занимаемой памяти растут в два раза. То есть short = 2 * byte, а int = 2 * short. Запомнить, на самом деле, легко. То, что байт = 8 бит, запомнили. То, что меньше быть не может, тоже запомнили. В английском языке целое число называется integer. Примитивный тип от него назвали сокращением int. Есть обычное целое число, int. Есть его короткая версия short и длинная версия long. Соответственно int занимает 32 бита (4 байта). Короткая версия в 2 раза меньше, 16 бит (2 байта), а длинная в два раза больше, то есть 64 бита (8 байт). Таким образом, int максимум может хранить число примерно в 2 миллиарда и сто миллионов (точнее, 2 147 483 647). А long максимум может хранить примерно 9 квинтиллионов (красивое слово): 9 223 372 036 854 775 807. Точные границы всех целых типов зафиксированы в спецификации языка, раздел 4.2.1. Вспоминая бородатый анекдот про то, что начинающий программист думает, что в килобайте 1000 байт, а законченный программист считает, что в килограмме 1024 грамм, можем понять:

1 mb = 1024 Kbyte = 1024 * 1024 = 1048576 bytes
1 int = 4 bytes
1 mb = 262144 int

Сколько занимает boolean

Внимательный читатель мог заметить, что у семи типов в таблице размер указан в битах, а у восьмого, boolean, стоит пометка «спецификацией не задан». boolean это логический тип данных, который имеет всего два значения: true и false. Но возникает вопрос: какого он размера? Ответит нам Java Virtual Machine Specification и раздел "2.3.4. The boolean Type":
Скриншот раздела 2.3.4 The boolean Type из спецификации JVM: отдельных инструкций для boolean нет, в выражениях он компилируется в int, а массивы boolean хранятся как массивы byte
То есть своих инструкций для boolean у виртуальной машины нет, и в вычислениях она обращается с ним как с int. Если же мы объявим массив из boolean, то каждый элемент массива будет занимать 1 байт. А вот сколько занимает одиночное поле типа boolean внутри объекта, спецификация не фиксирует вовсе: это решает конкретная реализация JVM. Вот такие вот чудеса :)

Заключение

Предлагаю ознакомиться еще с парой материалов, для закрепления: #Viacheslav

Вопросы и ответы

Сколько бит в байте и почему именно восемь?

Восемь. Байт это наименьшая единица памяти, к которой процессор может обратиться по адресу, и исторически она закрепилась на восьми битах, отсюда второе название «октет». Восемь бит дают 2 в восьмой степени, то есть 256 разных комбинаций: от 0 до 255 без знака или от -128 до 127 со знаком.

Почему byte в Java хранит числа от -128 до 127, а не от 0 до 255?

Потому что byte в Java знаковый: старший из восьми битов отведен под знак. На само значение остается семь битов, это 128 положительных комбинаций (с нулем) и 128 отрицательных. Беззнакового байта в языке нет, но есть методы вроде Byte.toUnsignedInt, которые читают те же восемь бит как число от 0 до 255.

Как хранятся отрицательные числа: что такое дополнительный код?

Чтобы получить минус N, берут двоичную запись N, инвертируют все биты и прибавляют единицу. Для 5 это 00000101, после инверсии 11111010, после плюс единицы 11111011, и это и есть -5. Такой способ выбран потому, что сложение и вычитание с ним работают одной и той же схемой без отдельной обработки знака. Отсюда же эффект ~5 == -6: инверсия без прибавления единицы дает число на один меньше.

Чем оператор >> отличается от >>>?

>> сдвигает вправо и заполняет освободившиеся слева биты копией знакового бита, поэтому отрицательное число остается отрицательным: -8 >> 1 дает -4. >>> заполняет их нулями, и знак теряется: -8 >>> 1 дает 2147483644. Для положительных чисел оба оператора работают одинаково.

Сколько памяти занимает boolean в Java?

Спецификация JVM это не фиксирует. У виртуальной машины нет отдельных инструкций для boolean, в вычислениях он обрабатывается как int, а в массивах на каждый элемент отводится один байт. Сколько занимает одиночное поле boolean внутри объекта, зависит от реализации JVM и выравнивания, на практике это обычно один байт плюс выравнивание до границы слова.

Как вывести число в двоичном виде в Java?

Методом Integer.toBinaryString(n): он возвращает строку из нулей и единиц без ведущих нулей, для 5 это "101". Если нужны все восемь разрядов, строку дополняют: String.format("%8s", Integer.toBinaryString(n)).replace(' ', '0') даст "00000101". Для long есть Long.toBinaryString. В обратную сторону работает Integer.parseInt("101", 2).

Читайте также