Skip to content

Optional and null handling

Why null causes so many bugs, how to use Optional well (and where not to), failing fast with requireNonNull, empty collections, and the Null Object pattern.

Builds on Lambdas, functional interfaces and streams

Stage 1core7 min read

Why null is a problem

null means "no object here". The trouble is that the type does not warn you. A User variable might hold a user, or it might hold null.

User user = userRepository.findUserById("u42");
String city = user.getAddress().getCity();     // which of these is null?

findUserById returns User. Does it return null when the user is missing, or throw? You cannot tell from the signature. You have to read the code or find out in production.

So people make one of two mistakes. They forget the check and get a NullPointerException (NPE). Java throws an NPE when you call a method or read a field on null. Or they get scared and add if (x != null) everywhere, which buries the real logic. NPEs are still among the most common exceptions in production Java logs. Almost every one comes from a value that someone assumed could never be missing.

Optional<T> (Java 8) is a box that either holds one non-null value or is empty. It puts "this may have no result" into the method signature. The return type now says: "there may be no answer, and the caller must decide what that means." It does not remove every possible null, but it makes this one boundary honest.

Optional<User> findUserById(String id);       // the signature now tells the truth

Optional basics

Creating one

Optional.of(user)            // user must not be null, throws NPE if it is
Optional.ofNullable(user)    // empty if user is null, otherwise holds it
Optional.empty()             // explicitly empty

Use ofNullable when the value might be null, like map.get(key). Use of when it must never be null, so a mistake fails loudly right away.

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

Getting the value out

User user = findUserById("u42").orElse(guestUser);
User user = findUserById("u42").orElseGet(() -> createGuestUser());
User user = findUserById("u42").orElseThrow();      // NoSuchElementException (Java 10)
User user = findUserById("u42")
        .orElseThrow(() -> new UserNotFoundException("u42"));

Avoid get(). It throws if the Optional is empty, so it is a null pointer bug with a different name. if (opt.isPresent()) { opt.get() } is also just a null check written the long way. The no argument orElseThrow() does exactly what get() does. Java 10 added it because the name get hides the fact that it can throw.

orElse vs orElseGet

Interviewers like this one because the wrong choice has a hidden cost.

static User createGuestUser() {
    System.out.println("creating guest user");
    return new User("guest", "Guest");
}

User a = findUserById("u42").orElse(createGuestUser());
// prints "creating guest user" even though u42 exists

User b = findUserById("u42").orElseGet(() -> createGuestUser());
// prints nothing, the supplier is only called when the Optional is empty
  • orElse(x) is eager, so it always runs. Java works out x before orElse is called, like any method argument.
  • orElseGet(supplier) is lazy. The supplier runs only if the value is missing.

For a constant like "Unknown" or 0, orElse is fine. If the default hits the database, creates objects, or calls another service, use orElseGet.

map, flatMap, filter

These let you work on the value inside the Optional without taking it out first.

// map: change the value inside, if there is one
String name = findUserById("u42")
        .map(User::getName)
        .orElse("Guest");

// flatMap: when the function itself returns an Optional
String city = findUserById("u42")
        .flatMap(User::getAddress)          // getAddress() returns Optional<Address>
        .map(Address::getCity)
        .orElse("Unknown");

// filter: keep the value only if it passes a test
findUserById("u42")
        .filter(User::isPremium)
        .ifPresentOrElse(
                u -> System.out.println("Free delivery for " + u.getName()),
                () -> System.out.println("Delivery fee Rs 30"));

Why flatMap there? getAddress() already returns an Optional. map would wrap it again and give Optional<Optional<Address>>. flatMap removes the extra layer, as it does on streams.

One handy detail: if the function in map returns null, you get an empty Optional, not a crash.

findUserById("u7").map(User::getEmail);   // Optional.empty if u7 has no email
Loading diagram…

Once the chain goes empty, every later step is skipped and you land on the fallback.

Where to use Optional, and where not

The Java designers meant Optional for one job: return types that might have no result.

Good:

Optional<User> findUserById(String id)
Optional<Order> findLatestOrder(String userId)
Optional<Player> getWinner()

Not good:

WhereWhy notDo this instead
FieldsAdds an object per field, and Optional is not Serializable (it cannot be turned into bytes to save or send)A nullable field with a getter that returns Optional
Method parametersCallers can still pass null for the Optional itself, and calls get uglyOverloads, or two separate methods
Collections (Optional<List<Order>>)An empty list already means "nothing"Return an empty list
Inside collections (List<Optional<X>>)Hard to read, almost always a design smellFilter the empties out
DTOs and JSON or JPA entitiesMost frameworks do not handle it wellPlain nullable fields

A parameter example. This:

void reportGameResult(Player p1, Player p2, Optional<Player> winner)

reads better as two methods:

void reportWin(Player winner, Player loser)
void reportDraw(Player p1, Player p2)

In an interview, returning Optional from a finder is enough to show you know this. Real codebases often go one step further. Teams mark nullable fields and parameters with an annotation such as JSpecify's @Nullable. The IDE or a build time checker like NullAway then flags any use that skips the null check. This catches the NPE before the code ships, which Optional alone cannot do for fields and parameters.

Fail fast with Objects.requireNonNull

The worst NPE is one that happens far from where the null came in. A null restaurant id gets stored in the constructor, and three calls later something crashes inside a pricing method with no clue where it came from.

Check at the door instead:

public class Order {
    private final String orderId;
    private final String restaurantId;
    private final PaymentMode paymentMode;

    public Order(String orderId, String restaurantId, PaymentMode paymentMode) {
        this.orderId = Objects.requireNonNull(orderId, "orderId must not be null");
        this.restaurantId = Objects.requireNonNull(restaurantId, "restaurantId must not be null");
        this.paymentMode = Objects.requireNonNullElse(paymentMode, PaymentMode.COD);
    }
}
  • requireNonNull(x, message) throws right away with your message if x is null, and returns x otherwise, so you can assign in one line.
  • requireNonNullElse(x, fallback) (Java 9) gives a default, so a null paymentMode above becomes COD.
  • requireNonNullElseGet(x, supplier) is the lazy version, like orElseGet.

Now the stack trace points at the constructor call that passed the null.

Return empty collections, never null

// Bad
public List<Order> getOrders(String userId) {
    if (!ordersByUser.containsKey(userId)) return null;
    return ordersByUser.get(userId);
}

// Good
public List<Order> getOrders(String userId) {
    return ordersByUser.getOrDefault(userId, List.of());
}

Every caller can now loop, stream or call isEmpty() without a check. An empty list already says "nothing here", so there is no need for null or Optional.

One catch: List.of() cannot be changed. A caller who tries to add to it gets UnsupportedOperationException, which is usually what you want. But when the user has orders, the good version hands out the internal list itself, so callers could change it. To stop that, wrap it with List.copyOf or Collections.unmodifiableList (see 19-StringsAndImmutability.md).

Null Object pattern

Sometimes "nothing" has a sensible behaviour. A missing discount means a discount of zero. Instead of passing null and checking for it everywhere, make an object that does nothing.

public interface Discount {
    BigDecimal apply(BigDecimal amount);
}

public class FlatDiscount implements Discount {
    private final BigDecimal off;
    public FlatDiscount(BigDecimal off) { this.off = off; }
    public BigDecimal apply(BigDecimal amount) { return amount.subtract(off).max(BigDecimal.ZERO); }
}

public class NoDiscount implements Discount {
    public BigDecimal apply(BigDecimal amount) { return amount; }
}

FlatDiscount uses max(BigDecimal.ZERO) so a large discount never makes the price negative.

// Before: every caller checks
BigDecimal total = discount != null ? discount.apply(subtotal) : subtotal;

// After: no check needed
BigDecimal total = discount.apply(subtotal);    // discount is NoDiscount when there is none

Use it when "nothing" has an obvious default behaviour. Do not use it when the caller really needs to know the value was missing. A NoUser object that quietly lets anonymous people check out is worse than an empty Optional.

Helpful NullPointerException messages (Java 14+)

Older Java gave you only a line number. With a chain like order.getUser().getAddress().getCity() on one line, you had to guess which part was null.

Since Java 14 (switched on by default from Java 15), the message tells you which part was null:

Cannot invoke "Address.getCity()" because the return value of "User.getAddress()" is null

For unboxing:

Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null

Local variable names show up only if the code was compiled with debug info (javac -g). Maven and Gradle do that by default. Otherwise you see "<local4>" instead of the name.

Common mistakes

  • Calling opt.get() without checking. Use orElse, orElseGet or orElseThrow.
  • Returning null from a method whose return type is Optional. The whole point is lost, and callers crash on null.isPresent().
  • Using orElse with an expensive default. It runs every time. Use orElseGet.
  • Optional.of(x) when x might be null. Use ofNullable.
  • Optional fields and parameters. Keep Optional for return types.
  • Optional<List<X>>. Return an empty list.
  • Checking for null deep inside the code instead of rejecting it at the constructor.

Interview questions

"What is the difference between orElse and orElseGet?"

orElse takes a value, so its argument is always evaluated, even when the Optional has a value. orElseGet takes a supplier that runs only when the Optional is empty, so use it when the default is expensive.

"Where should you use Optional?"

As the return type of methods that may have no result, like findUserById. Not for fields, method parameters or collections, and never return null from a method that returns Optional.

"What is the difference between map and flatMap on Optional?"

map wraps the function's result in an Optional. If the function already returns an Optional, map gives you Optional<Optional<T>>, and flatMap flattens that to Optional<T>.

"What does Objects.requireNonNull give you?"

It throws a NullPointerException with your message the moment a null comes in, usually in the constructor. The failure then points at the caller who passed the null instead of some later line that used it.

"Should a method that returns a list return null when there are no results?"

No. Return an empty list. Callers can loop over it safely without a null check, and empty already means "no results".

"What is the Null Object pattern?"

Instead of using null for "nothing", you pass an object that implements the same interface and does nothing, like a NoDiscount that returns the amount unchanged. Callers stop checking for null. Only use it when doing nothing is the right default.

Where you see it in this repo

  • Optional as a finder return type: TicketRepository.findById, PaymentRepository.findById and the other repositories in LLD/ParkingLot/src/repository return Optional.ofNullable(map.get(id)). That is the textbook use.
  • orElseThrow: AdminService in the same project does floorRepository.findByNumber(...).orElseThrow(...) and the same for pricing rules.
  • isEmpty then get: PricingService.calculateFee checks ruleOpt.isEmpty() and later calls ruleOpt.get(). It works, but it is the null check style written with Optional. A map(...).orElse(50.0) chain would avoid the separate get().
  • Optional return: Board.getWinner() in OOOD/07-tic-tac-toe returns Optional<Player>, since a game can end with no winner.
  • Optional as a parameter: ScoreTracker.reportGameResult(Player, Player, Optional<Player>) in the same game. This is the case the table above warns about. Two methods, one for a win and one for a draw, would read better.
  • requireNonNullElseGet: Ticket.calculateParkingDuration() in OOOD/01-parking-lot uses Objects.requireNonNullElseGet(exitTime, LocalDateTime::now) so a ticket with no exit time yet is priced up to the current time.
  • Methods that still return null: ParkingManager.findSpotForVehicle, ParkingManager.parkVehicle and ParkingLot.enterVehicle in OOOD/01-parking-lot return null when no spot is free, and ZomatoApp.checkout in LLD/Zomato returns null for an empty cart. These are good candidates for Optional or a clear exception.
  • A Null Object candidate: Receipt.printReceipt() in OOOD/06-grocery-store-system does getAppliedDiscounts().get(item) and checks if (discount != null). A NoDiscount default would remove the null check from the price maths. The receipt still needs to know whether a discount applied, so it can decide whether to print the discount line.

Related: 16-LambdasAndStreams.md for the method references used with map, and 20-MemoryAndPassByValue.md for the NPE you get from unboxing a null.

In this topic

Done with this topic?

How well do you know it?

PreviousMemory, pass by value and garbage collection
Practise 6 questions