How to use Java Optional to handle NullPointerException?

How to use Java Optional to handle NullPointerException?

We all know the pain of dealing with null references and the potential for NullPointerExceptions. That’s where Optional comes in, it’s a container object that may or may not contain a non-null value, and provides a variety of utility methods to check for the presence or absence of a value.

Tony Hoare, the inventor of the null reference, famously referred to it as his “billion-dollar mistake” due to the countless errors and system crashes that it has caused over the years. But with the introduction of Optional, we now have a way to effectively handle null references and prevent NullPointerExceptions.

In this article, we’ll discuss the ways to check for null references and how Optional can help us avoid NPEs. Whether you’re a seasoned Java developer or just starting out, this article will provide valuable insights into the best practices for handling null references in your code.

So let’s dive in!

Problems with Null Reference

Imagine you are building a Library management system. You are using java to fetch book details with the title of the book.

@Repository
public class BookRepository extends JpaRepository<Book, Long> {
    Book findByTitle(String title);
}

public class SearchService {
    BookRepository bookRepository;

    Book searchByTitle(String title) {
        return bookRepository.findByTitle(title);
    }
}

Here BookRepository is connecting to our database and SearchService deals with API requests. The findByTitle in BookRepository will return a Book object only when there is a title present in our database.

So, what happens when there is no book with the same title?

The function will return a null reference.

Since, this function can be used for other features like borrowing, purchasing, reading, and others. These functionalities can break if a null reference is not handled properly and can lead to NullPointerException.

Null Check Before Optional

There are other alternatives to null reference checks that we used before Optional and still use in some places.

  • If-else conditions: Checking if the function returns null before performing the operation on it.
public class SearchService {
    BookRepository bookRepository;

    Book searchByTitle(String title) {
        Book book = bookRepository.findByTitle(title);
        if (book == null){
          return new IllegalArgumentException("Title is invalid or not present");
        } else {
          return book;
        }
    }
}
  • Objects class: This class provides various static utility methods for operating on objects or checking certain conditions before operation.
public class SearchService {
    BookRepository bookRepository;

    Book searchByTitle(String title) {
        Book book = bookRepository.findByTitle(title);
        if (Objects.isNull(book)){
          return new IllegalArgumentException("Title is invalid or not present");
        } else {
          return book;
        }
    }
}

Why do we need Optional

While the use of if-else and Optional can solve the issue of null checks, it is not a scalable solution. As the system grows and new features are added, it becomes increasingly difficult to keep track of all the null checks and handle them properly.

For example, if some other feature uses bookRepository.findByTitle(title) or if someone wants to find the name of the author associated with the book using book.getAuthor().getName(), this could lead to a cascading series of null checks, making the codebase harder to maintain and understand.

Java Optional

Java Optional is a container object used to represent the presence or absence of a value. It was introduced in Java 8 and provides several features:

  1. Handling null values: Optional is used to avoid NullPointerException by explicitly representing the absence of a value.

  2. Chaining operations: Optional can be used to chain together multiple operations that depend on the presence of a value and handle the absence of a value in a single place.

  3. Functional methods: Optional class provides functional methods like map, flatMap, filter and orElse, orElseGet which are used to handle the absence of a value in a more elegant and readable way.

  4. Default values: orElse and orElseGet methods can be used to provide a default value if the Optional is empty.

  5. Type safety: Optional ensures that the value is of the correct type and eliminates the need for explicit casting.

  6. Thread safety: Optional is immutable, thus it is thread-safe.

  7. Empty Optional: It provides an easy way to create an empty Optional by calling Optional.empty().

Using as a wrapper

We can wrap our return type in findByTitle with optional.

@Repository
public class BookRepository extends JpaRepository<Book, Long> {
    Optional<Book> findByTitle(String title);
}

This way, the searchByTitle method in SearchService can check if a value is present in Optional before using it, preventing any potential null pointer exceptions.

Creating an Instance

There are several ways to create an instance of Optional in Java:

  • Optional.of(value): Creates an Optional with the given non-null value. If the value passed is null, it will throw a NullPointerException.
String value = "hello";
Optional<String> optional = Optional.of(value);
  • Optional.ofNullable(value): Creates an Optional with the given value, whether it is null or not.
String value = "hello";
Optional<String> optional = Optional.ofNullable(value);
  • Optional.empty(): Creates an empty Optional.
Optional<String> optional = Optional.empty();

It is important to keep in mind that Optional is a container object, it can’t be used to represent the presence of a null value, it’s used to represent the absence of a value.

Accessing

Once you have an instance of Optional, there are several ways to access the value it contains:

  • get(): Retrieves the value inside the Optional. If the Optional is empty, it will throw a NoSuchElementException.
Optional<String> optional = Optional.of("hello");
String value = optional.get(); // value = "hello"
  • isPresent(): Returns true if the Optional contains a value, false otherwise.
Optional<String> optional = Optional.of("hello");
if(optional.isPresent()){
    System.out.println("Is Present");
}
  • ifPresent(Consumer<T> consumer): If a value is present, invoke the specified consumer with the value, otherwise do nothing.
Optional<String> optional = Optional.of("hello");
optional.ifPresent(val -> System.out.println(val)); //prints "hello"
  • orElse(T other): Returns the value if present, otherwise returns other.
Optional<String> optional = Optional.empty();
String value = optional.orElse("default value"); // value = "default value"
  • orElseGet(Supplier<T> other): Returns the value if present, otherwise invokes other and returns the result of that invocation.
Optional<String> optional = Optional.empty();
String value = optional.orElseGet(() -> "default value"); // value = "default value"
  • orElseThrow(): Returns the contained value, if present. Otherwise, it will throw the exception provided as the parameter.
Optional<String> optional = Optional.empty();
String value = optional.orElseThrow(() -> new IllegalArgumentException("Value not present"));

These methods provide a way to safely access the value contained in an Optional without the risk of a NullPointerException.

Filters and Maps

The filter and map methods in the Optional class are used to manipulate the value contained in the Optional object.

  • filter(Predicate<T> predicate): If a value is present and the value matches the given predicate, return an Optional describing the value, otherwise, return an empty Optional.
Optional<Integer> optional = Optional.of(5);
Optional<Integer> filteredOptional = optional.filter(val -> val > 3); 
System.out.println(filteredOptional.get()); //5
  • map(Function<T, R> mapper): If a value is present, apply the provided mapping function to it, and if the result is non-null, return an Optional describing the result. Otherwise, return an empty Optional.
Optional<Integer> optional = Optional.of(5);
Optional<String> mappedOptional = optional.map(val -> "Value: " + val); 
System.out.println(mappedOptional.get()); //Value: 5

Both filter and map methods return a new Optional containing the result of the operation. It is important to note that if the original Optional is empty, the result of both methods will be an empty Optional.

You can chain multiple filters and map operations together.

Optional<Integer> optional = Optional.of(5);
Optional<String> finalOptional = optional
                .filter(val -> val > 3)
                .map(val -> "Value: " + val)
                .map(String::toUpperCase);
System.out.print(finalOptional);

Output

Optional[VALUE: 5]

FlatMap

The flatMap method in the Optional class is similar to the map method, but it is used when the mapping function returns an Optional instead of a value. The flatMap method "flattens" the nested Optional into a single Optional.

Here is an example of using flatMap:

Optional<String> opt1 = Optional.of("hello");
Optional<String> opt2 = opt1.flatMap(val -> Optional.of(val + " world"));
System.out.println(opt2.get()); // "hello world"

In the above example, the flatMap method takes a lambda expression that maps the value "hello" to a new Optional containing the string "hello world". The flatMap method then "flattens" this nested Optional and returns a new Optional containing the final value "hello world".

The flatMap method is useful when you have a sequence of operations that depend on the presence of a value, and you want to chain them together using Optional. It eliminates the need for explicit null-checking and makes the code more readable.

Here is an example of using flatMap with a class hierarchy:

class Outer {
    Inner inner;
    public Inner getInner() { 
       return inner; 
    }
}
class Inner {
    String value;
    public String getValue() { 
       return value; 
    }
}


Optional<Outer> outerOpt = Optional.of(new Outer());
Optional<String> valueOpt = 
     outerOpt.flatMap(outer -> Optional.ofNullable(outer.getInner()))
    .flatMap(inner -> Optional.ofNullable(inner.getValue()));

Here, the flatMap method is used to chain together the operations of getting the Inner object from the Outer object and then get the value from the Inner object, all within the context of an Optional.

It’s important to note that if any of the intermediate Optional is empty, the final result will be an empty Optional as well.

Things to keep in mind

Before using Optional in your code, there are several things to consider:

  1. Code readability: Using Optional can make the code more verbose and harder to read, especially if multiple Optional objects are used in the same method or if they are nested inside each other.

  2. Performance: Using Optional can have an impact on performance, as it creates additional objects and requires more memory. In situations where the performance is critical, it's better to use null checks or other alternatives.

  3. Overuse: While Optional is a useful tool for preventing NullPointerException, it should not be used everywhere. Overusing Optional can make the code more complex and harder to understand. It's important to use it only when it provides real value.

  4. Return type: Keep in mind that the return type of a method that uses Optional is different from the return type of a method that uses a direct value. This can make it harder to understand what the method returns and can create confusion when working with the method.

  5. Return empty optional: Returning an empty Optional should only be done when it makes sense if the method should return an empty Optional when there is no matching value found.

  6. Familiarize with methods: To make the most of Optional, you should be familiar with functional methods like map, flatMap, filter and orElse, orElseGet which are used to handle the absence of a value in a more elegant and readable way.

It’s important to weigh the pros and cons of using Optional and to use it only when it provides real value, in order to make the code more readable, maintainable, and performant.

In summary, Java Optional is a useful feature that provides a way to handle null values in a more elegant and readable way, it also allows to chain multiple operations together and provides functional methods like map and flatMap to deal with the absence of a value.

That’s all about “Java Optional”. Send us your feedback using the message button below. Your feedback helps us create better content for you and others. Thanks for reading!

If you like the article please like and share. To show your support!
Thanks.

Did you find this article valuable?

Support Ayush Singhal by becoming a sponsor. Any amount is appreciated!