Remove null check, use the Optional
Pawel Pawlak
Posted on July 21, 2022
Defensive programming seems for years like the only way to go. Now, after years of struggling with the null check, we can benefit from the Optional class that was introduced in the Java 8.
I am going to show you just a simple, teste of Optional, what can be done with it. You can take it much more further.
In this example we are going to simply display the number of movies that given user has seen/rented in the current month.
Here are the basic classes:
public final class RentHistory {
private Integer totalInThisMonth;
private Integer totalFromTheBeginning;
public RentHistory() {
}
public RentHistory(Integer totalInThisMonth, Integer totalFromTheBeginning) {
this.totalInThisMonth = totalInThisMonth;
this.totalFromTheBeginning = totalFromTheBeginning;
}
public RentHistory(Integer totalInThisMonth) {
this.totalInThisMonth = totalInThisMonth;
}
public Integer getTotalInCurrentMonth() {
return totalInThisMonth;
}
public Integer getTotalFromTheBeginning() {
return totalFromTheBeginning;
}
}
public final class User {
private RentHistory rentHistory;
public User() {
}
public User(RentHistory rentHistory) {
this.rentHistory = rentHistory;
}
public RentHistory getRentHistory() {
return rentHistory;
}
}
Now what we want is to display to the user the number of movies he has rented in this month. So the code would look like that:
public final class Test {
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
System.out.println("Number of movies user has rented in this month is: " + getTotal(user.getRentHistory()));
}
private Integer getTotal(RentHistory history) {
return history.getTotalInCurrentMonth();
}
public static void main(String[] args) {
Test test = new Test();
test.displayNumberOfMoviesRentInCurrentMonth(new User(new RentHistory(4)));
}
}
What user gets is this: Number of movies user has rented is this month is: 4
Now what happens if we create Rent history with the default constructor, so without the number of movies he has seen in the current month:
public static void main(String[] args) {
Test test = new Test();
test.displayNumberOfMoviesRentInCurrentMonth(new User(new RentHistory()));
}
We got very pretty text that says: Number of movies user has rented is this month is: null
For sure this is not that we want our user to see in the UI. What can be done about it, maybe this:
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
System.out.println("Number of movies user has rented in this month is: " + getTotal(user.getRentHistory()));
}
private Integer getTotal(RentHistory history) {
return history.getTotalInCurrentMonth() == null ? 0: history.getTotalInCurrentMonth();
}
This is the standard defensive approach we used to avoid getting null – add condition, check for the null, and perform action. Now with the use of Optional we can do this:
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
Optional<Integer> total = getTotal(user.getRentHistory());
if (total.isPresent()) {
System.out.println("Number of movies user has rented in this month is: " + total.get());
} else {
System.out.println("User has not rented any movie in the current month");
}
}
private Optional<Integer> getTotal(RentHistory history) {
return Optional.ofNullable(history.getTotalInCurrentMonth());
}
You see what it gave us…. yes, exactly nothing! You should never change the use of regular condition to the use of the isPresent! This is not the way Optional should be used for sure. What should have been done is this:
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
System.out.println(getTotal(user.getRentHistory())
.map(total -> "Number of movies user has rented in this month is: " + total)
.orElse("User has not rented any movie in the current month"));
}
private Optional<Integer> getTotal(RentHistory history) {
return Optional.ofNullable(history.getTotalInCurrentMonth());
}
Remember, when you are dealing with nulls an Optional.map is something you should give a try.
Now lets talk about the other situation. What will happen if we are going to pass a user that does not have a RentHistory at all, lets check.
public static void main(String[] args) {
Test test = new Test();
test.displayNumberOfMoviesRentInCurrentMonth(new User());
}
What we are getting now is this: Exception in thread “main” java.lang.NullPointerException at getTotal(Test.java:15)
What we can do now is something like this:
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
System.out.println(getTotal(user.getRentHistory())
.map(total -> "Number of movies user has rented in this month is: " + total)
.orElse("User has not rented any movie in the current month"));
}
private Optional<Integer> getTotal(RentHistory history) {
if (history == null){
return Optional.empty();
}
return Optional.ofNullable(history.getTotalInCurrentMonth());
}
Is this is the final solution that we really want, adding a null check, again, defensive programming. Nope, we should not leave it like that, we should do some refactor! First of all we are going to change the User class to this:
public final class User {
private RentHistory rentHistory;
public User() {
}
public User(RentHistory rentHistory) {
this.rentHistory = rentHistory;
}
public Optional<RentHistory> getRentHistory() {
return Optional.ofNullable(rentHistory);
}
}
Instead of returning entities we are going to return an optional of it! Now changes in the Test class.
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
System.out.println(getTotal(user.getRentHistory())
.map(total -> "Number of movies user rent in this month is: " + total)
.orElse("User has not rent any movie in the current month"));
}
private Optional<Integer> getTotal(Optional<RentHistory> history) {
if (!history.isPresent()){
return Optional.empty();
}
return Optional.ofNullable(history.get().getTotalInCurrentMonth());
}
Optional was not designed to be passed as argument! This is for sure not the way to go. We are going to refactor! InteliJ already tells us that I can make a change and introduce map method, lest do this!
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
System.out.println(getTotal(user.getRentHistory())
.map(total -> "Number of movies user has rented in this month is: " + total)
.orElse("User has not rented any movie in the current month"));
}
private Optional<Integer> getTotal(Optional<RentHistory> history) {
return history.map(RentHistory::getTotalInCurrentMonth);
}
Looks a bit better, but are still passing the Optional as an argument, that is bad! What we are going to do now, is to change the way around. First we are going to use the user, and then if thr user has his RentHistory we are going to use it in the getTotal method. So now code would look like that:
public final class Test {
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
System.out.println(user.getRentHistory()
.map(this::getTotal)
.map(total -> "Number of movies user has rented in this month is: " + total)
.orElse("User has not rented any movie in the current month"));
}
private Optional<Integer> getTotal(RentHistory history) {
return Optional.ofNullable(history.getTotalInCurrentMonth());
}
public static void main(String[] args) {
Test test = new Test();
test.displayNumberOfMoviesRentInCurrentMonth(new User());
}
}
The above solution is almost perfect! It works well for incorrect input, but when everything is fine like that…
public static void main(String[] args) {
Test test = new Test();
test.displayNumberOfMoviesRentInCurrentMonth(new User(new RentHistory(6)));
}
…it gives the response: _Number of movies user rent is this month is: Optional[6]
_
It is like that because we are wrapping the Optional with the Optional. What has to be done is the use of the flatMap:
public final class Test {
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
System.out.println(user.getRentHistory()
.flatMap(this::getTotal)
.map(total -> "Number of movies user has rented in this month is: " + total)
.orElse("User has not rented any movie in the current month"));
}
private Optional<Integer> getTotal(RentHistory history) {
return Optional.ofNullable(history.getTotalInCurrentMonth());
}
public static void main(String[] args) {
Test test = new Test();
test.displayNumberOfMoviesRentInCurrentMonth(new User(new RentHistory(6)));
}
}
This would be the final approach to this matter. Remember, always when dealing with nulls, try to use Optional. It may be at the beginning not that intuitive to use, but when time comes, you will get more and more used to this kind of approach.
This is of course just a simple example, simple test case that shows how powerful Optional is. I am not saying that you must always use the Optional in every cases, but for sure you should give it a try!
You can imagine the use of that for something much more sophisticated and complex, and not just simple text displaying.
This hole post is based on one of the talk that was given by Mr. Victor Rentea. I highly recommend watch this man in action!
Here is the git repository for this simple example.
Posted on July 21, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.