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

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

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

Логические и другие операторы в Java

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

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

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

После урока вы сможете записать условие словами и кодом, проверить граничные случаи и объяснить типичные ошибки. Примеры рассчитаны на JDK 17 или новее без preview. Работа с отдельными битами и сдвигами подробно разобрана в [предыдущем уроке](/lessons/java/java-for-developers/java-developer-13); здесь она нужна лишь для сравнения с логикой boolean.

Оператор, операнд и выражение

В записи price * quantity знак * — оператор, а price и quantity — операнды. Унарная операция использует один операнд, например !enabled или -amount. Бинарная — два, как a + b. Условная запись condition ? first : second содержит три части и выбирает значение по условию.

У выражения есть тип и результат; некоторые выражения дополнительно меняют переменные или вызывают методы. При чтении задавайте три вопроса: какой тип получится, какое значение будет вычислено и есть ли побочный эффект? Обзор основных групп приведён в Oracle Operators.

ЗадачаПримеры записиЧто получаем
Арифметикаa + b, a / b, a % bЧисло; + также соединяет строки
Сравнениеa < b, a >= b, a == b, a != bboolean
Логика!enabled, ready && valid, логическое ORboolean
Присваиваниеcount = 2, count += 3Новое состояние переменной
Изменение на единицуcount++, --countИзменение переменной и значение выражения
Выбор значенияvalid ? "yes" : "no"Одна из двух ветвей
Проверка или приведение типаvalue instanceof String, (int) amountboolean либо значение указанного типа

Логические операции: !, &&, || и XOR

boolean имеет значения true и false. Отрицание ! меняет их местами. AND, записываемое &&, требует истинности обеих частей. OR, записываемое ||, требует хотя бы одной истинной части. Для двух boolean оператор ^ выражает исключающее OR: результат истинен, когда значения различаются. Он не означает возведение в степень.

ab!aANDORXOR
falsefalsetruefalsefalsefalse
falsetruetruefalsetruetrue
truefalsefalsefalsetruetrue
truetruefalsetruetruefalse

Предположим, пройти на мероприятие можно по билету или приглашению, но заблокированному посетителю вход закрыт в любом случае. Сначала выделим альтернативы в скобки, затем добавим обязательное ограничение. Сохраните AdmissionRules.java:

public class AdmissionRules {
    private static boolean canEnter(boolean hasTicket, boolean invited, boolean blocked) {
        return (hasTicket || invited) && !blocked;
    }

    public static void main(String[] args) {
        boolean hasTicket = true;
        boolean invited = false;
        boolean blocked = true;

        System.out.println(canEnter(hasTicket, invited, blocked));
        System.out.println(hasTicket || invited && !blocked);
        System.out.println(canEnter(false, true, false));
        System.out.println(canEnter(false, false, false));
    }
}

Запуск:

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

Вывод: false, true, true, false на отдельных строках. Во второй строке намеренно записано неверное для нашей задачи условие: && связывает приглашение и отсутствие блокировки, а билет сам по себе даёт true. Скобки в первой версии распространяют проверку блокировки на оба способа входа. Остальные примеры также сохраняйте под именем публичного класса и собирайте той же командой, заменив имя файла.

Для чистых boolean-значений полезны законы де Моргана: !(a && b) соответствует !a || !b, а !(a || b) — !a && !b. Это помогает проверить отрицание требования словами. Но сначала дайте сложным проверкам понятные имена: длинную формулу с несколькими отрицаниями трудно читать даже при правильном преобразовании.

Короткое замыкание и безопасные проверки

У && правая часть пропускается, если слева false; у || — если слева true. Операторы & и | с boolean вычисляют обе части. Условия с вызовами методов могут поэтому давать одинаковый логический ответ, но разное поведение. Объяснение условного вычисления приведено в Oracle Conditional Operators.

В GuardedConditions.java отсутствие настройки enabled будем трактовать как выключенное состояние. Проверка текста выполняется только после проверки настройки и null:

public class GuardedConditions {
    private static boolean isReady(String text, Boolean enabled) {
        return Boolean.TRUE.equals(enabled) && text != null && !text.isBlank();
    }

    public static void main(String[] args) {
        System.out.println(isReady(null, true));
        System.out.println(isReady("   ", true));
        System.out.println(isReady("Java", true));
        System.out.println(isReady("Java", null));

        String missing = null;
        try {
            boolean eager = missing != null & !missing.isBlank();
            System.out.println(eager);
        } catch (NullPointerException exception) {
            System.out.println("right operand evaluated");
        }
    }
}

Получим false, false, true, false и right operand evaluated. Последняя часть намеренно показывает ошибку: одиночный & заставляет вызвать isBlank на null. Перехват исключения здесь нужен только для демонстрации; правильная проверка находится в isReady.

Boolean — ссылочный тип. При использовании его в условии происходит распаковка, и null вызывает NullPointerException. Это правило JLS об unboxing. Выражение Boolean.TRUE.equals(enabled) реализует выбранную нами политику «неизвестно значит выключено»; если отсутствие настройки считается ошибкой, его следует проверять отдельно. В Java нет автоматической «истинности» чисел и строк: if (1) не компилируется.

Арифметика: тип результата выбирается до присваивания

Операторы +, -, *, / и % выполняют обычные арифметические действия с учётом типов. Для целых / отбрасывает дробную часть в сторону нуля, а % возвращает остаток; ненулевой целочисленный остаток имеет знак делимого. Поэтому −5 / 2 равно −2, а −5 % 2 равно −1. Основные обозначения перечислены в Oracle Arithmetic Operators.

public class ArithmeticChoices {
    public static void main(String[] args) {
        double assignedAfterDivision = 5 / 2;
        System.out.println(5 / 2);
        System.out.println(assignedAfterDivision);
        System.out.println(5 / 2.0);
        System.out.println(-5 % 2);
        System.out.println(Math.floorMod(-5, 2));
        System.out.println("sum=" + 2 + 3);
        System.out.println("sum=" + (2 + 3));
        System.out.println(Integer.MAX_VALUE + 1);

        try {
            System.out.println(Math.addExact(Integer.MAX_VALUE, 1));
        } catch (ArithmeticException exception) {
            System.out.println("overflow");
        }

        int zero = 0;
        try {
            System.out.println(7 / zero);
        } catch (ArithmeticException exception) {
            System.out.println("zero divisor");
        }
        System.out.println(1.0 / zero);
        System.out.println(Double.isNaN(0.0 / zero));
        System.out.println(Double.NaN == Double.NaN);
    }
}

Вывод:

2
2.0
2.5
-1
1
sum=23
sum=5
-2147483648
overflow
zero divisor
Infinity
true
false

Присваивание результата 5 / 2 в double не возвращает потерянную дробь: деление уже произошло как целочисленное. В 5 / 2.0 один операнд изначально double. Аналогично выбор long после переполнившегося сложения int не исправит прошлую операцию; расширять нужно до вычисления.

Обычное сложение int не сообщает о переполнении исключением. Math.addExact позволяет обнаружить его явно. Math.floorMod задаёт другое соглашение об остатке; при положительном делителе результат находится от нуля до делителя минус один. Поэтому он удобен для циклического индекса с возможным отрицательным смещением. Оба метода описаны в Math API.

Целочисленные деление и остаток при нулевом делителе выбрасывают ArithmeticException. У double другие правила: возможны Infinity и NaN. NaN нельзя найти сравнением x == Double.NaN: используйте Double.isNaN. Double API. Такие значения не отменяют необходимости проверять входные данные согласно задаче.

У + есть отдельное применение к строкам. В "sum=" + 2 + 3 сначала получается строка "sum=2", к которой добавляется "3". Скобки в "sum=" + (2 + 3) сначала вычисляют числовую сумму. Произвольный пользовательский класс не получает аналогичного сложения автоматически.

Сравнения: значения, ссылки и содержимое

Для чисел доступны <, <=, >, >=, == и !=; их результат — boolean. Равенство записывают двумя знаками, а присваивание — одним. Диапазон в Java нельзя записать математической цепочкой 0 <= value < 10: нужны две проверки, например value >= 0 && value < 10.

При сравнении двух ссылочных значений == проверяет, относятся ли они к одному объекту, либо обе равны null. Для содержимого String используют equals. Чтобы не зависеть от переиспользования строковых литералов, в учебном примере специально создаются два разных объекта:

import java.util.Arrays;
import java.util.Objects;

public class EqualityContracts {
    public static void main(String[] args) {
        String first = new String("java");
        String second = new String("java");
        String alias = first;

        System.out.println(first == second);
        System.out.println(first.equals(second));
        System.out.println(first == alias);
        System.out.println(Objects.equals(null, first));
        System.out.println(Objects.equals(null, null));
        System.out.println(Arrays.equals(new int[] {1, 2}, new int[] {1, 2}));
    }
}

Вывод: false, true, true, false, true, true. Контракт String.equals описан в String API. Когда ссылка может быть null, Objects.equals делает такую проверку явно. Но этот метод не превращает любое равенство в глубокое сравнение: определение equals зависит от класса.

Для поэлементного сравнения массивов в примере используется Arrays.equals. Числовые обёртки вроде Integer тоже требуют внимания: два ссылочных операнда == сравниваются как ссылки, а при смешивании с примитивом возможна распаковка. Не проверяйте равенство числового содержимого через предположения о кеше объектов.

Присваивание, += и префиксный или постфиксный ++

Присваивание = сохраняет значение справа в переменной слева. Формы +=, -=, *=, /= и %= объединяют вычисление и запись результата. Операторы ++ и -- меняют переменную на единицу. Префикс возвращает уже изменённое значение, а постфикс — значение до изменения; сама переменная меняется в обоих случаях.

public class AssignmentSteps {
    public static void main(String[] args) {
        int score = 10;
        score += 3;
        score *= 2;
        score -= 4;
        score /= 2;
        score %= 6;
        System.out.println(score);

        byte small = 120;
        small += 10;
        System.out.println(small);

        int count = 5;
        int oldValue = count++;
        int newValue = ++count;
        System.out.println(oldValue + " " + newValue + " " + count);
        int prefix = --count;
        int postfix = count--;
        System.out.println(prefix + " " + postfix + " " + count);
    }
}

Вывод: 5, −126, 5 7 7 и 6 6 5. Составное присваивание включает приведение к типу левой части и вычисляет левую часть один раз. Поэтому small += 10 компилируется, а обычное small = small + 10 без явного приведения не компилируется: сумма продвигается к int. Успешная компиляция += не гарантирует сохранение диапазона byte. Детали закреплены в JLS, раздел 15.26.2.

Не смешивайте несколько изменений одной переменной в длинной формуле. Особенно обманчиво count = count++: постфикс возвращает старое значение, которое присваивание записывает обратно. В обычном счётчике достаточно отдельного count++. Это изменение переменной, а не обещание атомарности для нескольких потоков.

Выражение if (enabled = true) допустимо, если enabled имеет тип boolean: оно присваивает true и делает условие истинным. Это частая опечатка, которую компилятор не обязан отклонять. Для проверки boolean обычно пишут if (enabled), а для сравнения других значений — нужный оператор == или метод equals.

Тернарный выбор, instanceof и приведение типа

Запись condition ? whenTrue : whenFalse вычисляет условие и затем только выбранную ветвь. Однако тип результата определяется с учётом обеих ветвей. Не следует читать это как две безусловно выполняемые команды. Если ветви содержат сложные действия, обычный if часто яснее.

instanceof проверяет совместимость типа объекта; для null результат false. Современная форма позволяет сразу объявить переменную шаблона. В правой части && она доступна, когда левая проверка успешна. Это показано в руководстве по pattern matching.

public class ChoiceAndTypes {
    private static int lengthOrZero(String text) {
        return text == null ? 0 : text.length();
    }

    private static String describe(Object value) {
        return value instanceof String text && !text.isBlank()
                ? "text length=" + text.length()
                : "other";
    }

    public static void main(String[] args) {
        System.out.println(lengthOrZero(null));
        System.out.println(lengthOrZero("Java"));
        var numeric = true ? 1 : 2.0;
        System.out.println(numeric);
        System.out.println(describe("Java"));
        System.out.println(describe(null));

        Object amount = 7;
        System.out.println((Integer) amount + 1);
        System.out.println((int) 3.9);
    }
}

Получим 0, 4, 1.0, text length=4, other, 8 и 3. В числовом тернарном выражении int и double приводят к результату double, хотя выбрана ветвь с единицей. У результата lengthOrZero есть свой договор: отсутствие текста здесь означает нулевую длину; это решение примера, а не универсальное правило Java.

Приведение (Integer) amount допустимо, потому что amount действительно ссылается на Integer. Оно не превращает произвольную строку в число. Несовместимое приведение ссылочного типа может дать ClassCastException; сужение чисел способно потерять дробь или разряды. По возможности проверяйте объект через instanceof и используйте подходящие методы преобразования.

Встречаются и другие конструкции выражений: new создаёт объект, точка обращается к члену, квадратные скобки выбирают элемент массива. Лямбды и ссылки на методы относятся к отдельной теме [Stream API](/lessons/java/java-for-developers/java-developer-07). Не стоит объединять их в одно правило только потому, что в записи встречаются специальные символы.

Приоритет — не порядок вызовов

Приоритет определяет группировку выражения. Например, 2 + 3 * 4 означает 2 + (3 * 4). Ассоциативность разрешает группировку операций одного уровня: 20 / 5 * 2 даёт (20 / 5) * 2, то есть 8. Но это отдельный вопрос от того, в каком порядке вызываются методы, стоящие на местах операндов.

Группа, от более сильного связывания к более слабомуОперации
Постфиксное изменениеexpr++, expr--
Унарные операции++expr, --expr, +expr, -expr, !, ~, приведение типа
Умножение и деление*, /, %
Сложение и вычитание+, -
Сдвиги<<, >>, >>>
Сравнения и проверка типа<, <=, >, >=, instanceof
Равенство==, !=
Побитовые и полные логические операцииAND, затем XOR, затем OR
Условная логика&&, затем логическое OR с коротким замыканием
Выбор значения? :
Присваивание= и составные формы

Скобки явно задают группировку. Присваивание и условный выбор группируются справа; большинство рассмотренных бинарных операций — слева. В сомнительном месте скобки полезнее спора по памяти. Правила вычисления операндов и исключений описаны в JLS, раздел 15.7.

public class EvaluationOrder {
    private static int operand(String label, int value) {
        System.out.println(label);
        return value;
    }

    public static void main(String[] args) {
        int result = operand("A", 2) + operand("B", 3) * operand("C", 4);
        System.out.println(result);
        System.out.println(20 / 5 * 2);
    }
}

Вывод: A, B, C, 14, 8. Хотя умножение связывается сильнее сложения, первый вызов — operand("A", 2). Java вычисляет операнды слева направо, сохраняя группировку; короткое замыкание и выбранная ветвь ?: могут пропускать части выражения. Из этого не следует, что все операторы нужно выполнять механически слева направо без учёта структуры.

Практика: напишите условие и найдите контрпример

  1. Сформулируйте правило акции: возраст не меньше 18, есть подписка или купон, блокировки нет. Запишите выражение, затем проверьте возраст 17 и 18, оба способа участия, отсутствие обоих и блокировку. Не ограничивайтесь единственным успешным примером.
  2. Предскажите результаты 9 / 4, 9 / 4.0, −9 % 4 и Math.floorMod(−9, 4). Объясните, почему переменная double слева от присваивания сама по себе не меняет тип деления справа.
  3. Замените два new String("java") на два одинаковых строковых литерала. Может ли == теперь показать true? Объясните, почему такой результат не делает == проверкой содержимого произвольных строк.
  4. Для int count = 3 выполните count = count++. Предскажите результат, затем перепишите увеличение без лишнего присваивания. Сравните смысл с двумя отдельными строками int old = count; count++.
  5. В GuardedConditions поменяйте порядок на !text.isBlank() && text != null. Проверьте null и объясните, почему наличие проверки в выражении ещё не делает её защитой.

Разбор: правило акции можно записать как age >= 18 && (member || coupon) && !blocked. При любом blocked = true результат должен быть false, а на границе 18 остальные условия продолжают действовать. Во втором задании получатся 2, 2.25, −1 и 3. Одинаковые строковые литералы могут ссылаться на один объект, но динамически полученные строки не обязаны иметь ту же ссылку. В четвёртом задании count остаётся 3; отдельный count++ увеличивает его до 4. В пятом опасный вызов стоит раньше проверки и выбрасывает NullPointerException при null.

Проверка понимания: коллега предлагает «исправить» неверный доступ заблокированного посетителя, заменив && на &. Почему это не решение? Оба оператора вычисляют логическое AND, но различаются вычислением правой части; проблема исходного правила была в группировке альтернатив и области действия проверки блокировки.

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

Почему в if нельзя передать число, строку или null вместо boolean?

Java требует boolean либо Boolean с распаковкой. Числа и строки не имеют неявной истинности, а null в Boolean вызывает ошибку распаковки. Запишите нужное условие явно: число больше нуля, строка не пустая или настройка равна Boolean.TRUE, если таков договор задачи.

Нужно ли помнить все приоритеты операторов наизусть?

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

Источники