JEP 502 - Stable Values. New Feature of Java 25 Explained
In this article, we will look at JEP 502 - Stable Values. It’s a new feature that will appear with Java 25, our new LTS, planned to be released in September. It is going to be introduced as a first preview feature, which means that everything can be changed.
What is Stable Value?
A StableValue <T
> is a container that holds a single value of type T
. Once assigned, that value becomes immutable. You can think of it as an eventually final value.
It’s vital to notice that a reference to an object will be immutable. The object beneath can be changed.
Before Stable Values
Before Java 25, to achieve immutable data, we had to use the final keyword.
class Controller {
private final EmailSender sender = new EmailSender();
}
There were some problems with this approach.
Whenever our code has a final field, it has to be set eagerly, either initialized by a constructor or by having a static field. Because of that, the application's startup may suffer. Not all fields are immediately needed, right?
So maybe we could remove the final keyword and do something like that:
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!");
}
}
It’s going to work, right? The startup will be faster. And yes, that's one way to solve this issue before Java 25. Unfortunately, it's not free.
- Our sender is still mutable; we can assign a different value to this variable. However, we would need to use external tools to prevent us from doing that, or be very precise during code reviews.
- Potential null pointer exception while accessing a field from another way than just using a getter.
- It would be good to have "getSender" thread-safe.
- But even when we do so, we prevent the JVM from optimizing access for this field. For example, using constant-folding.
Let's use some Stable Values
Using our example from before, let's migrate to the new Java.
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!");
}
}
The code is very similar, but StableValue handles the nullability of EmailSender. Thanks to that, it’s impossible to call EmailSender without first calling a method to retrieve the value.
The value inside a StableValue is guaranteed to be set in a thread-safe way.
Stable functions
Stable Values provide the foundation for higher-level functional abstractions. Right now, we have three types of stable functions.
Supplier:
It is a function that will be computed only once, and the result of this will be stored and returned. For example, it can be used in our Controller.
class PetClinicController {
private Supplier<EmailSender> sender = StableValue.supplier(()-> new EmailSender(id));
void adoptPet(User user, Pet pet) {
// some logic here
sender.get().sendEmailTo(user, "You are great person!");
}
}
intFunction:
Function that takes an int parameter and uses it to compute a result that is then cached by the backing stable value storage for that parameter value. It can be helpful in mathematics.
As an example:
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));
}
When you call runIntFunction, response is:
Computing value for: 1
Value: 42
Value: 42
Value: 42
Computing value for: 2
Value: 42
The biggest issue with that is the size parameter. You need to declare upfront what the range of the input parameter is here.
If you go beyond the size of the function(e.g., 3), you will get a runtime exception "Input not allowed: 3"
Function:
It’s a more generic version of intFunction, where you can call your function with whatever you want.
For example:
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));
}
When you call runCuteFunction, response is:
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
As you may have noticed, even if your function will be able to work with input, if input is not defined in the inputs parameter, you will get an exception "Input not allowed".
Stable collections
We can also use unmodifiable collections together with stable values. For now, we only have List and Map.
List<Integer> list = StableValue.list(SIZE, INT_FUNCTION);
Map<Color, HowCute> map = StableValue.map(KEYS, CUTE_FUNCTION);
From the type, we don’t see that we use StableFunctions. Feels useful for complex computations. Although it is limited, as a list can only use intFunction. Hope to see more in the future.
Potential issues
Stable Values are simple and useful constructs. Yet it’s worth to be aware of the potential pitfalls of using them.
Serializable:
It’s worth noticing that Stable Values do not support Serializable. It’s not mentioned in JEP, but it’s possible to find it here.
So, if you use serializable and want to replace all finals with Stable Values, you may encounter some problems here.
Not using final:
Whenever we use StableValue, we need to remember the possibility of assigning a different value to this variable. Nothing prevents us from doing something like that (nothing but common sense, of course):
private StableValue<EmailSender> sender = StableValue.of();
void adoptPet(User user, Pet pet) {
sender = null;
// some logic here
sender.orElseSet(...) //Null pointer exception
}
You should use final with all your StableValue, but you are not forced to do so. It’s a case of Optional<> once again.
So why didn’t they introduce a new keyword? Something like “Lazy” from different languages? Let's look into their motivation.
What is the goal of this JEP?
From this JEP, we learn that the author's goal was to improve the startup of Java applications by decoupling the creation of stable values from initialization. Making sure that it will work with a multi-threaded environment and give us the possibility to use JVM optimization like constant-folding.
Their goal is not to:
- enhance the Java programming language with a means to declare stable values,
- alter the semantics of final fields.
So even if I personally would love to have "Lazy", I’m glad that they introduced something that will help us optimize code. Can’t wait for the following updates.
Reviewed by: Dawid Popczyk and Szymon Winiarz