ЯдроКодаподготовка к экзаменам
Учебная платформа

Загружаем материалы

Подготавливаем материалы и навигацию по разделу.

Двоичный код и побитовые операторы в Java

Автор: · Обновлено

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

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

После урока вы сможете перевести небольшое число в двоичную систему, объяснить дополнительный код, применить AND, OR, XOR, NOT и выбрать нужный сдвиг. Все примеры самостоятельные и работают на JDK 17 или новее без preview. Перед практикой полезно повторить [типы и ссылки](/lessons/java/java-for-developers/java-developer-03).

Бит, байт и позиционная запись

Бит принимает одно из двух значений: 0 или 1. Байт состоит из восьми битов. В двоичной позиционной записи разряды справа налево имеют веса 1, 2, 4, 8, 16 и далее — степени двойки. Поэтому 1101 в двоичной системе означает 1 × 8 + 1 × 4 + 0 × 2 + 1 × 1, то есть десятичное 13.

Чтобы получить запись 13 в двоичной системе вручную, выделим наибольшую подходящую степень двойки: 8, остаток 5; затем 4, остаток 1; разряд 2 пропускаем; разряд 1 используем. Получаем те же четыре цифры 1101. Начальные нули, например в 00001101, не меняют положительное значение, но позволяют показать фиксированную ширину поля.

Число, его текстовая запись и кодировка текста — разные вещи. Целое 13 не хранит настройку «печатать двоично»: способ вывода выбирает программа. Строка "1101" состоит из символов и требует разбора по основанию 2. Аналогично байты файла или изображения имеют смысл только вместе с соглашением об их интерпретации. Биты данных не следует путать с командами байткода JVM из [первого урока](/lessons/java/java-for-developers/java-developer-01).

Литералы и вывод в Java

Префикс 0b обозначает двоичный литерал, 0x — шестнадцатеричный. Одна шестнадцатеричная цифра удобно описывает четыре бита: F соответствует 1111. Символ подчёркивания разделяет цифры для чтения, а суффикс L задаёт long. Правила записи приведены в JLS об integer literals.

Сохраните BinaryNotation.java:

public class BinaryNotation {
    public static void main(String[] args) {
        int value = 0b0000_1101;
        System.out.println(value);
        System.out.println(Integer.toBinaryString(value));
        System.out.println(Integer.parseInt("1101", 2));
        System.out.println(value == 0x0D);
    }
}

Скомпилируйте и запустите:

javac -encoding UTF-8 --release 17 BinaryNotation.java
java BinaryNotation

Вывод: 13, 1101, 13 и true на отдельных строках. Для остальных примеров используйте имя их публичного класса вместо BinaryNotation и сохраняйте каждый в отдельном файле. Методы преобразования строки описаны в Integer API. toBinaryString не сохраняет ведущие нули исходного литерала: они были оформлением исходника, а не дополнительными данными числа.

Разрядность и отрицательные числа

byte, short, int и long — знаковые целые фиксированной ширины. char хранит беззнаковую 16-битную кодовую единицу UTF-16, а не произвольный символ Unicode целиком. float и double устроены иначе; рассматриваемые целочисленные побитовые операции к ним не применяются. Типы и диапазоны JLS.

ТипБитовДиапазон
byte8−128 … 127
short16−32 768 … 32 767
int32−2³¹ … 2³¹ − 1
long64−2⁶³ … 2⁶³ − 1
char160 … 65 535

Знаковые типы используют дополнительный код. Для восьмибитного примера −5 возьмём 00000101, инвертируем разряды и прибавим единицу:

 5: 00000101
    11111010  инверсия
-5: 11111011  прибавили 1

Это схема в восьми битах для пояснения, а не обещание, что любое выражение Java вычисляется как byte. У старшего разряда в такой знаковой записи вес −128: для 11111011 сумма равна −128 + 64 + 32 + 16 + 8 + 2 + 1 = −5. В 32-битном int то же значение имеет дополнительные единицы слева. Представление отрицательного числа нельзя получить, просто приписав знак минус к набору его битов.

AND, OR, XOR и инверсия

Для целых операторы &, | и ^ применяют операцию к каждой паре соответствующих битов. AND оставляет единицу, когда она есть у обоих операндов; OR — хотя бы у одного; XOR — когда биты различаются. Оператор ~ меняет каждый бит на противоположный. Обзор обозначений есть в учебнике Oracle; особенности числового продвижения разбираются ниже.

Первый битВторой битANDORXOR
00000
01011
10011
11110

Проверим на 12 и 10. В четырёх младших разрядах это 1100 и 1010: AND даст 1000, OR — 1110, XOR — 0110. В следующей программе функция lowByteBits показывает только младшие восемь разрядов, а сами операции выполняются с int.

public class BitwiseTable {
    private static String lowByteBits(int value) {
        return String.format("%8s", Integer.toBinaryString(value & 0xFF))
                .replace(' ', '0');
    }

    public static void main(String[] args) {
        int a = 12;
        int b = 10;
        System.out.println(lowByteBits(a & b));
        System.out.println(lowByteBits(a | b));
        System.out.println(lowByteBits(a ^ b));
        System.out.println(~a);
        System.out.println(lowByteBits(~a));
    }
}

Вывод:

00001000
00001110
00000110
-13
11110011

Почему ~12 не равняется 3? Инвертируются все 32 бита int, включая старшие нули. После инверсии они становятся единицами, а результат интерпретируется как знаковое число −13. Если нужен только четырёхбитный результат, явно ограничьте его: (~12) & 0b1111 даёт 3. Маска определяет, какие разряды вы оставляете, и не меняет тип выражения на «четырёхбитное число».

Не путайте ^ с возведением в степень: 2 ^ 3 даёт 1, поскольку это XOR двоичных 10 и 11. Для отрицания boolean используют !, а ~ предназначен для целых. Краткое имя операции полезно знать, но понимание конкретных разрядов важнее запоминания символа.

Сдвиги: <<, >> и >>>

Оператор << сдвигает биты влево и заполняет младшие позиции нулями. >> сдвигает вправо, заполняя старшие позиции знаковым битом. >>> сдвигает вправо с заполнением нулями. Биты, вышедшие за ширину типа, теряются. Тип результата определяется левым операндом после числового продвижения. JLS о сдвигах.

public class ShiftRules {
    public static void main(String[] args) {
        System.out.println(6 << 1);
        System.out.println(-8 >> 1);
        System.out.println(-8 >>> 1);
        System.out.println(-3 >> 1);
        System.out.println(-3 / 2);
        System.out.println(1 << 31);
        System.out.println(1 << 32);
        System.out.println(1L << 32);
        System.out.println(1L << 64);
        System.out.println(-1 >>> 32);
    }
}

Вывод по порядку:

12
-4
2147483644
-2
-1
-2147483648
1
4294967296
1
-1

Разберём несколько результатов самостоятельно. У −8 при 32-битном сдвиге вправо со знаком сохраняется отрицательный старший разряд, и получается −4. При заполнении нулями значение становится большим положительным. У −3 обычное целочисленное деление на 2 даёт −1, а >> 1 — −2. Поэтому заменять деление сдвигом для произвольного знакового числа нельзя.

Почему сдвиг на 32 не обнуляет int

Java использует только пять младших битов расстояния для int и шесть для long: фактическое расстояние равно n & 31 или n & 63 соответственно. Поэтому 1 << 32 совпадает с 1 << 0, а 1L << 64 — с 1L << 0. Даже >>> не обязан дать неотрицательный результат, если фактическое расстояние нулевое: последняя строка примера это показывает.

Когда нужен 64-битный расчёт, выбирайте long до операции. В присваивании long mask = 1 << 40 сначала выполняется сдвиг int на восемь позиций, и в long попадает 256. Выражение 1L << 40 сразу работает с 64 битами и даёт 1099511627776. Добавление L к правому операнду не расширяет левый. Если позиция бита приходит извне, проверяйте её диапазон, а не полагайтесь на молчаливое маскирование.

Сдвиг влево можно понимать как умножение на степень двойки с сохранением ширины типа. Но после переполнения это не обычное неограниченное математическое умножение: 1 << 31 устанавливает знаковый разряд int. Использовать сдвиги для ускорения обычной арифметики без измерений не стоит; понятное выражение и корректный диапазон важнее.

byte сначала расширяется до int

У byte всего восемь битов, но перед ~ и сдвигами он продвигается к int. При расширении отрицательного byte старшие биты заполняются единицами. Для целочисленных &, | и ^ тоже действуют правила числового продвижения. JLS о преобразованиях.

public class BytePromotion {
    public static void main(String[] args) {
        byte value = (byte) 0xF0;
        System.out.println(value);
        System.out.println(value >> 4);
        System.out.println(value >>> 4);
        System.out.println((value & 0xFF) >>> 4);
        System.out.println(Byte.toUnsignedInt(value));
        byte one = 1;
        System.out.println(~one);
    }
}

Получим −16, −1, 268435455, 15, 240 и −2. Каждое число печатается на отдельной строке. Маска 0xFF сохраняет младшие восемь битов расширенного значения и обнуляет остальные: после этого 11110000 интерпретируется как положительное int 240. Для той же задачи можно явно вызвать Byte.toUnsignedInt.

Это не создание беззнакового типа byte: value по-прежнему равен −16, а положительное представление хранится в int. Приведение к byte после операции опять оставит восемь битов и применит знаковую интерпретацию. При работе с бинарным форматом сначала решите, какие поля беззнаковые, а затем выполняйте нужное расширение и маскирование.

Битовые маски: установить, снять и переключить флаг

Маска — число, единицами которого отмечены нужные позиции. Для независимых флагов выбирают разные одиночные биты: 1, 2, 4, 8 и далее. Маска 3 уже объединяет два младших бита, поэтому случайно назначить её третьему независимому флагу было бы ошибкой проектирования.

В собственном примере READ, WRITE и EXECUTE обозначают три условных разрешения. Это арифметическая иллюстрация; реальная авторизация приложения требует отдельной модели доступа и проверки контекста пользователя.

public class PermissionBits {
    private static final int READ = bit(0);
    private static final int WRITE = bit(1);
    private static final int EXECUTE = bit(2);

    private static int bit(int index) {
        if (index < 0 || index >= Integer.SIZE) {
            throw new IllegalArgumentException("bit index must be between 0 and 31");
        }
        return 1 << index;
    }

    public static void main(String[] args) {
        int flags = READ | WRITE;
        System.out.println(flags);
        flags &= ~WRITE;
        System.out.println(flags);
        flags ^= EXECUTE;
        System.out.println(flags);

        int required = READ | WRITE;
        System.out.println((flags & required) != 0);
        System.out.println((flags & required) == required);
        System.out.println((bit(31) & bit(31)) != 0);
    }
}

Вывод: 3, 1, 5, true, false, true. Операция flags |= mask устанавливает биты, flags &= ~mask снимает их, flags ^= mask переключает. Последовательное снятие одного и того же флага оставляет его выключенным, а два переключения возвращают исходное состояние. Поэтому удаление флага через XOR неверно, если вы не знаете, был ли он установлен.

Проверки «есть хотя бы один» и «есть все» различаются: (flags & required) != 0 и (flags & required) == required. Для маски 0 первая проверка всегда false, вторая — true: требовать отсутствие требований допустимо, но это должно быть осознанным решением интерфейса.

Скобки вокруг flags & required обязательны в показанных сравнениях: у == выше приоритет. Для проверки одного бита используйте != 0, а не > 0: маска старшего разряда int отрицательна, хотя бит установлен. Именно поэтому последняя строка программы проверяет позицию 31.

Побитовые и логические операции — не одно и то же

С boolean операторы &, | и ^ тоже допустимы, но вычисляют оба операнда. Условные && и || используют короткое замыкание: правый операнд выполняется, только когда нужен для ответа. Это важно при проверке null и при вызовах с побочными эффектами. Условные операторы Oracle.

public class BooleanEvaluation {
    private static int calls;

    private static boolean probe() {
        calls++;
        return true;
    }

    public static void main(String[] args) {
        boolean first = false && probe();
        System.out.println(first + " " + calls);
        boolean second = false & probe();
        System.out.println(second + " " + calls);

        calls = 0;
        boolean third = true || probe();
        System.out.println(third + " " + calls);
        boolean fourth = true | probe();
        System.out.println(fourth + " " + calls);
    }
}

Вывод: false 0, false 1, true 0, true 1. Логический результат пары проверок совпадает, но число вызовов отличается. В условии text != null && !text.isEmpty() второе выражение пропускается при null. Замена && на & уберёт эту защиту. При этом выражение 3 && 1 в Java недопустимо: целое число не преобразуется автоматически в boolean.

Практический разбор: извлечь компоненты ARGB

Договоримся об упаковке цвета: восемь старших битов — alpha, затем красный, зелёный и синий каналы. Это явно заданный порядок разрядов внутри int; он не описывает порядок байтов в памяти или файле. Каждый канал в нашем примере занимает число от 0 до 255.

public class ArgbBits {
    public static void main(String[] args) {
        int color = 0x80FF1020;
        int alpha = (color >>> 24) & 0xFF;
        int red = (color >>> 16) & 0xFF;
        int green = (color >>> 8) & 0xFF;
        int blue = color & 0xFF;

        int restored = (alpha << 24) | (red << 16) | (green << 8) | blue;
        System.out.println(alpha + " " + red + " " + green + " " + blue);
        System.out.println(restored == color);
    }
}

Программа выводит 128 255 16 32 и true. Сначала сдвиг подводит нужный канал к младшим разрядам, затем 0xFF отбрасывает всё лишнее. Обратная сборка возвращает компоненты на их позиции и объединяет через OR. Отрицательное значение самого int не мешает такому соглашению: здесь мы интерпретируем его как контейнер из 32 битов.

Если компоненты приходят извне, проверяйте диапазон 0–255 до упаковки. Без проверки значение 256 может затронуть соседнее поле. Маскирование такого входа до восьми битов тоже не заменяет валидацию: оно молча изменяет данные. В нашем примере диапазон обеспечен предыдущим извлечением через 0xFF.

Когда выбрать готовую коллекцию вместо ручной маски

Для произвольного количества индексируемых признаков рассмотрите BitSet: он расширяется по необходимости и предоставляет операции установки, очистки и объединения битов. Если флаги соответствуют значениям одного enum, EnumSet даёт более выразительный типизированный интерфейс. Ручная маска особенно уместна, когда битовый формат уже задан протоколом или алгоритмом.

Умение читать битовое представление не означает, что каждую задачу нужно записывать через сдвиги. Имена констант, проверка границ и маленькие примеры часто полезнее компактной цепочки операторов. Форматирование и именование можно согласовать по [уроку Java Code Conventions](/lessons/java/java-for-developers/java-developer-12).

Практика: предскажите результат и проверьте границы

  1. Представьте десятичное 22 в двоичной системе, затем вычислите 22 & 15, 22 | 1 и 22 ^ 6. Сначала выпишите разряды на бумаге, после этого добавьте выражения в отдельный main.
  2. Объясните разницу между ~0b0101 и (~0b0101) & 0b1111. Укажите тип вычисления и число разрядов, которые вы оставили маской.
  3. До запуска предскажите -1 >>> 1, 1 << 32, 1L << 32 и 1 << -1. Затем объясните, почему метод bit из PermissionBits всё равно запрещает отрицательную позицию.
  4. В PermissionBits снимите WRITE дважды через &= ~WRITE, затем попробуйте дважды выполнить ^= WRITE. Сравните состояния и сформулируйте, какая операция подходит для команды «выключить».
  5. В ArgbBits проверьте значения 0x00000000, 0xFFFFFFFF и 0x01020304. Для каждого восстановленное число должно совпасть с исходным; отдельно сверьте четыре канала.

Разбор: 22 записывается как 10110; первые результаты равны 6, 23 и 16. Полная инверсия int 5 даёт −6, а ограниченная четырьмя битами — 10. В третьем задании получатся 2147483647, 1, 4294967296 и −2147483648: для отрицательного расстояния сдвига тоже используются младшие биты, но интерфейс выбора позиции не должен принимать такой ввод. Очистка бита повторяема без изменения результата, тогда как двойной XOR возвращает исходное состояние. Для цветов все нули и все 255 — два крайних случая; 0x01020304 даёт каналы 1, 2, 3, 4.

Проверка понимания: из файла прочитан byte со значением −1, а поле протокола должно означать число 255. Какие операции нужны перед сдвигом? Сначала получите int через value & 0xFF или Byte.toUnsignedInt(value), затем работайте с нужной маской. Один оператор >>> над исходным byte сначала расширит отрицательное значение до int и не решит задачу автоматически.

Частые вопросы

Почему ~byte и сдвиг byte возвращают int?

Перед этими операциями Java выполняет числовое продвижение: byte расширяется до int, и операция работает с 32 битами. Если нужны только восемь младших разрядов как положительное число, явно выделите их маской 0xFF. Приведение обратно к byte снова даст знаковое восьмибитное значение.

Можно ли заменить умножение и деление на сдвиги ради скорости?

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

Источники