Records (Java 14+)

Record — это специальный тип класса для хранения иммутабельных данных. Предложен в Java 14 как preview-фича (JEP 359) и стабилизирован в Java 16 (JEP 395). Records радикально сокращают boilerplate: одна строка заменяет 40+ строк класса с полями, конструктором, геттерами, equals, hashCode и toString.

По сути record — это «прозрачный носитель» неизменяемых данных (transparent carrier for immutable data). Компилятор сам генерирует все стандартные методы.

Минимальная версия: Java 14 (preview), стабильно с Java 16.

Синтаксис

public record ИмяРекорда(Тип1 поле1, Тип2 поле2, ...) { }

Что компилятор генерирует автоматически:

  • приватные final поля;

  • публичный конструктор с теми же параметрами (canonical constructor);

  • публичные методы доступа с именами полей (поле1(), не getПоле1());

  • equals(), hashCode(), toString().

Зачем нужно

  • Меньше кода — меньше ошибок.

  • Иммутабельность по умолчанию — безопасно для многопоточности.

  • Чёткая семантика: «это данные, а не поведение».

  • Отлично подходит для DTO, ключей map, value-объектов.

Пример 1. Базовый record

public class RecordsBasic {
    public record Point(int x, int y) { }

    public static void main(String[] args) {
        var p = new Point(3, 7);
        System.out.println("x = " + p.x());
        System.out.println("y = " + p.y());
        System.out.println(p);
    }
}

Output:

x = 3
y = 7
Point[x=3, y=7]

Пример 2. equals и hashCode из коробки

import java.util.HashSet;

public class RecordsEquals {
    public record Sensor(String name, int pin) { }

    public static void main(String[] args) {
        var a = new Sensor("trig", 3);
        var b = new Sensor("trig", 3);
        var c = new Sensor("echo", 7);

        System.out.println("a.equals(b) = " + a.equals(b));
        System.out.println("a.equals(c) = " + a.equals(c));

        var set = new HashSet<Sensor>();
        set.add(a);
        set.add(b);
        set.add(c);
        System.out.println("Размер множества: " + set.size());
    }
}

Output:

a.equals(b) = true
a.equals(c) = false
Размер множества: 2

Пример 3. Компактный конструктор с валидацией

public class RecordsCompact {
    public record Pin(int number) {
        // Компактный конструктор: без списка параметров
        public Pin {
            if (number < 0 || number > 19) {
                throw new IllegalArgumentException("Pin вне диапазона: " + number);
            }
        }
    }

    public static void main(String[] args) {
        var ok = new Pin(13);
        System.out.println("OK: " + ok);
        try {
            var bad = new Pin(42);
        } catch (IllegalArgumentException e) {
            System.out.println("Ошибка: " + e.getMessage());
        }
    }
}

Output:

OK: Pin[number=13]
Ошибка: Pin вне диапазона: 42

Пример 4. Дополнительные методы и static

public class RecordsMethods {
    public record Motor(int in1, int in2, int pwm) {
        public static Motor leftRobotPhobo() {
            return new Motor(8, 12, 6);
        }
        public static Motor rightRobotPhobo() {
            return new Motor(4, 2, 5);
        }
        public String describe() {
            return "Motor[in1=" + in1 + ", in2=" + in2 + ", pwm=" + pwm + "]";
        }
    }

    public static void main(String[] args) {
        var left = Motor.leftRobotPhobo();
        var right = Motor.rightRobotPhobo();
        System.out.println("Left:  " + left.describe());
        System.out.println("Right: " + right.describe());
    }
}

Output:

Left:  Motor[in1=8, in2=12, pwm=6]
Right: Motor[in1=4, in2=2, pwm=5]

Пример 5. Реализация интерфейса

public class RecordsInterface {
    interface Named {
        String name();
    }

    public record Robot(String name, int wheels) implements Named { }

    public static void main(String[] args) {
        Named n = new Robot("Phobo", 4);
        System.out.println("Имя: " + n.name());
        System.out.println("Объект: " + n);
    }
}

Output:

Имя: Phobo
Объект: Robot[name=Phobo, wheels=4]

Пример 6. Локальный record (Java 16+)

import java.util.List;

public class RecordsLocal {
    public static void main(String[] args) {
        // record можно объявить прямо внутри метода
        record Reading(String sensor, double value) { }

        var readings = List.of(
            new Reading("ultrasonic", 12.4),
            new Reading("line-left", 540.0),
            new Reading("line-right", 612.0)
        );

        for (var r : readings) {
            System.out.println(r.sensor() + " = " + r.value());
        }
    }
}

Output:

ultrasonic = 12.4
line-left = 540.0
line-right = 612.0

Подводные камни

Предупреждение

  • Records неявно final — наследовать от них нельзя.

  • Все поля final — модифицировать их после создания нельзя (но если поле — изменяемый объект, например List, его содержимое менять можно; для полной иммутабельности используйте List.copyOf).

  • Records не могут наследоваться от других классов, но могут реализовать интерфейсы.

  • Нельзя добавить дополнительные instance-поля вне списка компонентов.

  • Геттеры называются field(), а не getField() — это ломает совместимость с фреймворками, ожидающими JavaBeans.

Совет

Если объект изменчив или имеет сложное поведение — это не record, а обычный класс. Record предназначен именно для данных.

См. также

Примечание

Материал основан на официальной документации Oracle Java SE (docs.oracle.com/en/java/javase) и спецификациях JEP 359/395 (openjdk.org/jeps/395), распространяемой под лицензией Oracle Free Documentation License. Тексты и примеры написаны заново для AlashEd Wiki.