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

Какие бывают операторы в Java

Для начала определимся с терминологией. Для осуществления любой операции нам нужно как минимум две вещи:
  • оператор;
  • операнд.
Примером оператора может быть простой плюс в операции сложения двух чисел. А складываемые друг с другом числа будут в этом случае операндами. Итак, с помощью операторов мы выполняем операции над одним или несколькими операндами. Операторы, которые осуществляют операции над двумя операндами, называются бинарными. Например, сложение двух чисел. Операторы, которые осуществляют операции над одним операндом, называются унарными. Например, унарный минус.

Операторы Java в курсе JavaRush

Несколько лекций посвящено операторам Java на четвертом уровне первого квеста — Java Syntax. В частности, условным операторам, типу boolean. В курсе есть 22 задачи, которые помогут разобраться с работой операторов сравнения, условных, логических операторов.

Операции над числами в Java

Самая частая операция, которую программисты производят над числами — присвоение числового значения какой либо переменной. Данная операция, как и оператор = тебе уже знакомы:
int a = 1;
int b = 2;
int c = 3;
Есть также арифметические операции. Они осуществляются с помощью специальных бинарных арифметических операторов:
Таблица 1. Бинарные арифметические операторы
Первые четыре оператора не должны вызывать вопросов: все так же, как в математике. Последний оператор, остаток от деления, также не делает ничего сверхсложного. К примеру, если разделить 24 на 7, мы получим 3 целых и 3 в остатке. Именно остаток и вернет данный оператор:
System.out.println(24 % 7); // выведет 3
Вот примеры с сайта официальной документации Oracle:
class ArithmeticDemo {

	public static void main (String[] args) {

    	int result = 1 + 2;
    	// result is now 3
    	System.out.println("1 + 2 = " + result);
    	int original_result = result;

    	result = result - 1;
    	// result is now 2
        System.out.println(original_result + " - 1 = " + result);
    	original_result = result;

    	result = result * 2;
    	// result is now 4
	    System.out.println(original_result + " * 2 = " + result);
    	original_result = result;

    	result = result / 2;
    	// result is now 2
        System.out.println(original_result + " / 2 = " + result);
    	original_result = result;

    	result = result + 8;
    	// result is now 10
        System.out.println(original_result + " + 8 = " + result);
    	original_result = result;

    	result = result % 7;
    	// result is now 3
   	 System.out.println(original_result + " % 7 = " + result);
	}
}
Данная программа выведет следующее: 1 + 2 = 3 3 - 1 = 2 2 * 2 = 4 4 / 2 = 2 2 + 8 = 10 10 % 7 = 3 Java позволяет комбинировать: например, операторы присваивания и арифметические операторы. Рассмотрим пример:
int x = 0;
x = x + 1; // x = 0 + 1 => x = 1
x = x + 1; // x = 1 + 1 => x = 2
x = x + 1; // x = 2 + 1 => x = 3
Здесь мы задали переменную x и присвоили ей нулевое значение. Потом идут три строчки. В каждой строке мы присваивали значению x сумму текущего значения переменной x и единицы. В комментариях к каждой строке есть пояснения. В узких кругах данную процедуру называют наращиванием или инкрементированием переменной. Операцию инкрементирования из примера выше можно заменить на аналогичную с использованием комбинации операторов:
int x = 0;
x += 1; // x = 0 + 1 => x = 1
x += 1; // x = 1 + 1 => x = 2
x += 1; // x = 2 + 1 => x = 3
Комбинировать оператор присваивания можно с любым арифметическим оператором:
int x = 0;
x += 10; // x = 0 + 10 => x = 10
x -= 5; // x = 10 - 5 => x = 5
x *= 5; // x = 5 * 5 => x = 25
x /= 5; // x = 25 / 5 => x = 5
x %= 3; // x = 5 % 3 => x = 2;
Продемонстрируем работу последнего примера: Помимо бинарных, в Java есть унарные арифметические операторы.
Таблица 2. Унарные арифметические операторы:
Пример унарных плюса и минуса:
int x = 0;
x = (+5) + (+15); //Скобки для наглядности, можно и без них
System.out.println("x = " + x);

int y = -x;
System.out.println("y = " + y);
Операции инкремента и декремента по сути просты. В первом случае происходит увеличение переменной на 1, во втором — уменьшение переменной на 1. Пример ниже:
int x = 9;
x++;
System.out.println(x); // 10

int y = 21;
y--;
System.out.println(y); // 20
Есть два типа данных операций. Постфиксная и префиксная. Здесь тоже нет ничего сложного. В первом случае оператор пишется после переменной, во втором случае — перед переменной. Разница лишь в том, когда выполнится операция инкрементирования или декрементирования. Пример и описание в таблице ниже. Предположим, у нас есть переменная:
int a = 2;
Тогда:
Таблица 3. Операторы инкремента-декремента:
Демонстрация: Помимо арифметических операций, существуют операции сравнения. Как можно понять из их названия, данные операции сравнивают два числа. Результатом операции всегда является истина либо ложь (true / false).
Таблица 4. Операторы сравнения
Примеры:
int a = 1;
int b = 2;

boolean comparisonResult = a == b;
System.out.println("a == b :" + comparisonResult);

comparisonResult = a != b;
System.out.println("a != b :" + comparisonResult);

comparisonResult = a > b;
System.out.println("a >  b :" + comparisonResult);

comparisonResult = a >= b;
System.out.println("a >= b :" + comparisonResult);

comparisonResult = a < b;
System.out.println("a <  b :" + comparisonResult);

comparisonResult = a <= b;
System.out.println("a <= b :" + comparisonResult);
Демонстрация:

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

Рассмотрим логические операции и таблицы истинности каждой из них:
  • операция отрицания (NOT);
  • операция конъюнкции, логическое И (AND);
  • операция дизъюнкции, логическое ИЛИ (OR);
  • операция сложения по модулю, исключающее ИЛИ (XOR).
Операция отрицания унарная и применяется к одному операнду. Все остальные операции — бинарные. Рассмотрим таблицы истинности данных операций. Здесь 0 — аналог значения false в Java, а 1 — значения true.
Таблица 5. Таблица истинности оператора отрицания (NOT)
Таблица 6. Таблица истинности оператора конъюнкции (AND)
Таблица 7. Таблица истинности оператора дизъюнкции (OR)
Таблица 8. Таблица истинности оператора сложения по модулю (XOR)
В Java есть те же логические операции:
  • ! — оператор отрицания;
  • && — оператор логическое И (сокращенный);
  • || — оператор логическое ИЛИ (сокращенный);
  • & — оператор побитовое И;
  • | — оператор побитовое ИЛИ;
  • ^ — оператор побитовое исключающее ИЛИ.
Разницу между побитовыми и сокращенными операторами рассмотрим чуть ниже, пока давай преобразуем все таблицы истинности в Java код:
public class LogicDemo {

   public static void main(String[] args) {
   	notExample();
   	andExample();
   	orExample();
   	xorExample();
   }

   public static void notExample() {
   	System.out.println("NOT EXAMPLE:");
   	System.out.println("NOT false = " + !false);
       System.out.println("NOT true  = " + !true);
   	System.out.println();
   }

   public static void andExample() {
   	System.out.println("AND EXAMPLE:");
   	System.out.println("false AND false = " + (false & false));
   	System.out.println("false AND true  = " + (false & true));
   	System.out.println("true  AND false = " + (true & false));
   	System.out.println("true  AND true  = " + (true & true));
   	System.out.println();
   }

   public static void orExample() {
   	System.out.println("OR EXAMPLE:");
   	System.out.println("false OR false = " + (false | false));
   	System.out.println("false OR true  = " + (false | true));
   	System.out.println("true  OR false = " + (true | false));
  	 System.out.println("true  OR true  = " + (true | true));
   	System.out.println();
   }

   public static void xorExample() {
   	System.out.println("XOR EXAMPLE:");
   	System.out.println("false XOR false = " + (false ^ false));
   	System.out.println("false XOR true  = " + (false ^ true));
   	System.out.println("true  XOR false = " + (true ^ false));
   	System.out.println("true  XOR true  = " + (true ^ true));
   	System.out.println();
   }
}
Данная программа выведет на экран: NOT EXAMPLE: NOT false = true NOT true = false AND EXAMPLE: false AND false = false false AND true = false true AND false = false true AND true = true OR EXAMPLE: false OR false = false false OR true = true true OR false = true true OR true = true XOR EXAMPLE: false XOR false = false false XOR true = true true XOR false = true true XOR true = false Как можно понять из примеров, логические операторы применимы только к boolean переменным. В нашем случае мы применяли их сразу к значениям, но можно смело применять их и к boolean переменным: И к boolean выражениям: Теперь, как ты наверное заметил, у нас есть сокращенные операторы (&&, ||) и аналогичные побитовые операторы (&, |). В чем между ними разница? Во-первых, побитовые можно применять к целым числам. Об этом мы поговорим чуть позже. А во-вторых, в том, что одни сокращенные, а другие — нет. Чтобы понять, как выглядит сокращенность, посмотрим на выражение:
false AND x = ?
true OR x = ?
Здесь x может принимать любое булево значение. И в целом, согласно законам логики и таблицам истинности, независимо от того, будет x true или false, результатом первого выражения будет false, а результатом второго будет true. Не веришь? Смотри. А это значит, что бывают случаи, когда уже по первому операнду становится ясно, каким будет результат выражения. Этим и отличаются сокращенные операторы && и ||. В выражениях, аналогичных описанным выше, они не вычисляют значение второго операнда. Вот небольшой пример, чтобы ты понял, как это работает: В случае с сокращенными операторами не вычисляется вторая часть выражения. Но происходит это только тогда, когда результат выражения очевиден уже по первому операнду.

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

Ну вот мы и подобрались к самому интересному: побитовым операциям. Как можно понять из названия, это операции, которые производятся над битами. Но прежде чем мы погрузимся в эту тему, стоит поговорить о смежных областях.

Представление чисел в двоичной системе счисления

Числа, как и любая другая информация в программе, хранится в памяти компьютера в двоичном коде. Двоичный код — набор нулей и единиц. Каждый отдельно взятый ноль или единица представляют собой единицу измерения информации, которая называется бит. Согласно Википедии: Бит (русское обозначение: бит; международное: bit; от англ. binary digit — двоичное число; также игра слов: англ. bit — кусочек, частица) — единица измерения количества информации. 1 бит информации — это символ или сигнал, который может принимать два значения: включено или выключено, да или нет, высокий или низкий, заряженный или незаряженный; в двоичной системе исчисления это 1 (единица) или 0 (ноль).

С какими данными работают побитовые операторы?

Побитовые операции в Java осуществляются только над целыми числами. А целые числа (как впрочем и все остальное) хранятся в памяти компьютера в виде набора битов. Можно сказать, что компьютер переводит любую информацию в двоичную систему счисления (в набор битов) и только потом взаимодействует с ней. Но как устроена двоичная система счисления? Можно сказать, точно так же, как и привычная нам десятичная система исчисления. В десятичной системе счисления у нас есть всего 10 символов: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9. С помощью этих символов мы ведем счет. После 9 идет 10, после 19 — 20, после 99 — 100, после 749 — 750. То есть мы используем комбинацию имеющихся 10 символов и можем с их помощью считать «от нуля и до обеда». В двоичной системе счисления все так же, но вместо десяти символов в нашем распоряжении всего два — 0, 1. Но комбинируя эти символы по тому же принципу что и в десятичной системе, можем считать бесконечно долго.
Продемонстрируем счет от 0 до 15 в десятичной системе и в двоичной:
Как видим, все не так уж и сложно. Помимо битов, есть другие знакомые единицы измерения информации — байты, килобайты, мегабайты, гигабайты и тд. Ты, наверно, знаешь, что в 1 байте — 8 бит. Что это значит? Это значит, что 8 битов подряд занимают 1 байт. Вот примеры, какими могут быть байты:
00000000 - 1 байт
10110010 - 1 байт
01011011 - 1 байт
Количество возможных неповторяющихся комбинаций битов в одном байте — 256 (28 = 256). Но вернемся ближе к Java. Есть такой целочисленный тип данных — byte. Данный тип может принимать значения от -128 до 127 и одно число в памяти компьютера занимает ровно 8 бит, или 1 байт, или, если по английски 1 byte. Одно число этого типа занимает ровно 1 byte памяти компьютера. И здесь названия совпадают не случайно. Как мы помним, 1 байт может хранить 256 различных значений (различных комбинаций битов). И одно число типа byte может принимать 256 различных значений (128 отрицательных, 127 положительных и 1 ноль). Каждому значению числа byte соответствует уникальный набор из восьми битов. Так обстоят дела не только с типом byte, но и со всеми целочисленными типами. Тип byte приведен в пример как самый маленький. Ниже в таблице представлены все целочисленные типы Java и занимаемое ими место в памяти: Рассмотрим, к примеру, тип int. Он может хранить в себе 2147483648 отрицательных, 2147483647 положительных значений и один ноль. Итого:
2147483648 + 2147483647 + 1 = 4294967296.
Данный тип занимает в памяти компьютера 32 бита. Количество возможных комбинаций из набора 32-ух нулей и единиц равно:
232 = 4294967296.
То же число, что и у количества значений, вмещаемых в тип int. Это всего лишь демонстрация взаимосвязи между диапазоном значений типа данных и его размером (количество бит в памяти). Любое число любого типа в Java можно перевести в двоичную систему счисления. Давай посмотрим, как легко это можно сделать это с помощью Java языка. Будем учиться на примере типа int. У данного типа есть свой класс-обертка — Integer. А у данного класса есть метод toBinaryString, который и сделает за нас всю работу: Вуаля — все не так уж и сложно. Но все-таки кое-что следует уточнить. int число занимает 32 бита. Но когда мы выводим число 10 в примере выше, мы видим в консоли 1010. Это потому, что ведущие нули не выводятся на печать. Если бы они выводились, вместо 1010 мы бы видели в консоли 00000000000000000000000000001010. Но для удобства восприятия все ведущие нули опускаются. Не так уж и сложно до тех пор, пока не задашься вопросом: а что с отрицательными числами? Компьютером любая информация воспринимается только в двоичной системе. Получается, что знак минус также необходимо прописывать двоичным кодом. Это можно сделать с помощью прямого или дополнительного кода.

Прямой код

Прямой код — это способ представления чисел в двоичной системе счисления, при котором старший разряд (крайний левый бит) отводится под знак числа. Если число положительное, в крайний левый бит записывается 0, если отрицательное — 1.
Рассмотрим это на примере 8-ми битного числа:
Подход несложный и в принципе понятный. Однако у него есть недостатки: возникают трудности с выполнением математических операций. К примеру со сложением отрицательных и положительных чисел. Их нельзя складывать просто так, если не провести дополнительные манипуляции.

Дополнительный код

Используя дополнительный код, можно избежать недостатков прямого кода. Для получения дополнительного кода числа есть несложный алгоритм. Попробуем получить дополнительный код числа -5. Представим это число с помощью дополнительного кода в двоичной системе счисления. Шаг 1. Получаем представление отрицательного числа с помощью прямого кода. Для -5 это будет 10000101. Шаг 2. Инвертируем все разряды, кроме разряда знака. Заменим все нули на единицы, а единицы на нули везде, кроме крайнего левого бита.
10000101 => 11111010
Шаг 3. К полученному значению прибавим единицу:
11111010 + 1 = 11111011
Готово. Мы получили значение числа -5 в двоичной системе счисления с использованием дополнительного кода. Это важно для понимания дальнейшего материала, так как в Java для хранения отрицательных чисел в битах используется дополнительный код.

Типы побитовых операций

Теперь, когда мы разобрались со всеми вводными, поговорим о побитовых операциях в Java. Побитовая операция осуществляется над целыми числами, и ее результатом будет целое число. В процессе число переводится в двоичную систему, над каждым битом выполняется операция, и результат приводится обратно в десятичную систему. Список операций — в таблице ниже: Как мы уже выяснили числа можно представить в виде набора битов. Побитовые операции осуществляют операции как раз над каждым битом такого представления. Возьмем первые 4 операции из таблицы выше: NOT, AND, OR, XOR. Вспомним, что совсем недавно мы рассматривали таблицы истинности, только для логических операндов. В данном случае те же операции применяются к каждому биту целого числа.

Побитовый унарный оператор NOT ~

Данный оператор «инвертирует» все биты числа на обратные: заменяет все нули на единицы, а единицы — на нули. Предположим, у нас есть число 10 в десятичной системе исчисления. В двоичной системе это число равно 1010. Если применить к данному числу унарный побитовый оператор отрицания, мы получим примерно следующее: Давай взглянем как это выглядит в Java коде:
public static void main(String[] args) {
   int a = 10;

   System.out.println(" a = " + a + "; binary string: " + Integer.toBinaryString(a));
   System.out.println("~a = " + ~a + "; binary string: " + Integer.toBinaryString(~a));
}
Теперь посмотрим, что выведется в консоль: В первой строке мы получили значение в двоичной системе счисления без ведущих нулей. Но несмотря на то, что мы их не видим, они на самом деле есть. Об этом свидетельствует вторая строка, которой все биты трансформировались в обратные. Именно поэтому мы видим так много ведущих единиц. Это бывшие ведущие нули, которые игнорировались компилятором при выводе в первой строки. Вот небольшая программа, которая выводит для наглядности еще и ведущие нули. Давай посмотрим, как она работает:

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

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

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

Данный оператор применим к двум числам. Он производит операцию OR между битами каждого числа. Рассмотрим тот же пример: И взглянем на то, как бы это выглядело в IDEA: Не будем забывать, что результатом побитовой операции является целое число. Биты мы так подробно рассматриваем для наглядности.

Побитовая операция, исключающее ИЛИ (XOR)

Взглянем на тот же пример, но с новой операцией: Пример кода:

Побитовый сдвиг влево

Данный оператор применим к двум операндам, то есть в операции x << y, биты числа x сдвинутся на y позиций влево. Что это значит? Рассмотрим на примере операции 10 << 1 Результатом операции будет число 20 в десятичной системе. Как видно из схемы выше, все биты сдвигаются влево на 1. При этой операции значение старшего бита (крайнего левого) теряется. А самый младший бит (крайний правый) заполняется нулем. Что можно сказать об этой операции? Три важных пункта:
  1. В общем, сдвигая биты числа X на N битов влево мы умножаем число X на 2N.

    Вот пример, где это демонстрируется. Каждый сдвиг — это умножение на 2:

  2. Но! У нас может измениться знак числа, если бит со значением 1 займет крайнее левое положение.

  3. Если осуществлять сдвиг влево бесконечно долго, в какой то момент число просто превратится в 0. Продемонстрируем пункты 2 и 3:

Побитовый сдвиг вправо

Данный оператор применим к двум операндам. Т.е. в операции x >> y, биты числа x сдвинутся на y позиций вправо. Рассмотрим другой пример. Схематично разберем операцию 10 >> 1. Сдвинем все биты числа 10 на одну позицию вправо: Из этой схемы можно заметить, что при операции сдвига мы теряем правые биты. Они попросту исчезают. Как мы помним, крайний левый бит — показатель знака числа (0 — число положительное, 1 — отрицательное). Поэтому крайний левый бит в итоговом значении ставится таким же, как и в исходном числе. Рассмотрим схематичный пример с отрицательным числом: И снова мы можем увидеть, что крайний правый бит потерялся, а крайний левый бит был скопирован из исходного числа, как почетный показатель знака числа. Как это все осуществить в IDEA? В принципе, ничего сложного, просто берем и сдвигаем: Теперь. Что можно сказать о числах, над которыми осуществляется сдвиг вправо? Они делятся на 2. Каждый раз, осуществляя сдвиг на один бит вправо мы делим исходное число на 2. Если число нацело на 2 не делится, то, можно сказать, что результат будет округлен в сторону минус бесконечности (в меньшую сторону). Но это работает, только если мы сдвигаем биты ровно на 1. А если на 2 бита, делим на 4. На 3 бита — делим на 8. На 4 бита — на 16. Видишь? Степени двойки… При сдвиге числа X на N битов вправо, мы делим число X на 2 в степени N. Вот небольшая программа для того, чтобы тебе это продемонстрировать:
public class BitOperationsDemo {

   public static void main(String[] args) {

   	for (int i = 1; i <= 10; i++) {

       	int shiftOperationResult = 2048 >> i;
       	int devideOperationResult = 2048 / (int) Math.pow(2, i);


           System.out.println(shiftOperationResult + " - " + devideOperationResult);
   	}

   }

}
Что тут происходит?
  1. Цикл, в котором переменная i наращивается от 1 до 10.

  2. Каждую итерацию мы вычисляем 2 значения:
    • в переменную shiftOperationResult записываем результат сдвига числа 2048 на i битов вправо;

    • в переменную devideOperationResult записываем результат деления числа 2048 на 2 в степени i.

  3. Попарно выводим два полученных значения.

Результат выполнения программы таков: 1024 - 1024 512 - 512 256 - 256 128 - 128 64 - 64 32 - 32 16 - 16 8 - 8 4 - 4 2 - 2

Побитовый сдвиг вправо с заполнением нулями

Если обычный сдвиг битов вправо сохраняет знак числа (старший бит сохраняет свое значение), в случае со сдвигом вправо с заполнением нулями этого не происходит. А происходит заполнение старшего бита нулем. Давай посмотрим, как это выглядит:

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

Как и в математике, в Java есть приоритет операций. В таблице ниже приведен приоритет (от высшего к низшему) рассмотренных нами операций.

Полезные примеры использования

Определение четности числа

public class OperationsDemo {
   public static void main(String[] args) {

   	int a = 15;

   	if (a % 2 == 0) {
       	System.out.println("a is even");
   	} else {
       	System.out.println("a is odd");
   	}

   }
}

Поиск максимального элемента в массиве

public class OperationsDemo {
   public static void main(String[] args) {

   	int[] array = {1, 2, 3, 100, 4, 5, 6};

   	// Assuming array is not emtpy
   	int max = array[0];
   	for (int a : array) {
       	if (a > max) {
           	max = a;
       	}
   	}

   	System.out.println("Array maximum is: " + max);
   }
}
Для поиска минимального элемента просто меняем знак сравнения в нужном месте.

Подсчет чего либо (например, нечетных элементов массива)

public class OperationsDemo {
   public static void main(String[] args) {

   	int[] array = {1, 2, 3, 100, 4, 5, 6};

   	int countOdd = 0;
   	for (int i = 0; i < array.length; i++) {
       	if (array[i] % 2 != 0) {
           	countOdd ++ ;
       	}
   	}

   	System.out.println("Array has " + countOdd + " odd elements");
   }
}
Что же, мы рассмотрели, какие бывают операторы Java и на примерах разобрали применение побитовых операций. С последними все не так просто, ведь в реальном проекте они могут ухудшить читаемость кода. Когда придет время, ты сам поймешь, в каком случае лучше использовать побитовую операцию.

Ссылки на дополнительное чтение:

Операции над числами в Java Побитовые операции