Introduction
Before Java 8, methods had to throw an exception or return null
, with neither of which approaches were perfect. Optional, OptionalInt, OptionalLong, and OptionalDouble were introduced in Java 8 to represent values that might possibly be null
.
Optionals have two internal states, empty or present. An Optional is empty if the underlying reference is null
. An Optional is present when the underlying reference is not null.
Although there are many ways to use Optionals, chaining optionals usually provides for writing clear and concise code, especially when complex filtering is required.
Goals
At the end of this tutorial, you would have learned:
- How to use Optional methods
map()
andfilter()
.
Prerequisite Knowledge
- Java 8.
- Java functional concepts: Optional, method reference.
Tools Required
- A Java IDE with at least JDK 8 support.
Project Setup
To follow along with this tutorial, perform the steps below:
-
Create a new empty Java 8+ project.
-
Create a new package
com.example
. -
Create a new Java class called
Entry
. -
Create the
main()
method inside theEntry
class. -
Create two LocalDate constants inside the
Entry
class like below:private static final LocalDate GEN_ALPHA = LocalDate.ofYearDay(2010,1); //1 private static final LocalDate GEN_Z = LocalDate.ofYearDay(1997, 1); //2
-
Add a convenient method for checking if a LocalDate instance is considered a Gen Z.
private static boolean isGenZ(LocalDate d){ //7 return (d.isEqual(GEN_Z) || d.isAfter(GEN_Z)) && d.isBefore(GEN_ALPHA); }
Project Explanation
Our project is very simple to understand. The two LocalDate constants declared on lines 1 and 2 represent the beginning date for 2 demographics cohorts: Alpha and Gen Z. The convenient method declared on line 7 contains logic to check whether a date(birthday) can be classified as Gen Z.
How to NOT use an Optional
The method Optional#get
should never be used directly without checking whether the Optional is present(or NOT empty). If Optional#get
is called directly, the program will throw NoSuchElementException at runtime.
Add this method inside the Entry class.
private static void noCheckOpt(){ //8
Optional<LocalDate> opt = Optional.empty();
LocalDate ogDate = opt.get(); //9
if(isGenZ(ogDate)){
LocalDate modifiedDate = ogDate
.plusYears(1)
.plusMonths(5)
.plusDays(10);
System.out.println(modifiedDate);
}
}
The method above tries to get the underlying LocalDate object from an Optional, and then tries to create a new date in the if block only if the ogDate
is a Gen Z date.
The code snippet deliberately instantiated an empty Optional to simulate a situation where an Optional object received from an API can be empty. Professional code should never be written like this.
After calling the method above from main()
, we can see that the code throws NoSuchElementException as expected.
Basic Optional value check
To avoid the program throwing NoSuchElementException, the most basic thing that a developer can do is to at least check the Optional object whether it contains a value with Optional#isEmpty
or Optional#isPresent
.
The method below adds the presence check.
private static void checkOpt(){ //10
Optional<LocalDate> opt = Optional.empty(); //11
if(opt.isPresent()){ //12
LocalDate ogDate = opt.get(); //13
if(isGenZ(ogDate)){ //14
LocalDate modifiedDate = ogDate //15
.plusYears(1)
.plusMonths(5)
.plusDays(10);
System.out.println(modifiedDate);
}
}
}
If we comment out the call to the previous method, and then call this method, our code should not be throwing any exception anymore. It finds that the Optional opt is empty at line 12, so it stops executing the rest of the method.
But this method has a problem. It is very verbose. By having to check whether the Optional is empty AND whether the underlying LocalDate is Gen Z, we now have nested if
blocks. We also added two variable declarations.
Optional Chaining
To improve readability, we can use builtin Optional methods Optional#filter
and Optional#map
like the code snippet below.
private static void optChain(){ //16
Optional.<LocalDate>empty() //17
.filter(Entry::isGenZ) //18
.map(d -> d.plusYears(1).plusMonths(5).plusDays(10)) //19
.ifPresent(System.out::println); //20
}
To understand how this method works, let us review what the previous method was doing, from start to finish:
- Check whether the Optional is empty.
- Check whether the underlying LocalDate object is a Gen Z.
- If it is Gen Z, transform it.
- Prints out the LocalDate.
The filter()
and map()
methods automatically perform the Optional emptiness check for us, and return an empty Optional object downstream, so we do not have to write the checks ourselves. It is only at the last step that we have to call ifPresent()
, and ifPresent()
is certainly more readable than a manual if
block, therefore it increases readability.
Another added benefit of using filter()
, map()
, and ifPresent()
is that we can pass a lambda or a method reference into them, increasing readability even more.
Solution Code
package com.example;
import java.time.LocalDate;
import java.util.Optional;
public class Entry {
private static final LocalDate GEN_ALPHA = LocalDate.ofYearDay(2010,1); //1
private static final LocalDate GEN_Z = LocalDate.ofYearDay(1997, 1); //2
public static void main(String... args){ //3
//noCheckOpt(); //4
//checkOpt(); //5
//optChain(); //6
}
private static boolean isGenZ(LocalDate d){ //7
return (d.isEqual(GEN_Z) || d.isAfter(GEN_Z)) && d.isBefore(GEN_ALPHA);
}
private static void noCheckOpt(){ //8
Optional<LocalDate> opt = Optional.empty();
LocalDate ogDate = opt.get(); //9
if(isGenZ(ogDate)){
LocalDate modifiedDate = ogDate
.plusYears(1)
.plusMonths(5)
.plusDays(10);
System.out.println(modifiedDate);
}
}
private static void checkOpt(){ //10
Optional<LocalDate> opt = Optional.empty(); //11
if(opt.isPresent()){ //12
LocalDate ogDate = opt.get(); //13
if(isGenZ(ogDate)){ //14
LocalDate modifiedDate = ogDate //15
.plusYears(1)
.plusMonths(5)
.plusDays(10);
System.out.println(modifiedDate);
}
}
}
private static void optChain(){ //16
Optional.<LocalDate>empty() //17
.filter(Entry::isGenZ) //18
.map(d -> d.plusYears(1).plusMonths(5).plusDays(10)) //19
.ifPresent(System.out::println); //20
}
}
Summary
Manually checking for Optional presence works, but there are a lot of convenient methods that we can use together with lambdas and method reference sugar syntax. Surely it does cost some extra time to look them up, but the benefits to readability would be worth it.
The full project code can be found here https://github.com/dmitrilc/DaniWebJavaOptionalChaining/tree/master