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

Статья из группы 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 о логических операциях. Мы до них еще нескоро дойдем, но почитать можно уже сейчас, вреда не будет
Комментарии (152)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Firegears Уровень 11, Харьков, Украина
15 июня 2022
Уважаемые админы! Сделайте эту статью основной в курсе. Поменяете ту муть что в лекциях дана! Это же просто godsend! Все rookie-friendly! просто и понятно. Автор, дай Бог здоровья!
Тёма Панфилов Уровень 13, Подольск
7 июня 2022
Отлично!
Tomhetted Уровень 12, Санкт-Петербург , Россия
1 июня 2022
Пример с оператором &: "Как видишь, только в двух случаях оба бита в числах были равны 1 (первый и пятый по счету биты)." Разве не 5-й и 9-й? Ведь порядок бит идет справа налево.
Богдан Бородулін Уровень 2, Ukraine
30 мая 2022
Какое практическое применение побитовых операций?
FutureDev Уровень 17, Ukraine
5 мая 2022
Для тех кто не понял приоритетности инкремента/декремента. Логика вычисления довольно простая: 1. Сначала "пробегаем" слева направо (как сказано в лекции) по всем переменным и применяем где нужно инкремент/декремент. На скобки не обращаем внимания! 2. Приступаем к арифметическим вычислениям. Но уже принимаем во внимание скобки, т.к. они задают первоочередный приоритет! 3. И далее по списку с таблицы. Примеры:

int x = 2;
int y = x + ++x + ++x;
//  y = 2 + 3 + 4;
//  y = 9

int x = 2;
int y = x + (++x + ++x);
//  y = 2 + (3 + 4);
//  y = 9

int x = 2;
int y = x * (++x + ++x);
//  y = 2 * (3 + 4);
//  y = 14
И ещё наглядный пример разницы между префиксным (++x) и постфиксным (x++) инкрементом:

int x = 2;
int y = x + ++x + x;
//  y = 2 + 3 + 3;
//  y = 8

int y = x + x++ + x;
//  y = 2 + 2 + 3;
//  y = 7
P.S. с декрементом (--x / x--) ситуация аналогичная 😉
ZevoFF Уровень 8, Киев
12 марта 2022

int number = 2;
         number  =number + number + ++number*2;
Материал с ошибкой ., так как сначала выполняется левая часть до плюса, и уже потом согласно таблице , то есть в Этом примере ответ будет 2+2+(2+1)*2 = 10 , а не как в лекции 3 +3+6=12..
SerVit Уровень 9, Минск, Беларусь
1 марта 2022
Прочитал статью в первый раз, запомнил материал на 60%. Прочитал статью на следующий день - запомнил на 100%. Повторение - мать учения! ))
Dffay Уровень 9
14 февраля 2022
Подскажите эта тема, а именно побитовые операции, пригодятся в реальном программировании?
AV Уровень 28, Russian Federation
12 февраля 2022
В таблице приоритетов можно название операторов перевести на русский язык (что уже сделано в лекциях). Это упростит понимание.
Yulia Kull Уровень 32, Tallinn, Estonia
28 января 2022
Статья гораздо круче чем лекции! Автор, спасибо!