Большая часть классов в прикладных программах — это просто контейнеры данных: точка, адрес, строка отчёта, ответ сервера. Для каждого такого класса в старой Java приходилось писать поля, конструктор, геттеры, equals, hashCode и toString, то есть сорок строк ради двух полей. Запись (record, стандарт с Java 16) описывает то же самое одной строкой и гарантирует, что данные не изменятся после создания.
record Point(int x, int y) {}
var p = new Point(3, 4);
System.out.println(p.x()); // 3
System.out.println(p); // Point[x=3, y=4]
System.out.println(p.equals(new Point(3, 4))); // true
Компилятор сам создаёт канонический конструктор с параметрами в порядке объявления, методы доступа с именами компонентов без приставки get, а также equals, hashCode и toString, основанные на всех компонентах.
final-поля: у записи нет сеттеров, и она не может наследоваться от другого класса.final: расширить её тоже нельзя, но она может реализовывать интерфейсы.== по-прежнему сравнивает адреса.Проверять входные данные в записи можно, не переписывая весь конструктор. Компактный конструктор объявляется без списка параметров, а присваивание полей компилятор дописывает сам в конце:
record Money(long kopecks, String currency) {
Money {
if (kopecks < 0) {
throw new IllegalArgumentException("Сумма не может быть отрицательной");
}
currency = currency.strip().toUpperCase(); // нормализация до присваивания
}
static Money rub(long rubles) {
return new Money(rubles * 100, "RUB");
}
Money plus(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("Разные валюты");
}
return new Money(kopecks + other.kopecks, currency);
}
}
var total = Money.rub(150).plus(Money.rub(49));
System.out.println(total); // Money[kopecks=19900, currency=RUB]
Этот пример не проверить в JShell
Оболочка JShell из JDK 21 не умеет объявлять записи с компактным конструктором: она отвечает invalid canonical constructor in record Money (attempting to assign stronger access privileges; was public). Это ограничение самой оболочки, а не ошибка в коде — сохраните пример в файл и запустите командой java Money.java. Записи без компактного конструктора JShell обрабатывает нормально.
Запись может содержать статические фабричные методы и обычные методы экземпляра. Изменить состояние они не могут, поэтому «модифицирующие» операции возвращают новую запись, как plus в примере.
Если компонент — коллекция, сделайте в компактном конструкторе защитную копию. List.copyOf создаёт неизменяемый список и отвергает null-элементы:
record Group(String name, java.util.List<String> members) {
Group {
members = java.util.List.copyOf(members);
}
}
var source = new java.util.ArrayList<>(java.util.List.of("Анна", "Борис"));
var group = new Group("Первая", source);
source.add("Вера");
System.out.println(group.members().size()); // 2 — копия не изменилась
Благодаря корректным equals и hashCode записи безопасно использовать в HashMap и HashSet. Это избавляет от классической ошибки, когда самодельный класс без hashCode «теряется» в множестве:
record Cell(int row, int column) {}
var visited = new java.util.HashSet<Cell>();
visited.add(new Cell(1, 1));
System.out.println(visited.contains(new Cell(1, 1))); // true
Запись можно объявить прямо внутри метода, чтобы временно связать несколько значений. Это удобнее, чем массив или пара из библиотеки:
static String longestWord(java.util.List<String> words) {
record Scored(String word, int length) {}
return words.stream()
.map(w -> new Scored(w, w.length()))
.max(java.util.Comparator.comparingInt(Scored::length))
.map(Scored::word)
.orElse("");
}
Локальные записи неявно статические: они не видят переменные окружающего метода и не захватывают this.
Когда запись не подходит
Если объекту нужно менять состояние (счётчик, кэш, сущность с изменяемыми полями), наследование от другого класса или скрытые поля, не входящие в равенство, — используйте обычный класс. Запись — это прозрачный носитель данных, а не универсальная замена классам.
Современные библиотеки (Jackson, Gson последних версий, встроенные средства фреймворков) создают записи из JSON и обратно без дополнительных аннотаций: имена компонентов совпадают с ключами. Это ещё одна причина описывать структуры запросов и ответов именно записями.
Temperature(double celsius) с компактным конструктором, который запрещает значения ниже абсолютного нуля (−273,15 °C), и методом fahrenheit().Student(String name, java.util.List<Integer> grades) с защитной копией списка и методом average(). Проверьте, что изменение исходного списка не влияет на запись.Cell с повторяющимися координатами в HashSet и объясните, почему размер множества меньше десяти.