Про record


Появились в версии Java 14, но я предпочитаю использовать LTS версии, поэтому начал активно использовать только в Java 17.

Не вижу смысла в очередной раз переписывать однотипные статьи из интернета с одними и тем же примерами record-ов вида “Hello World”. Здесь хотел поделиться своими впечатлениями после нескольких месяцев использования, когда и как их удобно использовать, а также небольшими техническими нюансами.

Если кратко, то самое банальное использование record-ов, это применять их вместо кортежей (tuple). В более сложных кейсах – создание объекта для хранения/передачи данных. Причем хранение и передачи не в контексте работы с СУБД или передачи через сеть, а в самом простом смысле – внутри кода.

Вот простой пример. Представьте, допустим был какой-то метод:

String findFoo(...)

Пусть этот метод возвращает какой-то строковый идентификатор (ID).

Теперь представим, что ситуация изменилась и нужно возвращать не только ID, а например ещё булевый флаг true/false.

После наступления такого события у некоторых java-программистов (особенно начинающих) могут начаться душевные метания, экзистенциальный кризис. Возникает внутренний конфликт между необходимостью вернуть из метода сразу несколько значений и отсутствием такой возможность в синтаксисе Java. Причем осознание того, что в языках программирования Scala или Python такая возможность есть ещё больше усиливает страдания и начинает порождать мысли о том, что дальнейшее программирование на Java лишено смысла и нужно что-то делать. Потом возникают различные шальные идеи:

  • А что если просто сделать еще один метод, который делает почти тоже самое, но возвращает булевый флаг, например: boolean findFooFlag(..)?
  • А что если сделать мапу/лист/массив и в него положить два значения, сейчас же это просто! Есть Map.of(), List.of() или вообще все преобразовать к String[]?
    Был метод String findFoo(...), станет String[] findFoo(...).
  • А может взять какой-нибудь готовый Pair<String, Boolean>, смысл изобретать велосипед?
    Был метод String findFoo(...), станет Pair<String, Boolean> findFoo(...). На стековерфлоу предлагают кучу библиотек – javatuples, apache commons collections, Map.Entry и т.д.

Получается вместо того, чтобы сделать простой класс, который содержит пару полей, программист начинает искать разные обходные варианты и избегает создания нового класса. Иногда я даже замечаю, что у некоторых программистов есть особая фобия – боязнь создания нового класса – NewClassPhobia, т.к. для них создание совершенно нового класса, это равносильно выходу из зоны комфорта.

Сделать REST-контроллер на Spring-е, который смотрит в БД и возвращает данные – это за 5 минут, никаких проблем. Взять готовый код, поправить в нем какие-то хитрые условия, критерии выборки, разобрать бизнес-логику – 10-15 минут и никаких проблем…

Проблема возникает когда нужно создать концептуально новый класс, не используя типовую Spring заготовку (контроллер, компонент и т.д.). Другими словами обычный новый логический класс, с полями и методами.

Я вижу несколько причин.

Во-первых лень. Зачем делать, когда можно не делать и взять готовый Pair, Tuple и т.д.?
Во-вторых нужно придумывать название для класса. Это сложно.
В-третьих это “много строк кода” – объявить поля, сделать конструктор, сделать get***() методы для каждого поля. Среда разработки конечно делает эту всю работу за программиста, но все равно нужно лишний раз нажимать на кнопки.


Ситуация становится совсем фатальной, если добавляется еще один объект, например метод должен возвращать ещё какой-нибудь внешний идентификатор, например long extId. В таком случае Pair не проходит, а делать Pair<String, Pair<Boolean, Long>> или подключать Triplet<String, Boolean, Long> уже перебор.

Так вот тут нам и помогут record-ы.

Объявляем например:

record Foo(String id, boolean enabled, long extId) {} 

Головная боль уходит!

Правда есть неудобство, если собираем больше 3-х полей в одном рекорде, то начинает пухнуть конструктор. С учетом того, что в java нет именованных параметров, легко ошибиться и перепутать поля, особенно если все четыре поля – строки. Можно попробовать решить это через билдер, но совсем другая история.

Кроме этого неудобства, в остальном мне очень нравятся рекорды. Их удобно использовать как в императивном стиле, так и в функциональном, особенно удобно для map-инга в стримах.

В них есть сразу готовые методы для доступа к полям. В нашем случае id(), enabled(), extId(). Есть готовый toString(), hashCode(), equals().
Благодаря toString() чуть меньше мороки с логами, а готовый hashCode()/equals(), полезная штука в качестве защиты от дурака, например если рекорды складывать в коллекции.

Еще рекорды удобны если их вложить внутрь класса. Согласно спецификации вложенный рекорд будет неявный static, т.е. не будет прицепом тащить за собой внешний объект. Более того, его можно объявлять локально, т.е. внутри метода.

Кстати, для public record-ов конструктор будет всегда public. Просто есть любители статичных фабричных методов, которые прячут private-конструкторами возможность создания объекта. Здесь спрятать просто так не получится.

Есть интересный вид конструктора, называется компактный конструктор.
Например вот такой:

record Foo(String id, boolean enabled, long extId) {
    Foo {
        Objects.requireNonNull(id);
   }
}

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

Я думаю, что это не будет популярно. Будет как с assert-ами – штука полезная, но мало кто умеет пользоваться. Из моей практики, рекорды это простые объекты, которыми передают данные и компонуют объекты. По крайней мере я только так их и использую.

В целом по поводу данных, похоже увлечение функциональщиной проходит и отрасль начинает двигаться в сторону data oriented programming (не путать с data driven development и data-oriented design).

Также с нетерпением жду когда в java появятся а-ля алгебраические типы данных. Сейчас уже через record-классы – мы можем делать подобие умножения, а через sealed-классы – сложение.

Если из preview режима выведут type pattern matching, то можно начать использовать. По-крайней мере новый instaceof оказался очень удобный.

До Scala конечно далеко, но таков Java путь.

PS: У меня слетели все комментарии к блогу, т.к. были через фб, а как подключить новые еще не придумал. Без комментариев конечно грустно. Может придумаю что-то…
Еще после очередного обновления WP слетело форматирование кода, тоже буду думать, что с этим делать.