Professor Hans Noodles
41 уровень

Побитовые операции

Статья из группы Java Developer
Тебе наверняка знакомо слово “бит”. Если же нет, давай познакомимся с ним :) Бит — минимальная единица измерения информации в компьютере. Его название происходит от английского “binary digit” — “двоичное число”. Бит может быть выражен одним из двух чисел: 1 или 0. Существует специальная система счисления, основанная на единицах и нулях — двоичная. Не будем углубляться в дебри математики и отметим лишь, что любое число в Java можно сконвертировать в его двоичную форму. Для этого нужно использовать классы-обертки. Побитовые операции - 1Например, вот как можно сделать это для числа int:

public class Main {

   public static void main(String[] args) {

       int x = 342;
       System.out.println(Integer.toBinaryString(x));
   }
}
Вывод в консоль:

101010110
1010 10110 (я добавил пробел для удобства чтения) — это число 342 в двоичной системе. Мы фактически разделили это число на отдельные биты — нули и единицы. Именно с ними мы можем выполнять операции, которые называются побитовыми.
  • ~ — побитовый оператор “НЕ”.

Он работает очень просто: проходится по каждому биту нашего числа и меняет его значение на противоположное: нули — на единицы, единицы — на нули. Если мы применим его к нашему числу 342, вот что получится: 101010110 — число 342 в двоичной системe 010101001 — результат выражения ~342 Но так как переменная типа int занимает 4 байта, т.е. 32 бита, на самом деле число в переменной хранится как: 00000000 00000000 00000001 01010110 — число 342 в переменной типа int в java 11111111 11111111 11111110 10101001 — результат выражения ~342 в java Попробуем выполнить это на практике:

public class Main {

   public static void main(String[] args) {

       int x = 342;
       System.out.println(Integer.toBinaryString(~x));
   }
}
Вывод в консоль:
11111111111111111111111010101001
  • & — побитовый оператор “И”

Он, как видишь, довольно похож по написанию на логический “И” (&&). Оператор &&, как ты помнишь, возвращает true только если оба операнда являются истинными. Побитовый & работает схожим образом: он сравнивает два числа по битам. Результатом этого сравнения является третье число. Для примера, возьмем числа 277 и 432: 100010101 — число 277 в двоичной форме 110110000 — число 432 в двоичной форме Далее оператор & сравнивает первый бит верхнего числа с первым битом нижнего. Поскольку это оператор “И”, то результат будет равен 1 только в том случае, если оба бита равны 1. Во всех остальных случаях результатом будет 0. 100010101 & 110110000 _______________ 100010000 — результат работы & Мы сравниваем сначала первые биты двух чисел друг с другом, потом вторые биты, третьи и т.д. Как видишь, только в двух случаях оба бита в числах были равны 1 (первый и пятый по счету биты). Результатом всех остальных сравнений стал 0. Поэтому в итоге у нас получилось число 100010000. В десятичной системе ему соответствует число 272. Давай проверим:

public class Main {

   public static void main(String[] args) {
       System.out.println(277&432);
   }
}
Вывод в консоль:

272
  • | — побитовое “ИЛИ”. Принцип работы тот же — сравниваем два числа по битам. Только теперь если хотя бы один из битов равен 1, результат будет равен 1. Посмотрим на тех же числах — 277 и 432:
100010101 | 110110000 _______________ 110110101 — результат работы | Здесь уже результат другой: нулями остались только те биты, которые в обоих числах были нулями. Результат работы — число 110110101. В десятичной системе ему соответствует число 437. Проверим:

public class Main {

   public static void main(String[] args) {
       System.out.println(277|432);
   }
}
Вывод в консоль:

437
Мы все посчитали верно! :)
  • ^ — побитовое исключающее “ИЛИ” (также известно как XOR)
С таким оператором мы еще не сталкивались. Но ничего сложного в нем нет. Он похож на обычное “или”. Разница в одном: обычное “или” возвращает true, если хотя бы один операнд является истинным. Но не обязательно один — если оба будут true — то и результат true. А вот исключающее “или” возвращает true только если один из операндов является истинным. Если истинны оба операнда, обычное “или” вернет true(“хотя бы один истинный“), а вот исключающее или вернет false. Поэтому он и называется исключающим. Зная принцип предыдущих побитовых операций, ты наверняка и сам сможешь легко выполнить операцию 277^432. Но давай лучше лишний раз разберемся вместе :) 100010101 ^ 110110000 _______________ 010100101 — результат работы ^ Вот и наш результат. Те биты, которые были в обоих числах одинаковыми, вернули 0 (не сработала формула “один из”). А вот те, которые образовывали пару 0-1 или 1-0, в итоге превратились в единицу. В результате мы получили число 010100101. В десятичной системе ему соответствует число 165. Давай посмотрим, правильно ли мы посчитали:

public class Main {

   public static void main(String[] args) {
       System.out.println(277^432);
   }
}
Вывод в консоль:

165
Супер! Все именно так, как мы и думали :) Теперь самое время познакомиться с операциями, которые называют битовыми сдвигами. Название, в принципе, говорит само за себя. Мы возьмем какое-то число и будем двигать его биты влево и вправо :) Давай посмотрим как это выглядит:

Сдвиг влево

Сдвиг битов влево обозначается знаком << Пример:

public class Main {

   public static void main(String[] args) {
       int x = 64;//значение
       int y = 3;//количество

       int z = (x << y);
       System.out.println(Integer.toBinaryString(x));
       System.out.println(Integer.toBinaryString(z));
   }
}
В этом примере число x=64 называется значением. Именно его биты мы будем сдвигать. Сдвигать биты мы будем влево (это можно определить по направлению знака <<) В двоичной системе число 64 = 1000000 Число y=3 называется количеством. Количество отвечает на вопрос “на сколько бит вправо/влево нужно сдвинуть биты числа x” В нашем примере мы будем сдвигать их на 3 бита влево. Чтобы процесс сдвига был более понятен, посмотрим на картинке. У нас в примере используются числа типа int. Int’ы занимают в памяти компьютера 32 бита. Вот так выглядит наше изначальное число 64: Побитовые операции - 2А теперь мы, в прямом смысле слова, берем каждый из наших битов и сдвигаем влево на 3 ячейки: Побитовые операции - 3Вот что у нас получилось. Как видишь, все наши биты сдвинулись, а из-за пределов диапазона добавились еще 3 нуля. 3 — потому что мы делали сдвиг на 3. Если бы мы сдвигали на 10, добавилось бы 10 нулей. Таким образом, выражение x << y означает “сдвинуть биты числа х на y ячеек влево”. Результатом нашего выражения стало число 1000000000, которое в десятичной системе равно 512. Проверим:

public class Main {

   public static void main(String[] args) {
       int x = 64;//значение
       int y = 3;//количество

       int z = (x << y);
       System.out.println(z);
   }
}
Вывод в консоль:

512
Все верно! Теоретически, биты можно сдвигать до бесконечности. Но поскольку у нас число int, в распоряжении есть всего 32 ячейки. Из них 7 уже заняты числом 64 (1000000). Поэтому если мы сделаем, например, 27 сдвигов влево, наша единственная единица выйдет за пределы диапазона и “затрётся”. Останутся только нули!

public class Main {

   public static void main(String[] args) {
       int x = 64;//значение
       int y = 26;//количество

       int z = (x << y);
       System.out.println(z);
   }
}
Вывод в консоль:

0
Как мы и предполагали, единичка вышла за пределы 32 ячеек-битов и исчезла. У нас получилось 32-битное число, состоящее из одних нулей. Побитовые операции - 4Естественно, в десятичной системе ему соответствует 0. Простое правило для запоминания сдвигов влево: При каждом сдвиге влево выполняется умножение числа на 2. Например, попробуем без картинок с битами посчитать результат выражения 111111111 << 3 Нам нужно трижды умножить число 111111111 на 2. В результате у нас получается 888888888. Давай напишем код и проверим:

public class Main {

   public static void main(String[] args) {
       System.out.println(111111111 << 3);
   }
}
Вывод в консоль:

888888888

Сдвиги вправо

Они обозначаются знаком >>. Делают то же самое, только в другую сторону! :) Не будем изобретать велосипед и попробуем сделать это с тем же числом int 64.

public class Main {

   public static void main(String[] args) {
       int x = 64;//значение
       int y = 2;//количество

       int z = (x >> y);
       System.out.println(z);
   }
}
Побитовые операции - 5Побитовые операции - 6В результате сдвига на 2 вправо два крайних нуля нашего числа вышли за пределы диапазона и затерлись. У нас получилось число 10000, которому в десятичной системе соответствует число 16 Вывод в консоль:

16
Простое правило для запоминания сдвигов вправо: При каждом сдвиге вправо выполняется деление на два с отбрасыванием любого остатка. Например, 35 >> 2 означает, что нам нужно 2 раза разделить 35 на 2, отбрасывая остатки 35/2 = 17 (отбросили остаток 1) 17:2 = 8 (отбросили остаток 1) Итого, 35 >> 2 должно быть равно 8. Проверяем:

public class Main {

   public static void main(String[] args) {
       System.out.println(35 >> 2);
   }
}
Вывод в консоль:

8
Побитовые операции - 7

Приоритет операций в Java

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

Operator Precedence

Operators Precedence
postfix expr++ expr--
unary ++expr --expr +expr ~ !
Multiplicative * / %
additive + -
shift << >> >>>
relational < > <= >= instanceof
equality == !=
bitwise AND &
bitwise exclusive OR ^
bitwise inclusive OR |
logical AND &&
logical OR ||
ternary ? :
assignment = += -= *= /= %= &= ^= |= <<= >>= >>>=
Все операции выполняются слева направо, однако с учетом своего приоритета. Например, если мы пишем: int x = 6 - 4/2; вначале будет выполнена операция деления (4/2). Хоть она и идет второй по счету, но у нее выше приоритет. Круглые или квадратные скобки меняют любой приоритет на максимальный. Это ты наверняка помнишь еще со школы. Например, если добавить их к выражению: int x = (6 - 4)/2; первым выполнится именно вычитание, поскольку оно вычисляется в скобках. У логического оператора && приоритет довольно низкий, что видно из таблицы. Поэтому чаще всего он будет выполняться последним. Например: boolean x = 6 - 4/2 > 3 && 12*12 <= 119; Это выражение будет выполняться так:
  • 4/2 = 2

    
    boolean x = 6 - 2 > 3 && 12*12 <= 119;
    
  • 12*12 = 144

    
    boolean x = 6 - 2 > 3 && 144 <= 119;
    
  • 6-2 = 4

    
    boolean x = 4 > 3 && 144 <= 119;
    
  • Далее будут выполнены операторы сравнения:

    4 > 3 = true

    
    boolean x = true && 144 <= 119;
    
  • 144 <= 119 = false

    
    boolean x = true && false;
    
  • И, наконец, последним, будет выполнен оператор “И” &&.

    boolean x = true && false;

    boolean x = false;

    Оператор сложения (+), например, имеет более высокий приоритет, чем оператор сравнения != (“не равно”);

    Поэтому в выражении:

    boolean x = 7 != 6+1;

    сначала будет выполнена операция 6+1, потом проверка 7!=7 (false), а в конце — присваивания результата false переменной x. У присваивания вообще самый маленький приоритет из всех операций — посмотри в таблице.

Фух! Лекция у нас получилась большая, но ты справился! Если ты не до конца понял какие-то части этой и предыдущей лекций — не переживай, мы еще не раз коснемся данных тем в будущем. Вот тебе несколько полезных ссылок:
  • Отличная статья в картинках про побитовые операции
  • Логические операторы — лекция JavaRush о логических операциях. Мы до них еще нескоро дойдем, но почитать можно уже сейчас, вреда не будет
Комментарии (169)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Tony P Уровень 11
17 ноября 2022
Скажите пожалуйста, почему && выполняется последней? Если ранее было написано, что она работает по "ленивому принципу", то есть если выражение слева false правая часть не вычисляется...
Яков Мануилов Уровень 36
30 октября 2022
Спустя полгода изучения Java, я наконец-то понял что такое побитовые сдвиги, удобная штука однако! Вот только, слишком рано на курсе ее преподают, надо бы ближе к 30-35 уровню, иначе не поймешь ее
milniy87 Уровень 25
13 октября 2022
отличная статья!!!
Boris Уровень 17
7 октября 2022
Ну слава Б-гу! Я думал придётся всегда вручную переводить в DEC в BIN :)
RomaSky Уровень 12
6 сентября 2022
"Все операции выполняются слева направо, однако с учетом своего приоритета." А на курсе дана такая информация:
SM-2084 Уровень 14
23 августа 2022
Классная статья! Спасибо большое автору!
Ясин Адли Уровень 17
21 августа 2022
Иван Уровень 19
16 августа 2022
Отличная статья. Спасибо большое!!!
Dmitro Уровень 12
8 августа 2022
Стаття дуже цікава і якісно викладена. Розкриває тему побітових операцій краще ніж лекції. Автору 👍.
Alyaxandra Iuchyk Уровень 25
19 июля 2022
Исходя из таблицы, создаётся впечатление, что постфиксы приоритетнее префиксов... но они равнозначны же. Будь то постдекремент или предекремент - счёт слева направо. Возможно только у меня такое впечатление сложилось глядя на таблицу....