Перейти к основному содержимому

5.03. Операторы и циклы в Java

Разработчику Архитектору

Операторы и циклы в Java

Виды операторов

Операторы можно разделить на несколько видов:

ТипПримеры
Арифметические+ — сложение, - — вычитание, * — умножение, / — деление, % — остаток от деления
Сравнения== — равно, != — не равно, > — больше, < — меньше, >= — больше или равно, <= — меньше или равно
Логические! — отрицание (NOT), & — конъюнкция (AND), &#124; — дизъюнкция (OR), ^ — исключающее ИЛИ (XOR), && — сокращённая конъюнкция, &#124;&#124; — сокращённая дизъюнкция, ~ — побитовое дополнение (NOT)
Присваивания=, +=, -=, *=, /=
Тернарныйусловие ? значение_если_истина : значение_если_ложь
Условные операторыif-else, switch
Прочие++ — инкремент (увеличение на 1), -- — декремент (уменьшение на 1), // — однострочный комментарий, /* */ — многострочный комментарий

Сравнение

Для примитивных типов (например, int, double) оператор == сравнивает значения.

Для объектов (например, строк или пользовательских классов) оператор == сравнивает ссылки на объекты, то есть их физическое местоположение в памяти.

Чтобы сравнить содержимое объектов, используется метод .equals().

В Java есть методы, которые выполняют функции, аналогичные операторам:

  • .equals() — аналог оператора == для сравнения содержимого объектов.
  • .compareTo() — используется для сравнения строк или объектов, реализующих интерфейс Comparable;
  • Math.addExact() , Math.subtractExact() , Math.multiplyExact() — аналоги операторов +, -, *, но с проверкой на переполнение. Если результат выходит за пределы допустимых значений, выбрасывается исключение ArithmeticException;
  • Integer.bitCount() — подсчитывает количество единиц в двоичном представлении числа. Аналогично работе с битовыми операциями (&, |, ^);
  • Objects.equals() — безопасный способ сравнения двух объектов, который обрабатывает null. Если оба объекта равны null, метод вернёт true;
  • Integer.parseInt() , Double.parseDouble() — аналоги операторов приведения типов (например, (int)).

Метод equals() проверяет логическое равенство содержимого объектов. Для строк сравнение учитывает последовательность символов.

String text1 = new String("привет");
String text2 = new String("привет");
String text3 = "привет";

System.out.println(text1 == text2); // false — разные объекты в памяти
System.out.println(text1.equals(text2)); // true — одинаковое содержимое
System.out.println(text1.equals(text3)); // true — содержимое совпадает

Integer num1 = 100;
Integer num2 = 100;
System.out.println(num1 == num2); // true — кэширование малых целых чисел
System.out.println(num1.equals(num2)); // true

Integer big1 = 200;
Integer big2 = 200;
System.out.println(big1 == big2); // false — разные объекты
System.out.println(big1.equals(big2)); // true — содержимое одинаковое

При создании собственных классов метод equals() следует переопределять вместе с hashCode():

class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && name.equals(person.name);
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}
}

Person p1 = new Person("Анна", 30);
Person p2 = new Person("Анна", 30);
System.out.println(p1.equals(p2)); // true — содержимое совпадает

Метод compareTo() возвращает целое число: отрицательное, если текущий объект меньше аргумента, ноль при равенстве, положительное при превосходстве.

String a = "яблоко";
String b = "апельсин";
System.out.println(a.compareTo(b)); // положительное число — "яблоко" позже в алфавите

String c = "банан";
System.out.println(b.compareTo(c)); // отрицательное число — "апельсин" раньше "банана"

Integer x = 42;
Integer y = 17;
System.out.println(x.compareTo(y)); // положительное число — 42 больше 17

// Сортировка списка с использованием compareTo
List<String> fruits = Arrays.asList("груша", "яблоко", "апельсин", "банан");
fruits.sort(String::compareTo);
System.out.println(fruits); // [апельсин, банан, груша, яблоко]

Методы Math.addExact(), Math.subtractExact(), Math.multiplyExact() контролируют переполнение целочисленных типов.

int max = Integer.MAX_VALUE;  // 2147483647
System.out.println(max + 1); // -2147483648 — тихое переполнение

try {
int result = Math.addExact(max, 1);
System.out.println(result);
} catch (ArithmeticException e) {
System.out.println("Переполнение при сложении: " + e.getMessage());
}

long big = Long.MAX_VALUE;
try {
long product = Math.multiplyExact(big, 2);
} catch (ArithmeticException e) {
System.out.println("Переполнение при умножении");
}

// Безопасное вычитание
int safeSubtract = Math.subtractExact(100, 50);
System.out.println(safeSubtract); // 50

Метод Integer.bitCount() возвращает количество установленных битов (единиц) в двоичном представлении числа.

int number = 42;  // двоичное: 101010
System.out.println(Integer.toBinaryString(number)); // 101010
System.out.println(Integer.bitCount(number)); // 3 — три единицы

int mask = 0b11110000;
System.out.println(Integer.bitCount(mask)); // 4

// Практическое применение: проверка чётности количества единиц
int value = 15; // 1111
boolean evenOnes = (Integer.bitCount(value) % 2 == 0);
System.out.println("Чётное количество единиц: " + evenOnes); // true

// Сравнение с ручным подсчётом через битовые операции
int manualCount = 0;
int temp = number;
while (temp != 0) {
manualCount += temp & 1;
temp >>>= 1;
}
System.out.println("Ручной подсчёт: " + manualCount); // 3

Метод Objects.equals() корректно обрабатывает значения null, избегая NullPointerException.

String s1 = null;
String s2 = null;
String s3 = "текст";

System.out.println(Objects.equals(s1, s2)); // true — оба null
System.out.println(Objects.equals(s1, s3)); // false — null и непустая строка
System.out.println(Objects.equals(s3, s3)); // true — одинаковые строки

// Сравнение без Objects.equals() требует дополнительных проверок
if (s1 != null && s1.equals(s2)) {
// безопасное сравнение без использования Objects.equals()
}

Методы Integer.parseInt() и Double.parseDouble() преобразуют текстовые представления в числовые значения.

String intText = "12345";
int intValue = Integer.parseInt(intText);
System.out.println(intValue); // 12345

String doubleText = "3.14159";
double doubleValue = Double.parseDouble(doubleText);
System.out.println(doubleValue); // 3.14159

// Обработка ошибок преобразования
try {
int invalid = Integer.parseInt("не число");
} catch (NumberFormatException e) {
System.out.println("Невозможно преобразовать строку в число");
}

// Преобразование с указанием системы счисления
int hexValue = Integer.parseInt("FF", 16); // 255 в шестнадцатеричной
int binaryValue = Integer.parseInt("1010", 2); // 10 в двоичной
System.out.println(hexValue); // 255
System.out.println(binaryValue); // 10

Класс Object

Класс Object — базовый для всех классов в Java. Он определяет поведение, доступное каждому объекту.

Основные методы:

  • equals(Object obj) — сравнение объектов на логическое равенство
  • hashCode() — возвращает хеш-код объекта
  • toString() — строковое представление объекта
  • getClass() — возвращает объект Class, представляющий тип в рантайме
  • clone() — создает копию объекта (если поддерживает Cloneable)
  • finalize() — вызывается перед сборкой мусора (устарел и не рекомендуется)
  • wait(), notify(), notifyAll() — методы для межпоточной синхронизации.

Метод equals() определяет логическое равенство объектов. Метод hashCode() генерирует целочисленный хеш-код, используемый в хеш-коллекциях.

class Book {
private String title;
private String author;

public Book(String title, String author) {
this.title = title;
this.author = author;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return title.equals(book.title) && author.equals(book.author);
}

@Override
public int hashCode() {
return Objects.hash(title, author);
}
}

Book b1 = new Book("1984", "Оруэлл");
Book b2 = new Book("1984", "Оруэлл");
Book b3 = new Book("Мастер и Маргарита", "Булгаков");

System.out.println(b1.equals(b2)); // true
System.out.println(b1.equals(b3)); // false
System.out.println(b1.hashCode() == b2.hashCode()); // true — равные объекты имеют одинаковый хеш

Метод toString() предоставляет текстовое представление объекта. Переопределение улучшает отладку и логирование.

class Point {
private int x;
private int y;

public Point(int x, int y) {
this.x = x;
this.y = y;
}

@Override
public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}

Point p = new Point(10, 20);
System.out.println(p); // Point[x=10, y=20]
System.out.println(p.toString()); // Point[x=10, y=20]

Метод getClass() возвращает объект класса Class, описывающий тип объекта во время выполнения.

String text = "пример";
Class<?> clazz = text.getClass();
System.out.println(clazz.getName()); // java.lang.String
System.out.println(clazz.getSimpleName()); // String

Integer number = 42;
System.out.println(number.getClass().getName()); // java.lang.Integer

// Проверка типа во время выполнения
Object obj = "текст";
if (obj.getClass() == String.class) {
System.out.println("Это строка");
}

Метод clone() создаёт поверхностную копию объекта. Класс должен реализовывать интерфейс Cloneable.

class Data implements Cloneable {
private int value;
private String[] items;

public Data(int value, String[] items) {
this.value = value;
this.items = items;
}

@Override
protected Object clone() throws CloneNotSupportedException {
Data copy = (Data) super.clone();
copy.items = this.items.clone(); // глубокое копирование массива
return copy;
}

public void setValue(int value) { this.value = value; }
public void setItems(String[] items) { this.items = items; }
public int getValue() { return value; }
public String[] getItems() { return items; }
}

Data original = new Data(100, new String[]{"a", "b", "c"});
try {
Data copy = (Data) original.clone();
copy.setValue(200);
copy.getItems()[0] = "x";

System.out.println(original.getValue()); // 100 — значение не изменилось
System.out.println(original.getItems()[0]); // a — первый элемент массива не изменился благодаря глубокому копированию
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}

Методы синхронизации: wait(), notify(), notifyAll() координируют работу потоков при доступе к общим ресурсам.

class SharedBuffer {
private String content;
private boolean empty = true;

public synchronized String read() {
while (empty) {
try {
wait(); // ожидание, пока буфер не станет непустым
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
empty = true;
notifyAll(); // уведомление ожидающих потоков записи
return content;
}

public synchronized void write(String content) {
while (!empty) {
try {
wait(); // ожидание, пока буфер не станет пустым
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
this.content = content;
empty = false;
notifyAll(); // уведомление ожидающих потоков чтения
}
}

Метод finalize() объявлен устаревшим в Java 9 и удалён в последующих версиях. Для освобождения ресурсов следует использовать интерфейс AutoCloseable и конструкцию try-with-resources.


Условные операторы

Условия (условные операторы) управляют потоком выполнения программы, позволяют выполнять разный код, в зависимости от выполнения определённого условия.

if-else

Условный оператор if-else (если/иначе) выбирает путь выполнения программы на основе логического выражения.

int temperature = 22;

if (temperature > 30) {
System.out.println("Жарко");
} else if (temperature > 20) {
System.out.println("Тепло"); // выполняется эта ветка
} else if (temperature > 10) {
System.out.println("Прохладно");
} else {
System.out.println("Холодно");
}

Синтаксис:

if (условие) {
// выполняется, если условие истинно
} else {
// выполняется, если условие ложно
}

Пример:

int age = 18;
if (age >= 18) {
System.out.println("Вы совершеннолетний");
} else {
System.out.println("Вы несовершеннолетний");
}

Особенности синтаксиса:

  • Фигурные скобки можно опустить для одиночного оператора, но такой стиль не рекомендуется
  • Условие всегда приводится к булевому типу
  • Ветки else if проверяются последовательно сверху вниз
  • Выполняется только первая удовлетворяющая условию ветка

Примеры с логическими операторами:

int age = 25;
boolean hasLicense = true;

if (age >= 18 && hasLicense) {
System.out.println("Можно водить автомобиль");
}

String role = "admin";
if (role.equals("admin") || role.equals("moderator")) {
System.out.println("Доступ к административным функциям");
}

boolean isWeekend = true;
if (!isWeekend) {
System.out.println("Рабочий день");
} else {
System.out.println("Выходной"); // выполняется эта ветка
}

Вложенные условия:

int hour = 14;
boolean isHoliday = false;

if (!isHoliday) {
if (hour >= 9 && hour < 18) {
System.out.println("Рабочее время");
} else {
System.out.println("Нерабочее время");
}
} else {
System.out.println("Праздничный день");
}

switch

Оператор switch выбирает одну из нескольких веток выполнения на основе значения выражения.

Синтаксис:

switch (выражение) {
case значение1:
// код
break;
case значение2:
// код
break;
default:
// код по умолчанию
}

Пример:

int day = 3;
switch (day) {
case 1 -> System.out.println("Понедельник");
case 2 -> System.out.println("Вторник");
case 3 -> System.out.println("Среда");
default -> System.out.println("Неизвестный день");
}

Традиционный синтаксис с оператором break:

int month = 3;

switch (month) {
case 1:
System.out.println("Январь");
break;
case 2:
System.out.println("Февраль");
break;
case 3:
System.out.println("Март"); // выполняется эта ветка
break;
default:
System.out.println("Неизвестный месяц");
}

Современный синтаксис с оператором -> (доступен с Java 14):

String day = "вторник";

switch (day) {
case "понедельник" -> System.out.println("Начало недели");
case "вторник", "среда", "четверг" -> System.out.println("Середина недели"); // выполняется эта ветка
case "пятница" -> System.out.println("Конец рабочей недели");
case "суббота", "воскресенье" -> System.out.println("Выходные");
default -> System.out.println("Неизвестный день");
}

Оператор switch как выражение (возвращает значение):

int statusCode = 404;
String statusMessage = switch (statusCode) {
case 200 -> "OK";
case 404 -> "Not Found"; // возвращается это значение
case 500 -> "Internal Server Error";
default -> "Unknown Status";
};
System.out.println(statusMessage); // Not Found

Правила использования:

  • Выражение в switch может иметь тип byte, short, char, int, enum, String или их обёртки
  • Каждая метка case должна содержать константное выражение
  • Оператор break предотвращает переход к следующей ветке (проваливание)
  • Ветка default выполняется, если ни одна метка case не совпала
  • В синтаксисе с -> оператор break не требуется — выполняется только одна ветка

Циклы

Циклы позволяют выполнять блок кода несколько раз.


for

for (для) используется, когда известно количество повторений.

Синтаксис:

for (инициализация; условие; обновление) {
// тело цикла
}

Пример:

for (int i = 0; i < 5; i++) {
System.out.println("i = " + i);
}

Структура содержит три компонента: инициализацию, условие и обновление.

for (int i = 0; i < 5; i++) {
System.out.println("Итерация " + i);
}
// Вывод: Итерация 0, Итерация 1, Итерация 2, Итерация 3, Итерация 4

Инициализация выполняется один раз перед началом цикла. Обычно здесь объявляется и присваивается начальное значение счётчику.

for (int counter = 10; counter > 0; counter--) {
System.out.println(counter);
}
// Обратный отсчёт от 10 до 1

Можно объявить несколько переменных одного типа:

for (int i = 0, j = 10; i < 5; i++, j--) {
System.out.println("i=" + i + ", j=" + j);
}

Условие проверяется перед каждой итерацией. Цикл продолжается, пока условие истинно.

int limit = 3;
for (int i = 0; i < limit; i++) {
System.out.println(i);
}
// Вывод: 0, 1, 2

Бесконечный цикл создаётся опущением условия или указанием константы true:

for (;;) {
// бесконечный цикл
break; // выход по условию внутри тела
}

Обновление выполняется после каждой итерации. Обычно здесь изменяется значение счётчика.

for (int i = 1; i <= 100; i *= 2) {
System.out.println(i);
}
// Вывод: 1, 2, 4, 8, 16, 32, 64

Инкремент i++ увеличивает значение на единицу. Декремент i-- уменьшает на единицу. Можно использовать составные операторы: i += 5, i *= 2.

Каждое выполнение тела цикла называется итерацией. Шаг — изменение счётчика за одну итерацию.

// Шаг равен 3
for (int i = 0; i < 15; i += 3) {
System.out.println(i);
}
// Вывод: 0, 3, 6, 9, 12

// Отрицательный шаг
for (int i = 10; i >= 0; i -= 2) {
System.out.println(i);
}
// Вывод: 10, 8, 6, 4, 2, 0

Цикл for подходит для перебора массивов и коллекций:

int[] numbers = {5, 10, 15, 20};
for (int i = 0; i < numbers.length; i++) {
System.out.println("Элемент " + i + ": " + numbers[i]);
}

String[] words = {"яблоко", "банан", "апельсин"};
for (int i = 0; i < words.length; i++) {
System.out.println((i + 1) + ". " + words[i]);
}

Улучшенный цикл for (for-each) упрощает перебор элементов:

int[] values = {1, 2, 3, 4, 5};
for (int value : values) {
System.out.println(value * value);
}
// Вывод: 1, 4, 9, 16, 25

while

Цикл while повторяет выполнение блока кода, пока логическое условие остаётся истинным. Проверка условия происходит перед каждой итерацией.

Синтаксис:

while (условие) {
// тело цикла
}

Пример:

int i = 0;
while (i < 5) {
System.out.println("i = " + i);
i++;
}
int count = 3;
while (count > 0) {
System.out.println("Осталось: " + count);
count--;
}
// Вывод: Осталось: 3, Осталось: 2, Осталось: 1

Особенности:

  • Если условие изначально ложно, тело цикла не выполнится ни разу
  • Счётчик или переменная условия должны изменяться внутри тела цикла
  • Бесконечный цикл возникает при отсутствии изменения условия

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

// Чтение данных до достижения конца
boolean hasNext = true;
int dataIndex = 0;
while (hasNext) {
// обработка данных[dataIndex]
dataIndex++;
if (dataIndex >= 10) {
hasNext = false; // завершение цикла
}
}

// Поиск элемента в массиве
int[] items = {4, 8, 15, 16, 23, 42};
int target = 16;
int position = 0;
boolean found = false;

while (position < items.length && !found) {
if (items[position] == target) {
found = true;
} else {
position++;
}
}

if (found) {
System.out.println("Элемент найден на позиции " + position);
}

Цикл while удобен, когда количество итераций заранее неизвестно и определяется в процессе выполнения.


do-while

do-while (делать, пока), выполняется хотя бы один раз, даже если условие ложно. Проверка условия происходит после каждой итерации.

Синтаксис:

do {
// тело цикла
} while (условие);

Пример:

int j = 0;
do {
System.out.println("j = " + j);
j++;
} while (j < 5);
int attempts = 0;
do {
System.out.println("Попытка " + (attempts + 1));
attempts++;
} while (attempts < 3);
// Вывод: Попытка 1, Попытка 2, Попытка 3

Сравнение с while:

int value = 10;

// Цикл while не выполнится
while (value < 5) {
System.out.println("while: " + value);
value++;
}

// Цикл do-while выполнится один раз
do {
System.out.println("do-while: " + value);
value++;
} while (value < 5);
// Вывод: do-while: 10

Практическое применение — интерактивные меню:

Scanner scanner = new Scanner(System.in);
int choice;
do {
System.out.println("Меню:");
System.out.println("1. Показать баланс");
System.out.println("2. Пополнить счёт");
System.out.println("3. Выйти");
System.out.print("Выберите действие: ");
choice = scanner.nextInt();

switch (choice) {
case 1 -> System.out.println("Баланс: 1000 руб.");
case 2 -> System.out.println("Счёт пополнен");
case 3 -> System.out.println("Выход");
default -> System.out.println("Неверный выбор");
}
} while (choice != 3);
scanner.close();

Цикл do-while применяется, когда требуется как минимум одна итерация независимо от начального состояния условия.