Photo by Joshua Wilking on Unsplash

One of the most common exceptions in Java is the NullPointerException – simply put this means that somewhere in your program a method is being called on a null object.

Hotel hotel = null;
hotel.getAmenities(); // 💥 NullPointerException

This can be very frustrating and leads to cluttering the code with overly defensive null checks. This frustration has lead to some languages, like JVM-compatible Kotlin, to be created to solve this issue statically by outlawing null unless explicitly allowed in the types.

How do we reduce these NullPointerExceptions in Java, where we don't have language-level support like Kotlin? We can use a wrapper class introduced in Java 8, called Optional.

Optional

Very simply put, Optional is a thin wrapper around another object. The optional instance is either present (and non-null), or empty.

Optional<Hotel> hotel = Optional.of(sheraton);
if (hotel.isPresent()) {
  hotel.get().getAmenities();
}

Optional<Hotel> emptyHotel = Optional.empty();
if (hotel.isPresent()) {
  // never runs
}

However, this doesn't give us too many benefits since it is just as safe as checking against null. The power with Optional is the many utility methods that is exposes to us:

Optional methods

You can break down the Optional methods into a few overarching categories – creating an Optional, checking if the Optional is present, "unboxing" the Optional, and interacting/transforming the Optional without unboxing it.

Creating an Optional

There are three ways to create an Optional object – Optional.empty(), Optional.of(value), and Optional.ofNullable(value).

// If the object is always empty,
// use the empty() static method
Optional<Hotel> alwaysEmpty = Optional.empty();


// If the object is always present,
// use the of() static method
Optional<Hotel> alwaysPresent = Optional.of(hotel);
// Note: If null is passed to Optional.of(),
// a NullPointerException will be raised!


// If the object is sometimes null, sometimes not:
Optional<Hotel> sometimesNull = Optional.ofNullable(hotel);
// If the argument is null,
// it will become an Optional.empty() object

Typically if you're converting a value to optional, use Optional.ofNullable. If you're returning a known value in a method, use Optional.of or Optional.empty.

Check if the Optional has a value

There are two methods that return whether the optional variable has a value, isEmpty() and isPresent(). They both return a boolean and are opposites of each other.

if (optionalHotel.isPresent()) {
  // hotel has a non-null value in it
}

if (optionalHotel.isEmpty()) {
  // hotel does not have a value
}

"Unboxing" the Optional

There are a few ways to "unbox" the value – that is, convert between the Optional type and the underlying type.

.get()

The first and simplest is get. This returns the value inside the optional if it exists, or throws an exception if it's missing. Warning, this (intentionally) leads to runtime exceptions, so this might not be what you want!

optionalHotel.get().getName();
// If hotel is present, there are no errors.
// If hotel is empty, a runtime exception is raised!

.orElse(defaultValue)

A much safer alternative to get is orElse(), which accepts a default value to return if the value is missing. This then guarantees that the unboxed value is present, and not null.

Hotel hotel = optionalHotel.orElse(defaultHotel);
// hotel is either optionalHotel.get(), or defaultHotel.
// hotel is now guaranteed to be non-null.

.orElseGet(() -> defaultValue)

orElseGet functions exactly the same as orElse, but takes a function to run to get the default value instead of the value itself. This is useful to avoid running the default function unless it's needed.

optionalHotelId.orElseGet(() -> UUID.randomUUID())

// or, the simplified method reference syntax:
UUID id = optionalHotelId.orElseGet(UUID::randomUUID);
// note no parenthesis after randomUUID

.orElseThrow(() -> new CustomException())

get() throws an exception by default if the value is missing. However, it may be useful to throw an exception of a particular type.

Hotel hotel = optionalHotel.orElseThrow(
  () -> new MissingHotelException()
);

Interacting with the Optional without unboxing

Those unboxing methods are useful, but as soon as we unbox the value to its underlying type we lose null safety. Ideally we can keep these values wrapped in an Optional until we get the final value. Optional gives us a few methods to help:

.ifPresent(item -> use(item))

This method will call the lambda given with the unboxed value if it exists, or else skip the method. This is equivalent to calling if (item.isPresent()) first, but more concise.

optionalHotel.ifPresent(h -> {
  System.out.println("Hotel name is: " + h.getName());
});

.map(item -> transform(item))

Map lets you transform the value to a new value. This works exactly like .map in other languages, except it only maps if the value is present. This is chainable!

Optional<String> amenityName = optionalHotel
    .map(h -> h.getAmenities())
    .map(a -> a[0])
    .map(a -> a.getName());

.filter(item -> conditional(item))

Filter also works the same as in other languages. If the given lambda returns true, then the filter expression returns an Optional of the original value. If it returns false, then it will be an empty Optional. Once again, this can be chained!

String roomsAvailableDisplay = optionalHotel
    .map(h -> h.getAvailableRooms())
    .filter(rooms -> rooms > 0)
    .map(rooms -> "%d rooms available".format(rooms))
    .orElse("No rooms available");

Alternative way to pass Functions

One note for all the Optional methods that accept functions, like map, isPresent, filter, etc – those can also accept Method References. Method references look like double colons (::). This can reduce some of the boilerplate and increase readability.

hotel.map(Hotel::getId)            // instance method on type
     .orElseGet(UUID::randomUUID); // static method on class

Set<String> someSet = new HashSet<String>();
hotel.map(Hotel::getName)      // instance method on type
     .filter(String::isEmpty)  // instance method on type
     .ifPresent(someSet::add); // instance method on object

Putting it all together

In summary, Java's Optional class gives us a lot of tools to help avoid unnecessary NullPointerExceptions in our code. We can push some of the null safety into the type system itself without needing to use a different language like Kotlin. In some cases, this can make the code more concise, too!

String preview = optionalHotel.map(Hotel::getAmenities)
    .filter(amenities -> amenities.size() > 0)
    .map(amenities -> amenities.get(0))
    .map(Amenity::name)
    .map(name -> name.substr(0, 10) + "...")
    .orElse("Data not found");

This post only detailed a few of the most common and useful Optional methods. For the full list, the Java docs are a useful reference.