W3docs

Java Records: углублённое изучение

Углублённое изучение Java records — канонические и компактные конструкторы, валидация и практические случаи применения.

Record — это способ объявить в Java класс, единственная задача которого — хранить данные. Впервые появившись в режиме предварительного просмотра в Java 14 и окончательно стабилизировавшись в Java 16, record сворачивает стандартный шаблонный код — приватные финальные поля, конструктор, методы доступа, equals, hashCode и toString — в одну строку заголовка. В предыдущей главе о records рассмотрен базовый синтаксис; здесь мы погружаемся глубже: как records работают на самом деле, каковы их канонические и компактные конструкторы, как они обеспечивают соблюдение инвариантов, какие гарантии неизменяемости вы получаете и где их применять (а где — нет).

Что генерирует компилятор

Когда вы пишете record Point(int x, int y) {}, компилятор создаёт final-класс с двумя полями private final, публичный конструктор, принимающий оба параметра, публичные методы доступа с именами, точно совпадающими с именами компонентов (x(), y() — без префикса get), а также основанные на значениях реализации equals, hashCode и toString.

record Point(int x, int y) {}

// Equivalent to (roughly) hand-writing:
// final class Point {
//   private final int x;
//   private final int y;
//   Point(int x, int y) { this.x = x; this.y = y; }
//   int x() { return x; }
//   int y() { return y; }
//   public boolean equals(Object o) { ... compares x and y ... }
//   public int hashCode() { ... derived from x and y ... }
//   public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
// }

x и y в заголовке — это компоненты record. Все члены, генерируемые компилятором, полностью выводятся из них в порядке объявления.

Канонические и компактные конструкторы

У каждого record есть канонический конструктор, параметры которого соответствуют компонентам. Его редко пишут в полном виде — вместо этого используют компактный конструктор, который опускает список параметров и конечные присваивания this.field = field. Компилятор сначала выполняет ваш код, а затем присваивает (возможно, изменённые) параметры полям. Это естественное место для валидации и нормализации.

record Range(int low, int high) {
  Range {                                  // compact constructor — no (int low, int high)
    if (low > high) {
      throw new IllegalArgumentException("low must be <= high");
    }
    low = Math.max(low, 0);                // reassigning the parameter normalizes the field
  }
}

Если вам когда-либо понадобится явный канонический конструктор (например, чтобы сделать защитную копию изменяемого компонента), напишите полную сигнатуру и выполните присваивания самостоятельно:

record Tags(String name, List<String> values) {
  Tags(String name, List<String> values) {           // explicit canonical constructor
    this.name = name;
    this.values = List.copyOf(values);               // defensive, unmodifiable copy
  }
}

Неизменяемость и то, чем records не являются

Поля record являются final, поэтому ссылка, которую хранит каждый компонент, никогда не меняется после конструирования. Это делает records неизменяемыми на поверхностном уровне. Однако неизменяемость ограничивается ссылкой: если компонент указывает на изменяемый объект (например, ArrayList), вызывающий код, разделяющий этот объект, по-прежнему может изменять его содержимое. Защитные копии в каноническом конструкторе закрывают эту брешь.

СвойствоRecordsОбычные классы
Полявсегда private finalна ваш выбор
Класснеявно finalрасширяемый, если не final
Суперклассвсегда java.lang.Recordлюбой (по умолчанию Object)
Методы доступагенерируются автоматически, без префикса getсоздаются вручную
equals/hashCodeоснованы на значениях, генерируютсяпо умолчанию идентичность
Сеттерынет — неизменяемыдопускаются

Поскольку record всегда расширяет java.lang.Record, он не может расширять другой класс. Тем не менее он может реализовывать интерфейсы, объявлять статические члены и добавлять методы экземпляра.

Добавление поведения, статических членов и фабричных методов

Record по-прежнему является классом. Вы можете добавить в него дополнительные методы, статические фабричные методы, статические поля и даже вложенные типы. Компоненты определяют состояние; всё остальное — обычный Java.

record Money(String currency, long cents) {
  static Money of(String currency, long cents) {     // static factory
    return new Money(currency, cents);
  }
  Money plus(Money other) {                          // derived behavior
    if (!currency.equals(other.currency)) {
      throw new IllegalArgumentException("currency mismatch");
    }
    return new Money(currency, cents + other.cents); // returns a new value
  }
}

Records также хорошо сочетаются с запечатанными типами и сопоставлением с образцом, моделируя замкнутые наборы форм данных — основу алгебраического стиля проектирования данных в современном Java. Запечатанный интерфейс фиксирует набор допустимых реализаций-records, а switch по этим records позволяет деструктурировать каждый из них по его компонентам в одном выражении.

Практический пример: records от начала до конца

Эта программа демонстрирует работу сгенерированных членов record, проверяет гарантии неизменяемости и свойства класса через рефлексию, обеспечивает соблюдение инварианта в компактном конструкторе, выводит компоненты record в порядке объявления и показывает работу records с коллекциями и добавленным поведением.

java— editable, runs on the server

Что следует вынести из запуска:

  • Point, для которого не было написано никакого тела, всё равно вывел Point[x=3, y=4], ответил на a.x() и сообщил equals by value: true с совпадающими хэш-кодами — компилятор сгенерировал основанные на значениях toString, методы доступа, equals и hashCode исключительно из двух компонентов.
  • Рефлексия подтвердила контракт, гарантированный языком: is final class : true (records нельзя создавать подклассы) и is a record : true (каждый record расширяет java.lang.Record), что объясняет отсутствие сеттеров и неизменяемость полей.
  • Вызов Range(9, 2) был отклонён с сообщением low must be <= high. Компактный конструктор выполняется до присвоения полей, поэтому record никогда не создаётся в недопустимом состоянии — валидация принадлежит именно туда, а не в отдельную проверку фабрики.
  • getRecordComponents() вернул компоненты в порядке объявления: low:int high:int, демонстрируя, что структура record доступна через рефлексию — именно это является основой для библиотек сериализации и фреймворков, автоматически отображающих records.
  • Money.of("USD", 500).plus(Money.of("USD", 250)) вернул USD 750, а distinct() свёл два одинаковых значения Point(0,0) к 2 — records ведут себя как полноценные значения везде, включая потоки и множества, именно потому, что их equals/hashCode сравнивают содержимое.

Когда использовать record (и когда не стоит)

Выбирайте record, когда тип определяется своими данными и эти данные не меняются после создания объекта:

  • DTO и полезные нагрузки запросов/ответов API.
  • Ключи Map и элементы Set (основанные на значениях equals/hashCode достаются бесплатно).
  • Возвращаемые типы, объединяющие несколько значений, заменяя одноразовые кортежи или out-параметры.
  • «Листья» запечатанной иерархии, которые вы деструктурируете с помощью сопоставления с образцом.

Предпочтите обычный класс, когда:

  • Объект имеет изменяемое состояние или жизненный цикл (сущности, строители, сервисы).
  • Нужно расширить другой класс — records могут только реализовывать интерфейсы.
  • Идентичность объекта важнее его содержимого (нужно ссылочное равенство).

Распространённая ловушка: метод доступа record возвращает хранимую ссылку как есть. Если компонент является изменяемым типом (List, массив, Date), сделайте защитную копию в каноническом конструкторе — как в примере Tags выше с List.copyOf — иначе вызывающий код сможет изменить «неизменяемое» состояние record через переданную ссылку.

Практика

Практика
Что позволяет делать компактный конструктор record (например, 'Range { ... }'), для чего в обычном явном конструкторе потребовалось бы больше кода?
Что позволяет делать компактный конструктор record (например, 'Range { ... }'), для чего в обычном явном конструкторе потребовалось бы больше кода?
Was this page helpful?