Java долго ругали за многословность: чтобы описать простой класс-«коробку» с тремя полями, приходилось писать конструктор, геттеры, equals, hashCode и toString руками. За последние версии (17–21) язык заметно подтянулся. Разберём возможности, которые делают современный код короче и безопаснее, и начнём с самой полезной — record.
Зачем это всё: проблема многословности
Представьте класс, который просто хранит данные — точку на карте, строку заказа, координаты. Раньше это выглядело так:
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 x() { return x; }
public int y() { return y; }
@Override
public boolean equals(Object o) { /* ... 10 строк ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}
Тридцать строк ради двух чисел. Логики тут ноль — одна рутина, которую легко написать с ошибкой (забыть поле в equals). Именно эту боль и убирает record.
record подробнее
record — это специальный вид класса для хранения неизменяемых данных. Вы объявляете только поля, а компилятор сам генерирует конструктор, методы доступа, equals, hashCode и toString.
public record Point(int x, int y) {}
Одна строка делает всё то же, что 30 строк выше. Пользоваться так:
Point p = new Point(3, 4);
System.out.println(p.x()); // 3 — метод доступа называется как поле, без get
System.out.println(p); // Point[x=3, y=4] — готовый toString
Point q = new Point(3, 4);
System.out.println(p.equals(q)); // true — сравнение по значениям полей
Ключевые свойства record:
- Неизменяемость. Поля —
final, после создания их не поменять. Чтобы «изменить» точку, создают новую. - Методы доступа называются как поля:
p.x(), а неp.getX(). equals/hashCodeсравнивают по значению всех полей. Это делает record идеальным ключом дляMapили элементомSet.
Можно добавить свои методы и проверки в конструкторе. Компактный конструктор позволяет проверить аргументы без перечисления присваиваний:
public record Point(int x, int y) {
public Point { // компактный конструктор — без скобок с параметрами
if (x < 0 || y < 0) {
throw new IllegalArgumentException("Координаты не могут быть отрицательными");
}
// присваивание this.x = x; компилятор добавит сам
}
public double distanceToOrigin() { // свой метод — пожалуйста
return Math.sqrt(x * x + y * y);
}
}
Короткая формула: record — это «класс-данные» без рутины. Используйте его для DTO, ключей, координат, любых неизменяемых «коробок со значениями». Когда нужны изменяемое состояние или наследование — берите обычный класс.
Optional: как перестать бояться NPE
NullPointerException (NPE) — самая частая ошибка времени выполнения в Java. Она случается, когда вы вызываете метод у переменной, которая на самом деле null. Метод, возвращающий «может быть значение, а может ничего», раньше возвращал null — и про проверку легко было забыть.
Optional<T> — это «коробка», которая либо содержит значение, либо пуста. Она явно говорит вызывающему: «здесь может не быть результата, обработай оба случая».
Optional<User> found = repository.findById(42); // явно: может не найтись
// безопасно достать значение или подставить запасное
User user = found.orElse(User.guest());
// или среагировать только если значение есть
found.ifPresent(u -> System.out.println("Привет, " + u.name()));
Сила Optional — в цепочках преобразований без россыпи if (x != null):
String city = repository.findById(42) // Optional<User>
.map(User::address) // Optional<Address>
.map(Address::city) // Optional<String>
.orElse("неизвестен"); // String, без единого NPE
Если на любом шаге значения нет — цепочка спокойно «проваливается» в пустой Optional и отдаёт "неизвестен".
Антипаттерны Optional
Optional легко применить не туда. Чего не делать:
- Не вызывайте
.get()без проверки.opt.get()на пустом Optional бросит исключение — это тот же NPE, только сбоку. ИспользуйтеorElse,orElseThrow,ifPresent,map. - Не делайте поля класса
Optional. Optional придуман для возвращаемых значений методов, а не для хранения. Поле — пусть будет обычным, возможноnullвнутри. - Не передавайте
Optionalв параметры метода. Это запутывает вызов. Лучше сделайте перегрузку метода или принимайте обычное значение. - Не оборачивайте коллекции. Пустой
Listуже означает «ничего нет» —Optional<List<...>>лишний. Возвращайте пустой список.
Короткая формула: Optional — это тип возвращаемого значения «может не быть результата», а не замена null повсюду.
switch expressions
Старый switch был оператором: он выполнял действия, требовал break (забыли — провалитесь в следующую ветку) и не возвращал значение. Современный switch умеет быть выражением — то есть возвращать результат, который сразу присваивают переменной.
// стрелочный синтаксис: без break, без сквозного проваливания
String day = switch (dayOfWeek) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "будни";
case SATURDAY, SUNDAY -> "выходные";
};
Преимущества: нет break, несколько меток через запятую, всё выражение возвращает значение. Для enum компилятор ещё и проверит, что разобраны все варианты. Если ветка требует нескольких строк, используйте блок с yield:
int code = switch (status) {
case ACTIVE -> 1;
case BLOCKED -> {
log.warn("Заблокированный статус");
yield 0; // yield возвращает значение из блока
}
};
sealed-классы и pattern matching кратко
sealed-класс (или интерфейс) ограничивает список наследников: только перечисленные типы могут его реализовать. Это говорит и компилятору, и читателю: «вариантов ровно столько, других не будет».
public sealed interface Shape permits Circle, Square {}
public record Circle(double radius) implements Shape {}
public record Square(double side) implements Shape {}
Вместе с pattern matching в switch это даёт компактный и безопасный разбор по типам. Раньше писали if (s instanceof Circle) { Circle c = (Circle) s; ... } — с явным приведением. Теперь переменная объявляется прямо в проверке:
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius(); // c уже Circle, без приведения
case Square s -> s.side() * s.side();
};
Поскольку Shape — sealed, компилятор знает все варианты и не потребует ветку default. Добавите третью фигуру — он сам подскажет, где switch стал неполным. Pattern matching работает и в обычном instanceof:
if (obj instanceof String str && !str.isBlank()) {
System.out.println(str.length()); // str доступна и уже String
}
text blocks
Многострочные строки (JSON, SQL, HTML) раньше склеивали из кусков с \n и кавычками-ёлочками — читать невозможно. Text block — строка в тройных кавычках, которая сохраняет переносы и кавычки как есть:
String json = """
{
"name": "Иван",
"city": "Москва"
}
""";
Внутри не нужно экранировать кавычки, а отступ слева компилятор обрежет по самой левой непустой строке. Удобно для встроенных SQL-запросов и тестовых данных.
Что ещё подвезли в Java 17–21
Несколько мелочей, которые встретятся сразу:
var— вывод типа локальной переменной:var users = new ArrayList<User>();. Тип всё равно статический, просто его не пишут дважды. Только для локальных переменных, где тип и так очевиден из правой части.List.of,Map.of— быстрые неизменяемые коллекции:List.of("a", "b").- Helpful NullPointerExceptions — сообщение об NPE теперь точно называет, какая именно переменная оказалась
null. - Виртуальные потоки (Java 21) — лёгкие потоки для масштабируемого ввода-вывода; подробности — тема раздела про многопоточность, здесь просто знайте, что они есть.
Коротко
recordгенерирует конструктор, методы доступа,equals/hashCode/toString— используйте для неизменяемых классов-данных (DTO, ключи, value object).Optional<T>— честный тип возврата «значение может отсутствовать»; доставайте черезorElse/map/ifPresent, не через.get().- Антипаттерны Optional:
.get()без проверки, Optional в полях и параметрах, обёртка над коллекциями. - switch expression возвращает значение, не требует
break, проверяет полноту дляenum. sealed+ pattern matching дают безопасный разбор по фиксированному набору типов без ручного приведения.- Text block (
""") — читаемые многострочные строки без экранирования. var,List.of, понятные NPE и виртуальные потоки — приятные мелочи современного Java.
Что почитать дальше
- Лямбды и Stream API — где
Optional.mapиswitchвстречаются чаще всего. - ООП в Java — чем
recordиsealedотличаются от обычных классов и наследования. - Инструменты и сборка — как собрать и запустить проект на актуальной версии языка.