gadget59.ru лекции
Продвинутые приёмы · 38 слайдов

Продвинутые приёмы в Java

Эта лекция — для тех, кто уже уверенно пишет if, for и while и хочет узнать, что появилось в Java за последние годы и как это применять на практике.

🎯 switch-выражения 🏷️ метки 🌊 Stream API 📦 Optional 📝 record 📄 текстовые блоки 🔮 var 🧩 паттерн-матчинг
Мы не просто разберём синтаксис — мы применим его к рефакторингу: возьмём старый код и перепишем его современными приёмами.

План лекции

  • 1 switch-выражения (Java 14+)
  • 2 Метки в циклах
  • 3 var — локальный вывод типов
  • 4 Текстовые блоки
  • 5 Optional — как не возвращать null
  • 6 record — короткие неизменяемые классы
  • 7 Stream API — обработка коллекций
  • 8 Паттерн-матчинг для instanceof и switch
  • 9 Рефакторинг старого кода
  • 10 Типичные ошибки продвинутого уровня
  • 11 Практика: переписать «плохой» код
  • 12 Итоги и что дальше

switch-выражения (Java 14+)

Классический switch — это оператор. Он не возвращает значение, требует break и легко забывается. С Java 14 появились switch-выражения — они возвращают значение.

// Старый switch — нужно break и переменная снаружи String result; switch (day) { case 1: result = "Пн"; break; case 2: result = "Вт"; break; default: result = "?"; } // Новый switch — возвращает значение, break не нужен String result = switch (day) { case 1 -> "Пн"; case 2 -> "Вт"; default -> "?"; };
ОсобенностьСтарый switchНовый switch breakнуженне нужен Возвращает значениенетда Стрелка ->нетда Несколько значений в caseчерез case 1: case 2:case 1, 2 -> Блок кода—yield

switch-выражения: блок и yield

Если в case нужно несколько строк — используют блок с yield.

int daysInMonth = switch (month) { case 1, 3, 5, 7, 8, 10, 12 -> 31; case 4, 6, 9, 11 -> 30; case 2 -> { boolean leap = isLeapYear(year); yield leap ? 29 : 28; } default -> throw new IllegalArgumentException( "Месяц: " + month); };
yield — это «вернуть значение из блока switch». Похоже на return, но для switch.
switch-выражения можно использовать не только для строк и чисел, но и для enum — это особенно удобно.

Метки в циклах

Когда циклы вложены, break выходит только из внутреннего. Чтобы выйти из обоих — используют метки.

outer: for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (i == 1 && j == 1) { break outer; // выйти из ОБОИХ циклов } System.out.println(i + "," + j); } }
  • outer: — метка, имя для внешнего цикла.
  • break outer; — выйти из цикла с этой меткой.
  • Метки работают и с continue.
Метки — мощный, но редкий приём. Если код требует метки, возможно, стоит вынести логику в отдельный метод или использовать return.

var — локальный вывод типов

С Java 10 можно не писать тип переменной — компилятор выведет его сам.

// Было Map<String, List<Integer>> map = new HashMap<>(); // Стало var map = new HashMap<String, List<Integer>>(); // var работает и для циклов for (var entry : map.entrySet()) { System.out.println(entry.getKey()); }
✅
Можно
локальные переменные, параметры for-each, try-with-resources
❌
Нельзя
поля класса, параметры методов, возвращаемые типы
var — не динамическая типизация. Тип всё равно фиксируется на этапе компиляции. Это просто сокращение записи.

Текстовые блоки

С Java 15 многострочные строки можно писать без склейки и экранирования.

// Было — боль и страдания String json = "{\n" + " \"name\": \"Иван\",\n" + " \"age\": 25\n" + "}"; // Стало — текстовый блок String json = """ { "name": "Иван", "age": 25 } """;
  • Начинается и заканчивается тремя кавычками """.
  • Внутри можно писать кавычки, переносы, обратные слеши без экранирования.
  • Отступы автоматически обрезаются по минимальному.
  • Идеально для SQL, JSON, HTML, XML.
Текстовые блоки — это не «строка с переносами», а полноценный литерал. Компилятор сам убирает лишние отступы.

Optional — как не возвращать null

Optional — контейнер, который может содержать значение или быть пустым. Это способ сказать: «здесь может не быть результата».

// Плохо: возвращаем null public String findName(int id) { if (id == 1) return "Иван"; return null; // caller может забыть проверить } // Хорошо: возвращаем Optional public Optional<String> findName(int id) { if (id == 1) return Optional.of("Иван"); return Optional.empty(); } // Использование Optional<String> name = findName(1); name.ifPresent(n -> System.out.println("Нашли: " + n)); String result = name.orElse("Неизвестно");
МетодЧто делает Optional.of(x)создать с значением (не null) Optional.empty()пустой Optional Optional.ofNullable(x)безопасно: null → empty isPresent()есть ли значение orElse(default)вернуть значение или default orElseThrow()вернуть или бросить исключение map(fn)преобразовать, если есть
Optional — для возвращаемых значений. Не используй его для полей и параметров — это антипаттерн.

Optional: практические примеры

// Цепочка преобразований String city = findUser(id) .map(User::getAddress) .map(Address::getCity) .orElse("Неизвестно"); // Проверка с действием findUser(id).ifPresentOrElse( user -> System.out.println("Привет, " + user.getName()), () -> System.out.println("Пользователь не найден") ); // Бросить исключение, если пусто User user = findUser(id) .orElseThrow(() -> new IllegalStateException("User not found"));
Не вызывай optional.get() без проверки — это то же, что вернуться к null. Используй orElse, orElseThrow, ifPresent.

record — короткие неизменяемые классы

record — это класс для хранения данных. Компилятор сам создаёт конструктор, геттеры, equals, hashCode и toString.

// Было — 30 строк boilerplate public final class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } @Override public boolean equals(Object o) { ... } @Override public int hashCode() { ... } @Override public String toString() { ... } } // Стало — одна строка public record Point(int x, int y) { }
  • Все поля final — объект неизменяемый.
  • Геттеры называются x() и y(), без get.
  • Можно добавлять методы и конструкторы.
  • Идеально для DTO, координат, результатов, пар ключ-значение.
record — это не «магический класс». Это сокращённая запись для «контейнера данных». Если нужна логика — используй обычный класс.

record: конструктор и валидация

public record Person(String name, int age) { // Компактный конструктор — для валидации public Person { if (name == null || name.isBlank()) { throw new IllegalArgumentException("Имя не может быть пустым"); } if (age < 0) { throw new IllegalArgumentException("Возраст не может быть отрицательным"); } } // Дополнительный метод public String greeting() { return "Привет, меня зовут " + name; } }
Что можноЧто нельзя Компактный конструкторИзменяемые поля Дополнительные методыНаследование от record Реализация интерфейсовПоля экземпляра, не указанные в заголовке

Stream API: напоминание

Stream API — функциональный способ обработки коллекций. Мы разбирали его в лекции по ООП, но здесь — углублённо.

List<String> names = List.of("Иван", "Мария", "Пётр"); List<String> result = names.stream() .filter(n -> n.length() > 3) .map(String::toUpperCase) .sorted() .toList(); // [ИВАН, МАРИЯ]
Коллекция
→
stream()
→
операции
→
результат
Stream — это «поток» элементов, по которому можно прогонять операции. Он не хранит данные, а обрабатывает их по запросу.

Stream: основные операции

ОперацияЧто делаетПример filterоставляет только подходящие.filter(n -> n > 5) mapпреобразует каждый элемент.map(String::toUpperCase) sortedсортирует.sorted() distinctубирает дубли.distinct() limitограничивает количество.limit(3) skipпропускает первые N.skip(2) collectсобирает в коллекцию.collect(Collectors.toList()) forEachдействие для каждого.forEach(System.out::println) countсчитает элементы.count() reduceсворачивает в одно значение.reduce(0, Integer::sum)
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6); // Сумма квадратов чётных чисел int sum = numbers.stream() .filter(n -> n % 2 == 0) .mapToInt(n -> n * n) .sum(); // 4 + 16 + 36 = 56

Stream: Collectors

Collectors — «сборщики», которые превращают поток в коллекцию или другой результат.

List<String> names = List.of("Иван", "Мария", "Пётр"); // В список List<String> list = names.stream().collect(Collectors.toList()); // В множество (без дублей) Set<String> set = names.stream().collect(Collectors.toSet()); // В строку через запятую String joined = names.stream() .collect(Collectors.joining(", ")); // "Иван, Мария, Пётр" // Группировка по длине имени Map<Integer, List<String>> byLength = names.stream() .collect(Collectors.groupingBy(String::length)); // {4=[Иван, Пётр], 5=[Мария]}
Collectors — мощный инструмент. С ним можно группировать, считать статистику, объединять строки — одной строкой кода.

Stream: параллельные потоки

Если данных много, можно обрабатывать их параллельно.

// Обычный поток long count = numbers.stream() .filter(n -> n > 1000) .count(); // Параллельный поток long count = numbers.parallelStream() .filter(n -> n > 1000) .count();
⚡
Быстрее
на больших данных и многоядерных процессорах
⚠️
Осторожно
не для маленьких коллекций и не для операций с побочными эффектами
parallelStream не всегда быстрее. На маленьких коллекциях он медленнее из-за накладных расходов. И он небезопасен для операций, которые меняют общее состояние.

Паттерн-матчинг для instanceof

С Java 16 можно не приводить тип вручную после instanceof.

// Было — приведение вручную if (obj instanceof String) { String s = (String) obj; System.out.println(s.length()); } // Стало — паттерн-матчинг if (obj instanceof String s) { System.out.println(s.length()); // s уже String }
Это не «синтаксический сахар». Компилятор гарантирует, что s существует только внутри блока if. Это безопаснее ручного приведения.
Паттерн-матчинг работает и с отрицанием: if (!(obj instanceof String s)) return; — после этого s доступен дальше по коду.

Паттерн-матчинг в switch

С Java 21 switch умеет проверять типы и сразу приводить к ним.

sealed interface Shape permits Circle, Square {} record Circle(double radius) implements Shape {} record Square(double side) implements Shape {} double area = switch (shape) { case Circle c -> Math.PI * c.radius() * c.radius(); case Square s -> s.side() * s.side(); };
  • sealed гарантирует, что наследники известны — компилятор проверит, что все случаи покрыты.
  • default не нужен, если все варианты перечислены.
  • Можно комбинировать с проверкой условий: case Circle c when c.radius() > 10 -> ...
Паттерн-матчинг + sealed — это замена классическому instanceof + if-else. Код становится короче и безопаснее.

Рефакторинг: старый код

Возьмём типичный «старый» код и перепишем его современными приёмами.

// Задача: посчитать количество чётных чисел в списке List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); // Было int count = 0; for (Integer n : numbers) { if (n % 2 == 0) { count++; } } // Стало long count = numbers.stream() .filter(n -> n % 2 == 0) .count();
Stream не всегда лучше цикла. Если в цикле сложная логика, много условий и побочных эффектов — цикл может быть понятнее.

Рефакторинг: switch → switch-выражение

// Было String category; switch (age) { case 0: case 1: case 2: category = "Малыш"; break; case 3: case 4: case 5: category = "Дошкольник"; break; default: category = "Школьник"; } // Стало String category = switch (age) { case 0, 1, 2 -> "Малыш"; case 3, 4, 5 -> "Дошкольник"; default -> "Школьник"; };
БылоСтало Много caseГруппировка через запятую break в каждомне нужен Переменная снаружиВозврат значения 9 строк5 строк

Рефакторинг: Optional вместо null

// Было — легко забыть проверить public String getCity(User user) { if (user != null) { Address addr = user.getAddress(); if (addr != null) { return addr.getCity(); } } return "Неизвестно"; } // Стало public String getCity(User user) { return Optional.ofNullable(user) .map(User::getAddress) .map(Address::getCity) .orElse("Неизвестно"); }
Optional + map — это «безопасная цепочка». Если на любом шаге null — цепочка прерывается и возвращается orElse.
Optional не заменяет проверки везде. Он для возвращаемых значений. Не передавай Optional в параметры — это антипаттерн.

Рефакторинг: класс → record

// Было — 30 строк public class Book { private final String title; private final String author; private final int year; public Book(String title, String author, int year) { this.title = title; this.author = author; this.year = year; } public String getTitle() { return title; } public String getAuthor() { return author; } public int getYear() { return year; } @Override public boolean equals(Object o) { ... } @Override public int hashCode() { ... } @Override public String toString() { ... } } // Стало — 1 строка public record Book(String title, String author, int year) { }
БылоСтало 30+ строк1 строка getTitle()title() equals/hashCode/toStringавтоматически Можно изменить полеНельзя (final)

Рефакторинг: var

// Было Map<String, List<Integer>> scores = new HashMap<>(); BufferedReader reader = new BufferedReader(new FileReader("file.txt")); // Стало var scores = new HashMap<String, List<Integer>>(); var reader = new BufferedReader(new FileReader("file.txt"));
var — не «убрать тип», а «не повторять тип дважды». Справа тип всё равно указан — компилятор выведет его сам.
Не злоупотребляй. Если из правой части непонятно, что за тип — лучше написать явно. Читаемость важнее краткости.

Типичные ошибки продвинутого уровня

1. Злоупотребление Stream.
Если логика сложная — цикл может быть понятнее. Stream не всегда лучше.
2. Optional в полях и параметрах.
Optional — для возвращаемых значений. Для полей и параметров используй null или отдельный класс.
3. parallelStream везде.
На маленьких коллекциях он медленнее. И небезопасен для операций с общим состоянием.
4. record для всего.
record — для данных. Если нужна логика, изменяемость или наследование — обычный класс.
5. var бездумно.
Если из правой части непонятно, что за тип — пиши явно.
6. Паттерн-матчинг без sealed.
Без sealed компилятор не проверит полноту switch. Используй default.

Проверь себя

  • 1 Чем switch-выражение отличается от обычного switch?
  • 2 Зачем нужен yield?
  • 3 Когда используют метки в циклах?
  • 4 Что такое var и где его можно использовать?
  • 5 Чем текстовый блок отличается от обычной строки?
  • 6 Зачем нужен Optional?
  • 7 Чем record отличается от обычного класса?
  • 8 Что делают filter и map в Stream?
  • 9 Когда parallelStream может навредить?
  • 10 Чем паттерн-матчинг лучше ручного instanceof?
Если что-то не помнишь — вернись к слайду. Это нормально.

Практика: переписать код

Возьмите код и перепишите его с использованием современных приёмов.

// Код для рефакторинга public class Order { private String customer; private List<Item> items; public String getCustomer() { return customer; } public void setCustomer(String c) { this.customer = c; } public List<Item> getItems() { return items; } public void setItems(List<Item> i) { this.items = i; } public double total() { double sum = 0; for (Item item : items) { if (item.isActive()) { sum += item.getPrice() * item.getQuantity(); } } return sum; } }

Что сделать:

  1. Заменить класс на record.
  2. Переписать total() через Stream.
  3. Добавить валидацию в конструктор.
  4. Использовать var там, где уместно.
Решение можно обсудить с соседом. Цель — не «правильный ответ», а умение применять приёмы осознанно.

Практика: вариант решения

public record Order(String customer, List<Item> items) { public Order { if (customer == null || customer.isBlank()) { throw new IllegalArgumentException("customer is blank"); } items = List.copyOf(items); // защита от изменений } public double total() { return items.stream() .filter(Item::isActive) .mapToDouble(i -> i.price() * i.quantity()) .sum(); } }
БылоСтало Класс с геттерами и сеттерамиrecord Цикл с условиемStream + filter + mapToDouble Нет валидацииКомпактный конструктор Изменяемый списокList.copyOf

Что дальше

Мы разобрали продвинутые приёмы. Куда двигаться дальше?

🧵
Многопоточность
Thread, ExecutorService, CompletableFuture
📁
Файлы и сериализация
Files API, ObjectOutputStream
🗄️
JDBC
работа с базами данных
🌐
HTTP-клиент
HttpClient, работа с API
🧪
Тестирование
JUnit, Mockito
🏗️
Фреймворки
Spring, Quarkus
Продвинутые приёмы — это не «всё сразу». Это инструменты, которые применяются по мере необходимости.

Итоги

  • ✅ switch-выражения — возвращают значение, без break.
  • ✅ yield — вернуть значение из блока switch.
  • ✅ Метки — выход из вложенных циклов.
  • ✅ var — локальный вывод типов.
  • ✅ Текстовые блоки — многострочные строки без экранирования.
  • ✅ Optional — контейнер вместо null.
  • ✅ record — короткие неизменяемые классы.
  • ✅ Stream API — обработка коллекций.
  • ✅ Collectors — сборка результатов.
  • ✅ parallelStream — параллельная обработка (осторожно).
  • ✅ Паттерн-матчинг — instanceof и switch без приведения.
  • ✅ Рефакторинг — старый код → современный.
Эти приёмы делают код короче, безопаснее и понятнее. Но не забывай: главное — читаемость, а не «использовать всё новое».

Домашнее задание

  1. Перепишите класс Student (имя, возраст, оценки) в record с валидацией.
  2. Перепишите цикл поиска максимального элемента через Stream.
  3. Перепишите switch на switch-выражение.
  4. Создайте метод, который возвращает Optional<String> — например, поиск пользователя по ID.
  5. Напишите метод, который принимает список строк и возвращает Map<Integer, List<String>> — группировка по длине.
  6. Перепишите класс Order из практики полностью.
Первые три — обязательные. Остальные — для тренировки. Если что-то не получается — пиши в комментариях к коду, что именно не понял.

Спасибо за внимание!

🚀

Продвинутые приёмы — это инструменты

Используй их осознанно. Главное — читаемость и надёжность кода, а не «всё новое».

🎯 switch 🌊 Stream 📦 Optional 📝 record 🚀 практика