JB Header

Java 8 Optional Basics Tutorial with Examples

Introduction This tutorial covers the java.util.Optional<T> class introduced in Java 8, which was designed to address the age-old problem of NullPointerExceptions. We will start by examining the inherent flaws of the null reference in Java and how it leads to logic buried under layers of null checks. We will then go through the methods of Optional API for creating Optional instances, consuming their values, providing defaults, and transforming them in a fluent, functional style using map(), flatMap() and filter() methods. We will end with best practices on when to use, and when not to use, Optional. Null and NullPointerException: The Billion Dollar Mistake Sir Tony Hoare, who introduced null references in 1965, later called them his "billion-dollar mistake". Every Java programmer has seen the kind of issues the value null and the associated NullPointerException bring with them. The value null represents the absence of a value, which in itself is useful. However, the fact that at runtime any reference can be null presents two unique challenges -
  1. Ubiquity of NPEs: A NullPointerException can be thrown by any object reference at runtime. This leads to unstable production systems where unhandled edge cases crash the application simply because an attribute was accessed without a null check.
  2. Lack of Type Safety: There is no way to design a method signature that explicitly informs the programmer whether a return value could be null. This forces developers into a highly defensive coding style, checking every object before use.
Java 8's Optional class as the solution Java 8's Optional has been designed to solve these challenges by indicating, by its very definition, that a value may be absent. An Optional instance is essentially a container that either holds a non-null value or is empty.
Definition of Optional class in java.util package is -
public final class Optional<T> extends Object
By returning an Optional<T>, an API author communicates to the consumer - "I might return a value of type T, or I might return nothing. You must handle both cases." The possible absence of a value thus becomes part of the method's signature, and the compiler makes sure the caller deals with it.
Comparison: Defensive Null-Checks vs. Optional's Fluent API To see the difference in readability, let's look at a nested object hierarchy where we want to retrieve a User's Country ISO Code. Each of User, Address and Country could be null.
Java 7 style: Nested Null Checks (The Pyramid of Doom)
//Traditional approach - hard to read and maintain
public String getIsoCode(User user) {
 if (user != null) {
  Address address = user.getAddress();
  if (address != null) {
   Country country = address.getCountry();
   if (country != null) {
    return country.getIsocode();
   }
  }
 }
 return "UNKNOWN";
}
Now, compare this with the Java 8 approach using Optional. The focus moves from checking for failure to describing the transformation -
Java 8 style: Using Optional for Clean Code
//Modern approach - expressive and safe
public String getIsoCode(User user) {
 return Optional.ofNullable(user)
                .map(User::getAddress)
                .map(Address::getCountry)
                .map(Country::getIsocode)
                .orElse("UNKNOWN");
}
Each map() call is applied only if the previous step produced a value. As soon as any step returns null, the chain short-circuits to an empty Optional, and orElse() supplies the fallback value "UNKNOWN". We will look at each of these methods in detail in the following sections. Creating Optional instances Optional has no public constructor. Instead, it provides 3 static factory methods to create its instances -
  • Optional.empty() - returns an empty Optional which holds no value.
  • Optional.of(T value) - returns an Optional holding the given non-null value. It throws a NullPointerException if the value passed is null.
  • Optional.ofNullable(T value) - returns an Optional holding the value if it is non-null, otherwise returns an empty Optional.
Java 8 code showing creation of Optional instances
//OptionalCreationExample.java
import java.util.Optional;
public class OptionalCreationExample {
 public static void main(String args[]) {
  //An empty Optional
  Optional<String> emptyOptional = Optional.empty();
  System.out.println("emptyOptional -> " + emptyOptional);

  //An Optional holding a non-null value
  Optional<String> nameOptional = Optional.of("Tom Jones");
  System.out.println("nameOptional -> " + nameOptional);

  //An Optional from a value which may be null
  String nullName = null;
  Optional<String> nullableOptional = Optional.ofNullable(nullName);
  System.out.println("nullableOptional -> " + nullableOptional);

  //Optional.of() does not accept null
  try {
   Optional<String> invalidOptional = Optional.of(nullName);
  } catch (NullPointerException npe) {
   System.out.println("Optional.of(null) threw a NullPointerException");
  }
 }
}
 OUTPUT of the above code
emptyOptional -> Optional.empty
nameOptional -> Optional[Tom Jones]
nullableOptional -> Optional.empty
Optional.of(null) threw a NullPointerException
Explanation of the code
  • Optional.empty() creates an empty Optional which, when printed, shows as Optional.empty.
  • Optional.of() wraps the String "Tom Jones". Printing it shows the wrapped value as Optional[Tom Jones].
  • Optional.ofNullable() is passed a null reference, so it returns an empty Optional instead of throwing an exception.
  • Passing the same null reference to Optional.of() throws a NullPointerException. Use Optional.of() only when you are certain the value is non-null, and Optional.ofNullable() otherwise.
Checking and consuming the value of an Optional Once we have an Optional, we need ways to check whether it holds a value, and to use that value -
  • boolean isPresent() - returns true if a value is present, else false.
  • T get() - returns the value if present, else throws a NoSuchElementException.
  • void ifPresent(Consumer<? super T> consumer) - executes the given ConsumerClick to read tutorial on Consumer Functional Interface with the value if a value is present, and does nothing otherwise.
The examples in this and the following sections use the below Employee and Department classes. Note that an employee may not have been assigned a department yet, which Employee.getDepartment() communicates by returning an Optional<Department>.
Employee and Department classes used in the examples
//Employee.java
import java.util.Optional;
public class Employee {
 private String name;
 private Integer age;
 private Department department;
 public Employee(String name, Integer age, Department department) {
  this.name = name;
  this.age = age;
  this.department = department;
 }
 public String getName() {
  return name;
 }
 public Integer getAge() {
  return age;
 }
 //department is optional - an employee may not be assigned one yet
 public Optional<Department> getDepartment() {
  return Optional.ofNullable(department);
 }
}
//Department.java
public class Department {
 private String name;
 public Department(String name) {
  this.name = name;
 }
 public String getName() {
  return name;
 }
 @Override
 public String toString() {
  return "Department[" + name + "]";
 }
}
Java 8 code showing isPresent(), get() and ifPresent() methods
//OptionalConsumptionExample.java
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class OptionalConsumptionExample {
 static List<Employee> employeeList =
     Arrays.asList(new Employee("Tom Jones", 45, new Department("Sales")),
       new Employee("Harry Major", 25, null));

 //Return type tells the caller that an employee may not be found
 static Optional<Employee> findEmployee(String name) {
  return employeeList.stream()
                     .filter(emp -> emp.getName().equals(name))
                     .findFirst();
 }

 public static void main(String args[]) {
  //isPresent() & get()
  Optional<Employee> tomOptional = findEmployee("Tom Jones");
  if (tomOptional.isPresent()) {
   System.out.println("isPresent() & get() -> " + tomOptional.get().getName());
  }

  //ifPresent() executes the Consumer only if a value is present
  findEmployee("Tom Jones")
     .ifPresent(emp -> System.out.println("ifPresent() -> " + emp.getName()));
  findEmployee("Nancy Smith")
     .ifPresent(emp -> System.out.println("This line is never printed"));

  System.out.println("Is Nancy Smith present? " + findEmployee("Nancy Smith").isPresent());
 }
}
 OUTPUT of the above code
isPresent() & get() -> Tom Jones
ifPresent() -> Tom Jones
Is Nancy Smith present? false
Explanation of the code
  • findEmployee() searches the employee list using the findFirst() method of Streams APIClick to read tutorial on findFirst() and findAny() methods, which itself returns an Optional<Employee>. The return type makes it clear to every caller that an employee may not be found.
  • The first block checks isPresent() before calling get(). This works, but is not much better than a null check.
  • ifPresent() is the more idiomatic alternative. The lambda passed to it is executed for "Tom Jones", while for "Nancy Smith", who is not in the list, it is simply skipped. Hence "This line is never printed" does not appear in the output.
Default values and exceptions: orElse(), orElseGet(), orElseThrow() Rather than checking for presence, it is usually cleaner to tell the Optional what to do when it is empty -
  • T orElse(T other) - returns the value if present, otherwise returns other.
  • T orElseGet(Supplier<? extends T> other) - returns the value if present, otherwise invokes the given SupplierClick to read tutorial on Supplier Functional Interface and returns its result.
  • <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) - returns the value if present, otherwise throws the exception created by the given Supplier.
Java 8 code showing orElse(), orElseGet() and orElseThrow() methods
//OptionalDefaultsExample.java
import java.util.Optional;
public class OptionalDefaultsExample {
 public static void main(String args[]) {
  Optional<String> emptyName = Optional.empty();
  Optional<String> tomName = Optional.of("Tom Jones");

  //Fallback values when the Optional is empty
  System.out.println("orElse() -> " + emptyName.orElse("Unknown"));
  System.out.println("orElseGet() -> " + emptyName.orElseGet(() -> "Generated Name"));

  //orElse() vs orElseGet() when a value IS present
  System.out.println("orElse() on Tom -> " + tomName.orElse(getDefaultName()));
  System.out.println("orElseGet() on Tom -> " + tomName.orElseGet(OptionalDefaultsExample::getDefaultName));

  //orElseThrow() throws the supplied exception if empty
  try {
   emptyName.orElseThrow(() -> new IllegalArgumentException("Employee name not found"));
  } catch (IllegalArgumentException iae) {
   System.out.println("orElseThrow() -> " + iae.getMessage());
  }
 }

 static String getDefaultName() {
  System.out.println("  ..getDefaultName() invoked");
  return "Default Name";
 }
}
 OUTPUT of the above code
orElse() -> Unknown
orElseGet() -> Generated Name
  ..getDefaultName() invoked
orElse() on Tom -> Tom Jones
orElseGet() on Tom -> Tom Jones
orElseThrow() -> Employee name not found
Explanation of the code
  • On the empty emptyName Optional, orElse() returns the fallback value "Unknown", while orElseGet() invokes its Supplier lambda to produce "Generated Name".
  • The important difference between orElse() and orElseGet() shows up when a value IS present. The argument to orElse() is an ordinary method argument, so getDefaultName() is evaluated before orElse() is even called - even though Optional tomName holds a value and the default is discarded. This is why "..getDefaultName() invoked" is printed. orElseGet(), on the other hand, invokes its Supplier only when the Optional is empty, so getDefaultName() is not called the second time.
  • Hence, use orElse() for constants or already-computed values, and orElseGet() when computing the default is expensive or has side effects, such as a database or network call.
  • orElseThrow() throws the IllegalArgumentException created by its Supplier, as emptyName is empty. This is the right choice when a missing value is truly an error condition.
Transforming Optionals: map(), flatMap() and filter() These three methods let us work on the value inside an Optional without first taking it out. If the Optional is empty, they simply return an empty Optional, which is what makes the chained style shown earlier possible.
  • <U> Optional<U> map(Function<? super T, ? extends U> mapper) - applies the given FunctionClick to read tutorial on Function Interface to the value if present, and returns an Optional describing the result. If the function returns null, an empty Optional is returned.
  • <U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) - similar to map(), but for functions which themselves return an Optional. It returns that Optional directly instead of wrapping it in another Optional.
  • Optional<T> filter(Predicate<? super T> predicate) - returns the same Optional if a value is present and it matches the given PredicateClick to read tutorial on Predicate Functional Interface, otherwise returns an empty Optional.
Java 8 code showing map(), flatMap() and filter() methods
//OptionalTransformationExample.java
import java.util.Optional;
public class OptionalTransformationExample {
 public static void main(String args[]) {
  Employee tom = new Employee("Tom Jones", 45, new Department("Sales"));
  Employee harry = new Employee("Harry Major", 25, null);

  //map() - transform the value inside the Optional
  Optional<String> upperName = Optional.of(tom)
                                       .map(Employee::getName)
                                       .map(String::toUpperCase);
  System.out.println("map() -> " + upperName.orElse("Unknown"));

  //map() with a getter that itself returns an Optional gives a nested Optional
  Optional<Optional<Department>> nestedDept = Optional.of(tom).map(Employee::getDepartment);
  System.out.println("map() with Optional getter -> " + nestedDept);

  //flatMap() - flattens Optional<Optional<Department>> into Optional<Department>
  String tomDept = Optional.of(tom)
                           .flatMap(Employee::getDepartment)
                           .map(Department::getName)
                           .orElse("No Department");
  String harryDept = Optional.of(harry)
                             .flatMap(Employee::getDepartment)
                             .map(Department::getName)
                             .orElse("No Department");
  System.out.println("flatMap() -> Tom's department: " + tomDept);
  System.out.println("flatMap() -> Harry's department: " + harryDept);

  //filter() - keep the value only if it matches the Predicate
  Optional.of(tom)
          .filter(emp -> emp.getAge() > 40)
          .ifPresent(emp -> System.out.println("filter() -> " + emp.getName() + " is above 40"));
  boolean isHarryAbove40 = Optional.of(harry)
                                   .filter(emp -> emp.getAge() > 40)
                                   .isPresent();
  System.out.println("filter() -> Is Harry Major above 40? " + isHarryAbove40);
 }
}
 OUTPUT of the above code
map() -> TOM JONES
map() with Optional getter -> Optional[Optional[Department[Sales]]]
flatMap() -> Tom's department: Sales
flatMap() -> Harry's department: No Department
filter() -> Tom Jones is above 40
filter() -> Is Harry Major above 40? false
Explanation of the code
  • map(): Two map() calls are chained using method referencesClick to read tutorial on Method References. The first converts Optional<Employee> to Optional<String> holding the name, and the second converts the name to uppercase.
  • Why flatMap() is needed: Employee.getDepartment() already returns an Optional<Department>. Passing it to map() wraps that Optional into another one, giving the awkward nested type Optional<Optional<Department>>, as seen in the output.
  • flatMap() flattens this into a simple Optional<Department>, on which map(Department::getName) is then applied. For Tom, the chain yields "Sales". Harry has no department, so flatMap() returns an empty Optional, the subsequent map() is skipped, and orElse() returns "No Department". No null checks were needed anywhere.
  • filter(): Tom's age of 45 satisfies the predicate emp -> emp.getAge() > 40, so the Optional remains non-empty and ifPresent() prints the message. Harry's age of 25 does not, so filter() returns an empty Optional and isPresent() returns false.
Note - map(), flatMap() and filter() of Optional work the same way as their namesakes in the Streams API. You can think of an Optional as a stream which holds at most one element. To learn more, read Mapping with Streams using map and flatMap methodsClick to read how Mapping with Java 8 Streams works. Best practices for using Optional Optional is a powerful tool, but it was designed for a specific purpose - as a return type for methods which may not have a result to return. Following are some guidelines for using it well -
  • Never return null from a method whose return type is Optional. Return Optional.empty() instead. A null Optional defeats the entire purpose of using one.
  • Avoid calling get() without checking for a value. An unchecked get() simply replaces a NullPointerException with a NoSuchElementException. Prefer orElse(), orElseGet(), orElseThrow() or ifPresent().
  • Do not use Optional for fields or method parameters. Optional is not Serializable, and an Optional parameter forces every caller to wrap its arguments. Use overloaded methods, or plain null checks internally, instead.
  • Do not wrap collections in Optional. Return an empty collection instead of Optional<List<T>>, since an empty list already represents "no results".
  • Use the primitive variants for primitive values. OptionalInt, OptionalLong and OptionalDouble avoid the boxing overhead of Optional<Integer>, Optional<Long> and Optional<Double>.
  • Prefer orElseGet() over orElse() when the default value is expensive to compute, as explained earlier.
Note - later Java versions have added more methods to Optional, such as ifPresentOrElse(), or() and stream() in Java 9, the no-argument orElseThrow() in Java 10, and isEmpty() in Java 11. This tutorial covers the Java 8 API, which forms the basis for all of these. Summary In this tutorial we saw how the Optional class acts as a powerful alternative to the null reference. We looked at how to create Optional instances using empty(), of() and ofNullable(), how to consume their values using isPresent(), get() and ifPresent(), and how to provide defaults using orElse(), orElseGet() and orElseThrow(). We then saw how map(), flatMap() and filter() allow writing null-safe code in a fluent style, and ended with best practices for using Optional.

Java 8 Optional Basics Tutorial with Examples - JavaBrahman