A complete guide to null values, NullPointerException and How to use Java Optional to avoid NullPointerException.
Tutorial Contents
What is Null? What is a NullPointerException?
Java, by default, assigns null to the uninitialized object references.
public class TestClass {
String string1;
String string2 = null;
public static void main(String[] a){
TestClass testClass = new TestClass();
System.out.println(testClass.string1); // Output: null
System.out.println(testClass.string2); // Output: null
}
}
Code language: Java (java)
The string1 and string2 are two reference variables that are not assigned to any objects. Thus when we print them, we get null values.
What causes NullPointerException?
In Java, the reference variables are not objects but handle to the objects. To interact with an object, we have to create a reference variable of a suitable type and assign the reference variable to the object.
A reference variable that doesn’t refer to any object has a null value. Using the dot notation on such uninitialized variables results in the NullPointerException.
For example, the following is a snippet of code with a high chance of getting NullPointerException.
User user = service.getUser(id);
String state = user.getAddress()
.getState()
.toUpperCase();
Code language: Java (java)
The code block blindly relies on the data it receives from the service and will get a NullPointerException when one of the following statements is true.
- The User is null.
- The User’s Address is null.
- The ‘state‘ in the User’s Address is null.
Let’s see how to write a better code that avoids NullPointerExceptions.
Null Checks to avoid NullPointerException
The fundamental way of avoiding NullPointerException is to perform a null check on every reference object we try to access. The following block of code proactively performs null checks; thus, it is protected from NullPointerExceptions.
String state = null;
User user = service.getUser(id);
if (user != null) {
Address address = user.getAddress();
if (address != null) {
state = address.getState();
if (state != null) {
state = state.toUpperCase();
}
}
}
Code language: Java (java)
However, the code block now focuses more on null checks than minding its business. Also, because of the branches, the code block is hard to test, and it will be even more complicated when we have more nesting levels.
What is Java Optional?
The Java Optional is a container for storing a null or non-null value. On top of this, Java Optional provides convenient methods to access the object value, return defaults, throw exceptions, or perform null checks.
A Java method or a POJO should use the Optional type for potentially null objects meant for external consumption. For example, a field in a POJO or return type of a method. Their consumers can use the Optional instance to safely access the object without dealing with the null values or checks.
// Consumer needs to handle nulls proactively.
public User getUser(Long id);
// Optional keeps the consumer free from null checks
public Optional<User> getUser(Long id);
Code language: Java (java)
Benefits of using Java Optional
Here are some of the benefits of using Java Optional.
The Java Optional avoids explicit handling of null values. Hence it prevents the code from NullPointerExceptions or null checks.
The Optional type explicitly indicates the behaviour of the method. For example, the following method signature means that the method may or may not return the intended User.
public Optional<User> getUser(Long id){
User user = null;
// Fetch User
return Optional.of(user);
}
Code language: Java (java)
Also, the Optional improves documentation and readability. For example, let’s change the Address field in the User POJO to make it Optional. The statement “User has an Optional Address” is technically more correct.
public class User {
private String name;
private String lastName;
private Optional<Address> address;
...
}
Code language: Java (java)
How to create an Optional instance?
The Java Optional API provides several factory methods to create an Optional instance.
Create an Empty Optional Instance
The empty() method can create an empty Optional that doesn’t contain any value. A method that has nothing to return can return an empty Optional.
return Optional.empty();
Code language: Java (java)
Create an Optional with Value
Alternatively, we can use the of() method to create an Optional with a non-null value.
Optional.of(notNullObject);
Code language: Java (java)
This factory method throws NullPointerException when we pass a null value to it.
Create an Optional of a Nullable Value
Lastly, to create an Optional of a potentially null value, we can use the ofNullable() method.
Optional.ofNullable(notNullObject);
//or
Optional.ofNullable(nullObject);
Code language: Java (java)
How to use Optional?
We have understood why Optional instances are essential and how a method can create an Optional instance to return null or non-null values. This section will teach us to use an Optional instance to access the contained object safely.
Get the Contained Value – get()
The get() method in an Optional instance returns the contained value as is. That means the returned value can be null or non-null.
Optional<User> optUser = service.getUser(id);
User user = optUser.get();
Code language: Java (java)
Check if the Value Exists – isPresent(), isEmpty()
Use isPresent() to check if the Optional has a non-null value.
Optional<User> optUser = service.getUser(id);
if(optUser.isPresent()){
user = optUser.get();
}
Code language: Java (java)
Or, use isEmpty() to check if the Optional object is null.
Optional<User> optUser = service.getUser(id);
if(optUser.isEmpty()){
throw new UserNotFoundException("Not found; id: "+ id);
}
Code language: Java (java)
Do Something if the value Exists – ifPresent(), ifPresentOrElse()
ifPresent()
If the Optional object is non-null, the ifPresent() method passes the value to the given Consumer Function.
Optional<User> optUser = service.getUser(id);
optUser.ifPresent(processor::processUser);
Code language: Java (java)
The processUser() can be any method that takes a User object or a suitable type as a parameter.
ifPresentOrElse()
The ifPresentOrElse() takes a value-based consumer and an empty consumer. If the Optional object is non-null, it passes it to the value-based consumer. Else, it invokes the empty consumer.
Optional<User> optUser = service.getUser(id);
optUser.ifPresentOrElse(processor::processUser,
processor::processDefaultUser);
Code language: Java (java)
The processUser() must be of User or matching type, and the processDefaultUser() must be a no-argument method.
Throw Exception when Null – orElseThrow()
The Optional’s orElseThrow() method shows its true power. This method performs the null check and returns the object if it exists. Else, it throws NoSuchElementException.
Optional<User> optUser = service.getUser(id);
User user = optUser.orElseThrow();
Code language: Java (java)
Alternatively, we can supply a different exception to the orElseThrow() method. To do so, we can provide a supplier of a Throwable type.
Optional<User> optUser = service.getUser(id);
User user = optUser.orElseThrow(UserNotFoundException::new);
Code language: Java (java)
Note: the UserNotFoundException::new is an example of a Constructor Reference.
Get Default Value if Null – orElse()
Similar to the orElseThrow(), the orElse() method performs the null check internally. It returns the value if it is non-null. Else returns the provided default value.
User defaultUser = new User (....);
Optional<User> optUser = service.getUser(id);
User user = optUser.orElse(defaultUser);
Code language: Java (java)
Please make a note that this method calculates the default value eagerly. That means if the default value is an expression or a method call, it is always executed irrespective of the object being null or non-null.
Calculate Default Value if Null – orElseGet()
The orElseGet() method performs the null check internally and returns the value if present. Else it produces the default value by invoking the given supplier of the same type.
Optional<User> optUser = service.getUser(id);
User user = optUser.orElseGet(service::getDefaultUser());
Code language: Java (java)
The difference between orElse() and orElseGet() is that the latter accepts a supplier it invokes when the value is null.
Streaming an Optional Value – stream()
The stream() method of the Optional class returns a Java Stream of the value if it exists. Else, it returns an empty stream.
Optional<User> optUser = Optional.of(new User());
Stream<User> streamUser = optUser.stream();
Code language: Java (java)
Mapping the Optional to Other Type – map()
Java Optional’s map() function performs the null check, and if the object exists, it applies the given mapping function and returns a new Optional containing the mapping result. If the object is null, it returns an empty Optional instance.
Optional<User> optUser = Optional.of(new User());
Optional<Address> optAddress = optUser.map(User::getAddress);
Optional<String> optState = optAddress.map(Address::getState);
Code language: HTML, XML (xml)
The map() method is helpful when we want to traverse safely through nested objects without dealing with null values and NullPointerExceptions.
Summary
This thorough tutorial taught us all the details of the Java Optional class. Firstly, we started by understanding the null values and the reasons behind a NullPointerException with the help of examples. Then, we understood how null checks help make the code blocks robust with the side effect of introducing a focus shift.
Then we understood why Java Optional is so essential and how it can help us to avoid NullPointerExceptions while staying focused. We then looked at various ways of creating Optional instances and using the Java Optional API to safely deal with potentially null values, deriving defaults, conditional actions, and processing the Optional results.