← назад к разделу

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();
};

Поскольку Shapesealed, компилятор знает все варианты и не потребует ветку 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 отличаются от обычных классов и наследования.
  • Инструменты и сборка — как собрать и запустить проект на актуальной версии языка.