About this article:
Language: Java
Framework: None
Difficulty: Intermediate
Related Topics: Immutability, concurrency, development practices
Problem Domain
Often, I encounter cases like this:
void mutableUseCase(Integer someInput) {
/*
* This method demonstrates a mutable use case where the variable `mutableVar`
* depends on an internal process. The value of `mutableVar` is set only after
* a condition is evaluated.
*/
Integer mutableVar;
/*
* Internal process based on the input value.
* This block could involve a complex computation or require external dependencies.
*/
final Integer internalDependence = someHardProcess(someInput);
/*
* Condition where the value of `mutableVar` is determined.
*/
if (someInput > 0) {
mutableVar = internalDependence;
} else {
mutableVar = internalDependence + SOME_CALC;
}
nextStep(mutableVar);
}
void nextStep(final Integer someInput) {
System.out.println(someInput);
}
Given an input, we need to define the value of mutableVar
, which depends on a process (someHardProcess
), such as a JDBC adapter call, and an additional condition like an if/else
block, before passing it to a subsequent process (nextStep
).
The hidden problem in such cases lies in reference variations in concurrent environments. While waiting for the value of the mutable variable to be resolved, another process could redefine the reference in memory.
Modern languages like Kotlin and Rust, among others, declare memory references as immutable by default.
Approaches to Solve This Problem
1. Using Java's Functional Approach with a Supplier
void approachUsingSupplierUseCase(Integer someInput) {
final Integer internalProcess = someHardProcess(someInput);
final Supplier<Integer> immutableInputSupplier = () -> {
if (someInput > 0) {
return internalProcess;
} else {
return internalProcess + SOME_CALC;
}
};
nextStep(immutableInputSupplier.get());
}
This approach is my personal favorite because it leverages the closure property of functional programming, allowing access to all resources defined within the higher-order function, such as internalProcess
, which is immutable.
() -> {
if (someInput > 0) {
return internalProcess;
} else {
return internalProcess + SOME_CALC;
}
}
The execution of the immutable instance immutableInputSupplier
ensures thread-safe execution within concurrent blocks, and its returned value is securely passed to the nextStep
method.
Advantages:
With closures, it's possible to access all resources defined in the higher-order function.
Refactoring is more natural.
Disadvantages:
- Testing the nested function is impossible without testing the entire container function.
2. Using a Stateless Class Method
Another way to address the initial problem is through class methods, such as getImmutableInput
, serving the same purpose as the Supplier
above.
⚠️ Important: This method must be a pure function, i.e., it should not depend on external state (commonly referred to as hidden state).
void approachUsingMethodReference(final Integer someInput) {
final Integer internalProcess = someHardProcess(someInput);
nextStep(getImmutableInput(someInput, internalProcess));
}
Integer getImmutableInput(final Integer input, final Integer internalDependence) {
if (input > 0) {
return internalDependence;
} else {
return internalDependence + SOME_CALC;
}
}
Advantages:
Within Java, it avoids creating a new function object.
Since it's externally accessible, unit testing can be performed on the function independently.
As a stateless method, it can be declared static.
Disadvantages:
The method interface will have as many arguments as external dependencies required for its execution. In this example, there are only two:
input
andinternalDependence
.It clutters the class contract with auxiliary methods.
Increasing Complexity
Rewriting the initial problem with an additional common scenario: a try/catch
block. Depending on its execution, we will assign a specific value.
Here, the debated variable mutableVar
is declared outside the try/catch
block while awaiting the resolution of its value.
As seen in the block below, the possible outcomes for mutableVar
can be:
internalDependence
internalDependence + SOME_CALC
0
void mutableWithExceptionUseCase(final Integer someInput) {
Integer mutableVar;
try {
final Integer internalDependence = someHardProcess(someInput);
if (someInput > 0) {
mutableVar = internalDependence;
} else {
mutableVar = internalDependence + SOME_CALC;
}
} catch (Exception e) {
mutableVar = 0;
}
nextStep(mutableVar);
}
For this particular case, I developed a class called SafeSupplier
, which complements Optional
. It allows us to rewrite the above code as follows:
void approachUsingSafeSupplier(Integer someInput) {
final Integer internalProcess = someHardProcess(someInput);
nextStep(SafeSupplier.tryGet(() -> {
if (someInput > 0) {
return internalProcess;
} else {
return internalProcess + SOME_CALC;
}
})
.ifErrorThenGet(() -> 0).get());
}
As shown, this refactoring results in a process that is not only safer in concurrent environments but also much more expressive and easier to understand.
SafeSupplier: A Reliable Way to Handle Errors in Functional Java Programming
The SafeSupplier
class can simplify error handling in Java by providing a safe and expressive way to manage exceptions in functional-style code. With SafeSupplier
, you can:
Safely attempt operations using
tryGet
, which wraps the result or captures any runtime exception.Handle errors gracefully with methods like
ifErrorThenGet
, enabling fallback values in case of failures.Throw custom exceptions when necessary using
ifErrorThenThrow
.Access results safely with methods like
getNullable
, wrapped inOptional
for enhanced safety and clarity.
public final class SafeSupplier<T> {
private final T value;
private final Throwable cause;
private SafeSupplier(Throwable cause) {
this.value = null;
this.cause = cause;
}
private SafeSupplier(T value) {
this.value = value;
this.cause = null;
}
static public <T> SafeSupplier<T> tryGet(Supplier<T> supplier) {
Preconditions.isNullOrEmpty(supplier, ()->
new IllegalArgumentException("Supplier cannot be null"));
try {
return new SafeSupplier<>(supplier.get());
} catch (RuntimeException exception) {
return new SafeSupplier<>(exception);
}
}
public boolean isError() {
return cause != null;
}
public Optional<T> ifErrorThenGet(Supplier<T> supplier) {
Preconditions.isNullOrEmpty(supplier, ()->
new IllegalArgumentException("Supplier cannot be null"));
if (isError()) {
return Optional.ofNullable(supplier.get());
}
return Optional.ofNullable(value);
}
public SafeSupplier<T> ifErrorThenThrow(Function<Throwable, ? extends RuntimeException> exceptionSupplier) {
Preconditions.isNullOrEmpty(exceptionSupplier, ()->
new IllegalArgumentException("Exception supplier cannot be null"));
if (isError()) {
throw exceptionSupplier.apply(cause);
}
return this;
}
public Optional<T> getNullable() {
if (isError()) {
return Optional.empty();
}
return Optional.ofNullable(value);
}
}
This lightweight utility ensures immutability and enhances code readability, making it a great addition for robust and concurrent-safe functional Java development.
Check the unit test section to learn how to implement and use SafeSupplier
!
Resources
Conceptual use case: src/main/java/fitodev/article/MutableUseCase.java
SafeSupplier: src/main/java/fitodev/utils/SafeSupplier.java
SafeSupplier(Unit testing): src/test/java/fitodev/utils/SafeSupplierTest.java