Use Java Optionals For Safer, Cleaner and Functional Code
Table of Contents
What are Java Optionals? #
Java Optionals were introduced in Java 8 as part of the java.util package. The primary purpose of introducing Optional is to provide a container that may or may not contain a non-null value, thus helping to avoid potential NullPointerException issues.
An Optional object can either contain a value or be empty. It provides methods to check if a value is present, retrieve the value if it is present, and perform actions based on the presence or absence of a value.
Creating an Optional #
You can create an Optional object using the of, ofNullable, or empty methods.
Using of() #
The of method creates an Optional object that contains a non-null value. If you pass a null value to the of method, it will throw a NullPointerException. It is typically used when you are sure that the value is not null, such a constant value or a primitive type.
String value = "Hello, World!";
Optional<String> optional = Optional.of(value);
// throws NullPointerException
String value = null;
Optional<String> optional = Optional.of(value);
// with a primitive type
Optional<Integer> optional = Optional.of(10);
Using empty() #
The empty method creates an empty Optional object.
Optional<String> optional = Optional.empty();
Using ofNullable() #
Since the of method throws a NullPointerException if you pass a null value, the ofNullable method is a safer way to create an Optional object when you are not sure if the value is null. If you pass a null value to the ofNullable method, it will create an empty Optional.
String value = null;
Optional<String> optional = Optional.ofNullable(value);
Optionals Reduce NullPointerExceptions #
Java programs often face NullPointerException when trying to access methods or properties of a null object. Before Java 8, developers had to manually check for null references, which made the code verbose and error-prone. The Optional class encourages proper handling of potentially null values, reducing these kinds of exceptions.
String value = null;
Optional<String> optional = Optional.ofNullable(value);
if (optional.isPresent()) {
System.out.println("Value is present: " + optional.get());
} else {
System.out.println("Value is absent");
}
Better Expressiveness with Optional #
Using Optional in method return types makes it explicit that a method might return a value, or it might not. This improves readability and clarity of code, making the developer aware that the result may be empty or non-existent.
public Optional<String> findValue() {
// some logic to find the value
return Optional.ofNullable(value);
}
Optionals Encourage Functional Programming #
Optionals are a good fit for functional programming paradigms. They provide methods like map, filter, and flatMap that allow you to perform operations on the value inside the Optional without explicitly checking for null.
Using map() #
Use the
mapmethod to change the value.
This is a common method used with Optionals to transform the value inside the Optional. IT will automatically cater for the case where the value is null.
The map method applies a function to the value inside the Optional if it is present. It returns an Optional object that contains the result of the function. The Function passed to the map method should be a Unary function that accepts one argument and returns a value.
Optional<String> optional = Optional.of("Hello, World!");
Optional<String> upperCaseOptional = optional.map(String::toUpperCase);
If the value is present, the upperCaseOptional will contain the uppercase version of the value. If the value is absent, the upperCaseOptional will be empty.
If the function passed to the map method returns null, the upperCaseOptional will be empty.
Using filter() #
A filter is used to check whether something meets a condition, and if it does, you keep it; if not, you discard it.
The filter method takes a Predicate as an argument and returns an Optional object that contains the value if it satisfies the Predicate. If the value does not satisfy the Predicate, the filter method returns an empty Optional.
Optional<Integer> optional = Optional.of(10);
// Apply filter to check if the value is greater than 5
Optional<Integer> filteredOptional = optional.filter(value -> value > 5);
filteredOptional.ifPresent(System.out::println); // Output: 10
Using flatMap() #
The flatMap method is used to transform the value inside the Optional and return another Optional. It is similar to the map method, but the function passed to the flatMap method should return an Optional.
In simple terms, flatMap is used to handle situations where you have an Optional inside another Optional (or some other nested structure), and you want to “flatten” it into a single Optional.
Optional<String> optionalString = Optional.of("Hello");
// Results in Optional<Optional<Integer>>
Optional<Optional<Integer>> mapped = optionalString.map(s -> Optional.of(s.length()));
// Results in Optional<Integer>
Optional<Integer> flatMapped = optionalString.flatMap(s -> Optional.of(s.length()));
So flatMap is useful when you’re dealing with nested optionals and want to “flatten” them into a single Optional. It helps simplify your code and avoids unnecessary layers of wrapping.
Optionals Produce Cleaner Code #
Methods like isPresent(), orElse(), orElseGet(), and orElseThrow() make the code cleaner and more readable, reducing boilerplate code.
Using isPresent() #
The isPresent method checks if the Optional contains a value. It returns true if the value is present, and false if the value is absent.
Optional<String> optional = Optional.of("Hello, World!");
if (optional.isPresent()) {
System.out.println("Value is present: " + optional.get());
} else {
System.out.println("Value is absent");
}
Using ifPresent() #
The ifPresent method takes a Consumer as an argument and executes the Consumer if the value is present. It is a more functional way to handle the presence of a value. The above isPresent() example can be rewritten using ifPresent() as follows.
Optional<String> optional = Optional.of("Hello, World!");
optional.ifPresent(value -> System.out.println("Value is present: " + value));
Using orElse() #
The orElse method returns the value if it is present; otherwise, it returns the default value passed as an argument.
Optional<String> optional = Optional.empty();
String value = optional.orElse("Default Value");
System.out.println(value); // Output: Default Value
Note orElse is always evaluated, even if the value is present. This can be a preformance issue if the default value is expensive to compute.
Optional<String> optional = Optional.of("Hello, World!");
// someSlowMethod() is always called, even if the value is present
String value = optional.orElse(someSlowMethod());
orElse is useful if you want a default value when the result of a stream is empty.
List<String> list = new ArrayList<>();
String result = list.stream().findFirst().orElse("Default Value");
System.out.println(result); // Output: Default Value
Using orElseGet() #
The orElseGet method is similar to orElse, but it takes a Supplier as an argument. The Supplier is only invoked if the value is absent, making it more efficient than orElse when the default value is expensive to compute.
Optional<String> optional = Optional.empty();
String value = optional.orElseGet(() -> "Default Value");
System.out.println(value); // Output: Default Value
Using orElseThrow() #
The orElseThrow method returns the value if it is present; otherwise, it throws an exception provided by the Supplier.
Optional<String> optional = Optional.empty();
String value = optional.orElseThrow(() -> new RuntimeException("Value is absent"));
Conclusion #
Java Optionals are a way to handle null values in a more expressive and type-safe manner. They provide a way to represent the absence of a value without resorting to null references, which can lead to NullPointerExceptions. Optionals encourage proper handling of potentially null values, reduce NullPointerExceptions, and make the code cleaner and more readable. They are a powerful tool in the Java developer’s toolkit for writing robust and error-free code.