Java Functional - Optional Chaining

dimitrilc
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:

  1. How to use Optional methods map() and filter().
Prerequisite Knowledge
  1. Java 8.
  2. Java functional concepts: Optional, method reference.
Tools Required
  1. A Java IDE with at least JDK 8 support.
Project Setup

To follow along with this tutorial, perform the steps below:

  1. Create a new empty Java 8+ project.

  2. Create a new package com.example.

  3. Create a new Java class called Entry.

  4. Create the main() method inside the Entry class.

  5. 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
  6. 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:

  1. Check whether the Optional is empty.
  2. Check whether the underlying LocalDate object is a Gen Z.
  3. If it is Gen Z, transform it.
  4. 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

56 Views
About the Author

My name is Dimitri Nguyen. I am a Java Developer specializing in backend development on the Java/Spring/MySQL stack.

I can also work on the frontend using Angular/Typescript/JS/HTML/CSS and native Android with Kotlin.

JamesCherrill 4,426 Most Valuable Poster Moderator Featured Poster

The filter() and map() methods automatically perform the Optional emptiness check for us, and return an empty Optional object downstream

Thank you so much for this little gem.

I was always disappointed with Optional, too much overhead and verbiage compared to the ?. syntax of some other languages. But I hadn't realised the way they work with streams, and that changes everything.

Great thread.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts learning and sharing knowledge.