JEP 502 — Stable Values. Новая фича из Java 25

от автора

Данная статья является переводом (оригинал) за авторством Sebastian Rabiej


В этой статье мы рассмотрим JEP 502 — Stable Values. Это новая возможность, которая появится в Java 25 — следующем LTS-релизе, намеченном на сентябрь. Фича будет представлена как первая preview-версия, следовательно, все еще может измениться.

Что такое Stable Value?

StableValue<T> — это контейнер, который хранит единственное значение типа T. После первого присвоения значение становится неизменяемым. Данный подход можно рассматривать как «в итоге становящемся final» — значение (eventually final).

Важно: неизменяемой становится ссылка на объект. Сам объект под этой ссылкой может изменяться.

До Stable Values

До Java 25, чтобы добиться неизменности, мы использовали ключевое слово final:

class Controller {   private final EmailSender sender = new EmailSender(); }

У этого подхода есть недостатки.

Если у нас есть final-поле, его нужно инициализировать заранее — через конструктор или как статическое поле. Из-за этого запуск приложения может замедляться: ведь далеко не все поля нужны сразу, верно?

Можно убрать final и сделать ленивую инициализацию:

class PetClinicController {    private EmailSender sender = null;      EmailSender getSender() {     if (sender == null) {       sender = new EmailSender();     }     return sender;   }    void adoptPet(User user, Pet pet) {     // some logic here     getSender().sendEmailTo(user, "You are great person!");   } }

Так работать будет, и запуск приложения действительно станет быстрее. Но за такой подход придется заплатить:

  • sender остаётся изменяемым — мы можем присвоить ему другое значение. Придётся полагаться на внешние инструменты/правила код-ревью, чтобы этого не допустить.

  • Появляется риск NullPointerException, если доступ к полю получить не через геттер.

  • Геттер хорошо бы сделать потокобезопасным.

  • Даже если всё учесть, мы мешаем JVM оптимизировать доступ к полю (например, через constant-folding).

Переходим на Stable Values

Возьмём тот же пример и перенесём его на новую Java 25:

class PetClinicController {    private final StableValue<EmailSender> sender = StableValue.of();    EmailSender getSender() {     return sender.orElseSet(() -> new EmailSender());   }    void adoptPet(User user, Pet pet) {     // some logic here     getSender().sendEmailTo(user, "You are great person!");   } }

Код очень похож, но StableValue берёт на себя управление null-значением для EmailSender. Благодаря этому невозможно использовать EmailSender, не вызвав прежде метод получения значения.

Значение внутри StableValue гарантированно устанавливается потокобезопасно.

Stable-функции

StableValue служит базой для более высокоуровневых функциональных абстракций. Сейчас есть три варианта stable-функций.

Supplier

Функция вычисляется ровно один раз, а результат кешируется и возвращается при следующих обращениях. Можно использовать в нашем контроллере:

class PetClinicController {    private Supplier<EmailSender> sender = StableValue.supplier(() -> new EmailSender());    void adoptPet(User user, Pet pet) {     // some logic here     sender.get().sendEmailTo(user, "You are great person!");   } }

intFunction

Функция принимает int и вычисляет результат, который затем кешируется для данного значения параметра. Полезно, например, в математических задачах.

private final int SIZE = 3; private final IntFunction<Integer> INT_FUNCTION = v -> {   // Simulate expensive computation   log("Computing value for: " + v);   return 42; };  private void runIntFunction() {   var integerIntFunction = StableValue.intFunction(SIZE, INT_FUNCTION);    log(integerIntFunction.apply(1));   log(integerIntFunction.apply(1));   log(integerIntFunction.apply(1));    log(integerIntFunction.apply(2)); }

Результат вызова runIntFunction:

Computing value for: 1 Value: 42 Value: 42 Value: 42 Computing value for: 2 Value: 42

Главная особенность здесь — параметр size: диапазон входов нужно объявить заранее. Если выйти за пределы (например, 3), будет runtime-исключение: Input not allowed: 3.

Function

Более общий вариант intFunction: можно вызывать функцию с чем угодно.

private Set<Color> KEYS = Set.of(Color.GRAY, Color.GOLDEN); private Function<Color, HowCute> CUTE_FUNCTION = color -> {   System.out.println("Computing cuteness for: " + color);   return switch (color) {     case RED -> HowCute.CUTE;     case GRAY -> HowCute.VERY_CUTE;     case GOLDEN -> HowCute.SUPER_CUTE;   }; };  private void runCuteFunction() {   var cuteFunction = StableValue.function(KEYS, CUTE_FUNCTION);    log(cuteFunction.apply(Color.GOLDEN));   log(cuteFunction.apply(Color.GOLDEN));   log(cuteFunction.apply(Color.GOLDEN));    log(cuteFunction.apply(Color.GRAY));    log(cuteFunction.apply(Color.RED)); }

Результат:

Computing cuteness for: GOLDEN Value: SUPER_CUTE Value: SUPER_CUTE Value: SUPER_CUTE Computing cuteness for: GRAY Value: VERY_CUTE Exception in thread "main" java.lang.IllegalArgumentException: Input not allowed: RED

Как видно, даже если функция в принципе умеет работать с таким входом, но он не указан в заранее разрешённом наборе inputs, вы получите исключение Input not allowed.

Stable-коллекции

Можно использовать неизменяемые коллекции вместе со StableValue. Пока доступны только List и Map.

List<Integer> list = StableValue.list(SIZE, INT_FUNCTION); Map<Color, HowCute> map = StableValue.map(KEYS, CUTE_FUNCTION);

Из типов коллекций не видно, что внутри используются StableFunction — это удобно для сложных вычислений. Правда, есть ограничение: для списка применима только intFunction.

Возможные проблемы

Stable Values просты и полезны, но стоит помнить и о подводных камнях.

Serializable
StableValue не поддерживает Serializable. В самом JEP это не упомянуто, но информацию об этом можно найти тут. Если вы активно используете сериализацию и захотите заменить все final на StableValue, тут могут возникнуть трудности.

Без final
Используя StableValue, не забывайте, что переменной по-прежнему можно присвоить другое значение. Ничто (кроме здравого смысла) не мешает сделать так:

private StableValue<EmailSender> sender = StableValue.of(); void adoptPet(User user, Pet pet) {   sender = null;   // some logic here   sender.orElseSet(...) // Null pointer exception }

Лучше помечать все поля StableValue как final, хотя это и не обязательно. Ситуация очень напоминает Optional<>.

Возникает вопрос: почему не ввели новый ключевое слово — что-то вроде Lazy, как в других языках? Посмотрим на мотивацию JEP.

Какова цель JEP?

Из JEP следует, что цель авторов — ускорить запуск Java-приложений, отделив создание «стабильных» значений от инициализации. При этом обеспечить корректную работу в многопоточной среде и дать JVM возможность применять оптимизации вроде constant-folding.

Они не стремятся:

  • расширять язык Java новым способом объявления «стабильных» значений;

  • менять семантику final-полей.

Так что, хотя лично мне хотелось бы видеть «Lazy», я рад появлению инструмента, который помогает оптимизировать код. С нетерпением жду следующих обновлений.



ссылка на оригинал статьи https://habr.com/ru/articles/946682/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *