Данная статья является переводом (оригинал) за авторством 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 просты и полезны, но стоит помнить и о подводных камнях.
SerializableStableValue не поддерживает 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», я рад появлению инструмента, который помогает оптимизировать код. С нетерпением жду следующих обновлений.
Комментарии (0)
 - kacetal13.09.2025 20:46- А кто-то смотрел исходники? Какой механизм позволяет делать потоко безопасное присваивание? Всё тот же double check locking?  - pkokoshnikov13.09.2025 20:46- Как будто это единственный способ? CAS?  - UbuRus13.09.2025 20:46- Не просто CAS, т.к нужна гарантия что саплаер будет вызван только раз, используют UNSAFE и локи 
 
 
 - Bifurcated13.09.2025 20:46- У них есть jep с новым ключевым словом lazy, но это на будущее. Они выпустили обзор на канале java по разбору этой фичи, там более подробно объясняют и показывают примеры использования https://youtu.be/uMypEIx8qY8?si=KQn3d-Ad8_Dwt5_r  - UbuRus13.09.2025 20:46- Есть превью этого джепа? Пример синтаксиса? 
 Странно тащить ключевое слово ради этого, лучше бы делегаты как в Котлине завезли и можно было бы и lazy делешат сделать (https://youtrack.jetbrains.com/issue/KT-80669/Add-Lazy-implementation-using-JDKs-25-StableValues-API) и много чего еще красивого и DSL-подобного
 
 
           
 
BugM
Нормальный синтаксис в итоге получился. Четкое соответствие Optional и по виду и по поведению и по сигнатурам. Просто и понятно в итоге.