Blog

EN
Optional in Java: how to say goodbye (almost) to NullPointerException

Optional in Java: how to say goodbye (almost) to NullPointerException

8 min read

If you've been programming in Java for a while, you're probably very familiar with NullPointerException. It always seems to appear at the worst possible time, almost always in production, and its error message rarely tells you exactly which variable was null.

Since Java 8, the language includes a tool specifically designed to solve this problem: the Optional class. In this article, we will look at what it is, why it exists, and how to use it correctly with code examples.

The Problem It Solves

Imagine this classic method:

public User findUserById(String id) {
    // may return a User or null if not found
    return db.get(id);
}

And then, somewhere else in the code:

User user = findUserById("123");
System.out.println(user.getName()); // NullPointerException if it doesn't exist

The real problem isn't just that the program crashes. It's that the method doesn't warn you. It is an invisible contract, as the rule exists because there might not be a user, but it isn't written anywhere visible. It is only discovered when the NullPointerException occurs.

NullPointerException meme

What is Optional?

Optional<T> is a container that can either hold a value of type T or be empty. Internally, it is basically a box with a reference that can be null or not, but it forces you to handle it explicitly instead of letting you access the value "blindly".

Let's rewrite the previous example:

public Optional<User> findUserById(String id) {
    User user = db.get(id);
    return Optional.ofNullable(user);
}

Now, anyone calling this method knows that the user might not exist.

Creating an Optional

There are three main ways to create one:

// 1. When you are SURE the value is not null
Optional<String> name = Optional.of("Ana");

// 2. When the value CAN be null
Optional<String> potentiallyNullName = Optional.ofNullable(potentiallyNullName);

// 3. An explicitly empty Optional
Optional<String> empty = Optional.empty();

Be careful with Optional.of(); if you pass null, it throws an exception immediately (NullPointerException). It is only used when you are certain that the value exists. For everything else, ofNullable is your friend.

Checking if a Value is Present

Optional<User> result = findUserById("123");

if (result.isPresent()) {
    System.out.println("Found: " + result.get());
} else {
    System.out.println("This user does not exist");
}

This works, but it is practically the same pattern as an if (user != null) check. We are not leveraging what Optional truly offers.

The Best Way: Avoid `get()`

Calling .get() without checking if a value is present first is a very common anti-pattern. If the Optional is empty, .get() throws a NoSuchElementException, so you would only be swapping one type of exception for another.

Instead, Optional offers much more expressive methods:

orElse: a default value

String name = findUserById("123")
    .map(User::getName)
    .orElse("Unknown user");

orElseGet: a default value

String name = findUserById("123")
    .map(User::getName)
    .orElseGet(() -> getDefaultNameFromConfiguration());

The difference with orElse is subtle but important: orElseGet only executes the Supplier if it is actually needed. orElse always evaluates its argument, even if a value is present. If calculating the default value is expensive (a database call, for example), **always use orElseGet**.

orElseThrow: throwing a meaningful exception

User user = findUserById("123")
    .orElseThrow(() -> new UserNotFoundException("User 123 does not exist"));

This is much clearer than a generic NullPointerException since the exception explains exactly what happened.

Transforming Values with map and flatMap

This is where Optional becomes very useful, as it allows you to chain operations without having to nest if statements everywhere.

Optional<User> user = findUserById("123");

Optional<String> nameInUppercase = user
    .map(User::getName)
    .map(String::toUpperCase);

System.out.println(nameInUppercase.orElse("NO NAME"));

If user is empty, each map is simply skipped and the final result is an empty Optional. There is no need to check for nulls at each step.

flatMap is used when the function you apply already returns an Optional, to avoid ending up with an Optional<Optional<T>>:

public Optional<Address> searchAddress(User user) {
    return Optional.ofNullable(user.getAddress());
}

Optional<String> city = findUserById("123")
    .flatMap(this::searchAddress)
    .map(Address::getCity);

Executing Actions with `ifPresent` and `ifPresentOrElse`

Instead of checking if a value is present and then doing something, you can directly tell Optional what to do in each case:

findUserById("123")
    .ifPresent(u -> System.out.println("Welcome, " + u.getName()));

Since Java 9, ifPresentOrElse also exists to handle both cases at once:

findUserById("123").ifPresentOrElse(
    u -> System.out.println("Welcome, " + u.getName()),
    () -> System.out.println("User not found")
);

Complete Example

public String getBillingCity(String idUser) {
    return findUserById(idUser)
        .filter(User::isActive)
        .flatMap(this::searchAddress)
        .map(Address::getCity)
        .map(String::toUpperCase)
        .orElse("CITY NOT AVAILABLE");
}

Compared to the alternative of nested null checks, the difference is huge.

Optional does not eliminate the concept of null, but it does eliminate the ambiguity that exists. It forces you to think about the case where the value does not exist.

Share this article on