admin / 15.12.2017

Беззнаковая арифметика в Java / Хабр

.

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

Последнее обновление: 16.04.2018

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

Логические операции

Логические операции над числами представляют поразрядные операции. В данном случае числа рассматриваются в двоичном представлении, например, 2 в двоичной системе равно 10 и имеет два разряда, число 7 — 111 и имеет три разряда.

  • Умножение производится поразрядно, и если у обоих операндов значения разрядов равно 1, то операция возвращает 1, иначе возвращается число 0. Например:

    int a1 = 2; //010 int b1 = 5;//101 System.out.println(a1&b1); // результат 0 int a2 = 4; //100 int b2 = 5; //101 System.out.println(a2 & b2); // результат 4

    В первом случае у нас два числа 2 и 5. 2 в двоичном виде представляет число 010, а 5 — 101. Поразрядное умножение чисел (0*1, 1*0, 0*1) дает результат 000.

    Во втором случае у нас вместо двойки число 4, у которого в первом разряде 1, так же как и у числа 5, поэтому здесь результатом операции (1*1, 0*0, 0 *1) = 100 будет число 4 в десятичном формате.

  • (логическое сложение)

    Данная операция также производится по двоичным разрядам, но теперь возвращается единица, если хотя бы у одного числа в данном разряде имеется единица (операция «логическое ИЛИ»).

    Операции над примитивными типами в Java

    Например:

    int a1 = 2; //010 int b1 = 5;//101 System.out.println(a1|b1); // результат 7 — 111 int a2 = 4; //100 int b2 = 5;//101 System.out.println(a2 | b2); // результат 5 — 101

  • (логическое исключающее ИЛИ)

    Также эту операцию называют XOR, нередко ее применяют для простого шифрования:

    int number = 45; // 1001 Значение, которое надо зашифровать — в двоичной форме 101101 int key = 102; //Ключ шифрования — в двоичной системе 1100110 int encrypt = number ^ key; //Результатом будет число 1001011 или 75 System.out.println(«Зашифрованное число: » +encrypt); int decrypt = encrypt ^ key; // Результатом будет исходное число 45 System.out.println(«Расшифрованное число: » + decrypt);

    Здесь также производятся поразрядные операции. Если у нас значения текущего разряда у обоих чисел разные, то возвращается 1, иначе возвращается 0. Например, результатом выражения 9^5 будет число 12. А чтобы расшифровать число, мы применяем обратную операцию к результату.

  • (логическое отрицание)

    Поразрядная операция, инвертирующая все разряды числа: если значение разряда равно 1, то оно становится равным нулю, и наоборот.

    int a = 56; System.out.println(~a);

Операции сдвига

Операции сдвига также производятся над разрядами чисел. Сдвиг может происходить вправо и влево.

  • — сдвигает число a влево на b разрядов. Например, выражение сдвигает число 4 (которое в двоичном представлении 100) на один разряд влево, в результате получается число 1000 или число 8 в десятичном представлении.

  • — смещает число a вправо на b разрядов. Например, сдвигает число 16 (которое в двоичной системе 10000) на один разряд вправо, то есть в итоге получается 1000 или число 8 в десятичном представлении.

  • — в отличие от предыдущих типов сдвигов данная операция представляет беззнаковый сдвиг — сдвигает число a вправо на b разрядов. Например, выражение будет равно 1073741822.

Таким образом, если исходное число, которое надо сдвинуть в ту или другую строну, делится на два, то фактически получается умножение или деление на два. Поэтому подобную операцию можно использовать вместо непосредственного умножения или деления на два, так как операция сдвига на аппаратном уровне менее дорогостоящая операция в отличие от операции деления или умножения.

НазадСодержаниеВперед

Арифметические операции

Последнее обновление: 30.11.2015

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

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

Операндом является переменная или значение (например, число), участвующее в операции. Рассмотрим все виды операций.

В арифметических операциях участвуют числами. В Java есть бинарные арифметические операции (производятся над двумя операндами) и унарные (выполняются над одним операндом). К бинарным операциям относят следующие:

  • операция сложения двух чисел:

    int a = 10; int b = 7; int c = a + b; // 17 int d = 4 + b; // 11

  • операция вычитания двух чисел:

    int a = 10; int b = 7; int c = a — b; // 3 int d = 4 — a; // -6

  • операция умножения двух чисел

    int a = 10; int b = 7; int c = a * b; // 70 int d = b * 5; // 35

  • операция деления двух чисел:

    int a = 20; int b = 5; int c = a / b; // 4 double d = 22.5 / 4.5; // 5.0

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

    double k = 10 / 4; // 2 System.out.println(k);

    Чтобы результат представлял числос плавающей точкой, один из операндов также должен представлять число с плавающей точкой:

    double k = 10.0 / 4; // 2.5 System.out.println(k);

  • получение остатка от деления двух чисел:

    int a = 33; int b = 5; int c = a % b; // 3 int d = 22 % 4; // 2 (22 — 4*5 = 2)

Также есть две унарные арифметические операции, которые производятся над одним числом: ++ (инкремент) и — (декремент). Каждая из операций имеет две разновидности: префиксная и постфиксная:

  • ++ (префиксный инкремент)

    Предполагает увеличение переменной на единицу, например, (вначале значение переменной y увеличивается на 1, а затем ее значение присваивается переменной z)

    int a = 8; int b = ++a; System.out.println(a); // 9 System.out.println(b); // 9

  • ++ (постфиксный инкремент)

    Также представляет увеличение переменной на единицу, например, (вначале значение переменной y присваивается переменной z, а потом значение переменной y увеличивается на 1)

    int a = 8; int b = a++; System.out.println(a); // 9 System.out.println(b); // 8

  • уменьшение переменной на единицу, например, (вначале значение переменной y уменьшается на 1, а потом ее значение присваивается переменной z)

    int a = 8; int b = —a; System.out.println(a); // 7 System.out.println(b); // 7

  • (сначала значение переменной y присваивается переменной z, а затем значение переменной y уменьшается на 1)

    int a = 8; int b = a—; System.out.println(a); // 7 System.out.println(b); // 8

Арифметические операции вычисляются слева направо. Одни операции имеют больший приоритет чем другие и поэтому выполняются вначале. Операции в порядке уменьшения приоритета:

++ (инкремент), — (декремент)

* (умножение), / (деление), % (остаток от деления)

+ (сложение), — (вычитание)

Приоритет операций следует учитывать при выполнении набора арифметических выражений:

int a = 8; int b = 7; int c = a + 5 * ++b; System.out.println(c); // 48

Вначале будет выполняться операция инкремента , которая имеет больший приоритет — она увеличит значение переменной b и возвратит его в качестве результата. Затем выполняется умножение , и только в последнюю очередь выполняется сложение

Скобки позволяют переопределить порядок вычислений:

int a = 8; int b = 7; int c = (a + 5) * ++b; System.out.println(c); // 104

Несмотря на то, что операция сложения имеет меньший приоритет, но вначале будет выполняться именно сложение, а не умножение, так как операция сложения заключена в скобки.

НазадСодержаниеВперед

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

Существует несколько побитовых операторов, применимых к целочисленными типам long, int, short, char, byte.

~ Побитовый унарный оператор NOT
& Побитовый AND
&= Побитовый AND с присваиванием
| Побитовый OR
|= Побитовый OR с присваиванием
^ Побитовый исключающее OR
^= Побитовый исключающее OR с присваиванием
>> Сдвиг вправо
>>= Сдвиг вправо с присваиванием
>>> Сдвиг вправо с заполнением нулями
<< Сдвиг влево
<<= Сдвиг влево с присваиванием
>>>= Сдвиг вправо с заполнением нулями с присваиванием

Все целочисленные типы представляются двоичными числами различной длины. Например, значение типа byte, равное 42, в двоичном представлении имеет вид 00101010, в котором каждая позиция представляет степень числа два.

Все целочисленные типа, за исключением char — типы со знаком, т.е. могут быть положительными или отрицательными. В Java применяется двоичное дополнение, при котором отрицательные числа представляются в результате инвертирования всех битов значения (изменения 1 на 0 и наоборот) и последующего добавления 1 к результату. Например, -42 представляется в результате инвертирования всех битов в двоичном представлении числа 42, что даёт значение 11010101, и добавления 1, что приводит к значению 110110110, или -42. Чтобы декодировать отрицательное число, необходимо вначале инвертировать все биты, а затем добавить 1 к результату. Например, инвертирование значения -42, или 11010110, приводит к значению 00101001, или 41, после добавления 1 к которому мы получим 42.

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

Побитовые логические операторы

Побитовые логические операторы — это &, |, ^, ~. Побитовые операторы применяются к каждому отдельному биту каждого операнда.

Результаты выполнения побитовых логических операторов

A B A | B A & B A ^ B ~A
0 0 0 0 0 1
1 0 1 0 1 0
0 1 1 0 1 1
1 1 1 1 0 0

Побитовое OR (|)

Результирующий бит, полученный в результате выполнения оператора OR, |, равен 1, если соответствующий бит в любом из операндов равен 1.

00101010 42 | 00001111 15 ————— 00101111 47

Побитовое AND (&)

Значение бита, полученное в результате выполнения побитового оператора AND, &, равно 1, если соответствующие биты в операндах также равны 1. Во всех остальных случаях значение результирующего бита равно 0.

00101010 42 & 00001111 15 ————— 00001010 10

Побитовое XOR (^)

Результирующий бит, полученный в результате выполнения оператора XOR, ^, равен 1, если соответствующий бит только в одном из операндов равен 1. Во всех других случаях результирующий бит равен 0.

00101010 42 ^ 00001111 15 ————— 00100101 37

Данный оператор обладает полезной особенностью, часто используемый в программировании. Например, можно обменять значения переменных без использования дополнительной переменной

Но увлекаться такой записью не стоит, это работает медленнее и код менее читаем.

Гораздо шире используется XOR для шифрования. В простейшем виде это выглядит так. Есть текст, к которому применяется ключ с оператором XOR. Зашифрованное сообщение передаётся человеку, который знает ключ. Всё, что ему нужно сделать — это применить к зашифрованному тексту тот же ключ, используемый для шифровки и текст снова станет читаемым.

Ниже приводится приблизительный пример работы шифровки/дешифровки текста.

Java 8 операции

С русским текстом пример не будет работать, так как используются разные кодировки и требуется писать дополнительный код. Итак, у нас есть некий текст, который мы зашифровали с помощью ключа (meow) и передали полученный набор байтов другому человеку. Если кто-то перехватит ваше сообщение, то увидит просто набор символов. А ваш сообщник сможет прочитать сообщение, так как вы заранее условились, каким будем ключ для расшифровки.

Побитовое NOT (~)

Унарный оператор NOT (Не), ~, называемый также побитовым дополнением, инвертирует все биты операнда. Например, число 42 в битовом представлении имеет вид:

00101010

В результате применения оператора NOT преобразуется в значение:

11010101

Сдвиг влево

Оператор сдвига влево, <<, смещает все биты влево на указанное количество позиций:

значение << количество

Параметр количество указывает, на сколько нужно сдвинуть влево биты в параметре значение. При каждом сдвиге влево самый старший бит смещается за пределы допустимого значения и теряется, а справа дописывается нуль. Это означает, что при применении оператора сдвига влево к операнду типа int биты теряются, как только они сдвигаются за пределы 31 позиции. Если операнд имеет тип long, биты теряются после сдвига за пределы 63 позиции.

Автоматическое повышение типа, используемое в Java, может привести к странным результатам при выполнении сдвига в значениях типа byte, short.

При вычислении выражений тип значений byte и short повышается до типа int. Это означает, что результатом выполнения сдвига влево значения типа byte или short будет значение int, и сдвинутые влево позиции не будут отброшены до тех пор, пока они не будут сдвинуты за пределы 31 позиции. Более того, при повышении до типа int отрицательное значение типа byte или short получит дополнительный знаковый разряд. Следовательно, старшие биты будут заполнены единицами. Поэтому выполнение оператора сдвига влево предполагает необходимость отбрасывания старших байтов результата типа int. Иными словами, при выполнении сдвига влево в значении типа byte сначала будет повышение до типа int и лишь затем сдвиг. Это означает, что для получения требуемого сдвинутого значения типа byte необходимо отбросить три старших байта результата. Простейший способ достижения этого — обратное приведение результата к типу byte.

Результат будет следующим:

i равно: 256 y равно: 0

Поскольку для выполнения вычислений тип переменной повышается до int, сдвиг влево на две позиции значение 64 (0100 0000) приводит к значению 256 (1 0000 0000). Однако, переменная y содержит значение не 256, а 0, поскольку после сдвига крайний единичный бит оказывается сдвинутым за пределы допустимого диапазона.

Приёмом сдвига влево часто пользуются в программах, где происходит много сложных вычислений. По сути, это замена операции умножения на 2, которая в силу особенностей процессора гораздо эффективнее. При этом следует соблюдать осторожность, так как при сдвиге единичного бита в старшую позицию (бит 31 или 63) значение становится отрицательным.

Сдвиг вправо

Оператор сдвига вправо, >>, смещает все биты значения вправо на указанное количество позиций:

значение >> количество

Вы указываете количество позиций, на которое нужно сдвинуть вправо биты в значении. Следующий код выполняет сдвиг право на две позиции в значении 32, в результате чего значение переменной станет равным 8.

Крайние биты при сдвиге просто теряются. Например, значение 35 при сдвиге вправо на две позиции также приводит к значению 8, так как теряются два младщих бита.

00100011 35 >>2 ———- 00001000 8

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

При выполнении сдвига вправо старшие (расположенные в крайних левых позициях) биты, освобожденные в результате сдвига, заполняются предыдущим содержимым старшего бита. Этот эффект называется дополнительных знаковым разрядом и служит для сохранения знака отрицательных чисел при их сдвиге.

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

Иногда при выполнении сдвига вправо появление дополнительных знаковых разрядов нежелательно.

В этом случае используется маскировка за счёт объединения значения со значением 0x0f оператором AND, что приводит к отбрасыванию любых битов дополнительных знаковых разрядов.

Сдвиг вправо без учёта знака

Как было показано выше, при каждом выполнении оператор >> автоматически заполняет старший бит его предыдущим содержимым. В результате знак значения сохраняется. Однако иногда это нежелательно. Например, при работе со значениями пикселей и графическими изображениями. Как правило, в этих случаях требуется сдвиг нуля в позицию старшего бита независимо от его первоначального значения. Такое действие называют сдвигом вправо без учета знака, когда всегда вставляется нуль в позицию старшего бита.

Допустим, мы хотим сдвинуть вправо на 24 бит значение -1 (в двоичном представлении 11111111 11111111 11111111 11111111):

11111111 11111111 11111111 11111111 >>> 24 ————————————— 00000000 00000000 00000000 11111111 255 в двоичном виде типа int

Оператор >>> не столь полезен, поскольку имеет смысл только для 32- и 64-разрядных значений.

Побитовые составные операторы с присваиванием

Двоичные побитовые операторы имеют составную форму, которая объединяет побитовый оператор с оператором присваивания.

Реклама

Статья проплачена кошками — всемирно известными производителями котят.

Если статья вам понравилась, то можете поддержать проект.

FILED UNDER : IT

Submit a Comment

Must be required * marked fields.

:*
:*