Optional in Java: Null-Safe Patterns with Runnable Examples

Learn Java Optional from creation to stream pipelines through one user lookup. Trace present and empty paths, compare fallbacks, and repair common mistakes.

KnowledgeGate Team

Exam prep & CS education

Updated 23 Sep 20265 min read

Replacing null with Optional does not automatically make code safe if you still call get(), build fallbacks eagerly, or nest one Optional inside another. The useful mental model is a result with exactly two states, present or empty, followed by an explicit operation for each state. ID 205 reaches RAVI, ID 999 becomes NOT FOUND, and ID 101 is filtered out despite being present. For wider programming and DSA foundations, browse Coding & DSA Courses for Placements.

Optional in Java: the two-state model and creation methods

Optional<T> is a container that is either present with one non-null T or empty. It tells a caller that a method may have no result. It does not make the contained object immutable, and it is not a collection that can hold several values.

java
System.out.println(Optional.of("Java"));              // Optional[Java]
System.out.println(Optional.empty());                 // Optional.empty
Optional<String> nickname = Optional.ofNullable(null);
System.out.println(nickname);                         // Optional.empty

Optional.of(null) throws NullPointerException. ofNullable(null) deliberately produces the empty state.

Creation method

Use it when

of(value)

A null value would be a caller or programmer error

ofNullable(value)

A boundary such as Map.get may return null

empty()

The method has intentionally found no result

This is not permission to pass null deeper into a program. Convert a nullable boundary once, then keep the contract explicit.

Optional user lookup with creation, filtering, mapping, and fallback

java
import java.util.Map;
import java.util.Optional;

public class OptionalUserDemo {
    record User(int id, String name, String email) {}

    private static final Map<Integer, User> USERS = Map.of(
            101, new User(101, "Asha", "asha@example.com"),
            205, new User(205, "Ravi", "ravi@kg.ai")
    );

    static Optional<User> findUser(int id) {
        return Optional.ofNullable(USERS.get(id));
    }

    public static void main(String[] args) {
        String verifiedName = findUser(205)
                .filter(user -> user.email().endsWith("@kg.ai"))
                .map(User::name)
                .map(String::toUpperCase)
                .orElse("NOT FOUND");

        String missingName = findUser(999)
                .filter(user -> user.email().endsWith("@kg.ai"))
                .map(User::name)
                .map(String::toUpperCase)
                .orElse("NOT FOUND");

        System.out.println(verifiedName);
        System.out.println(missingName);
    }
}

For ID 205, USERS.get(205) returns User(205, "Ravi", "ravi@kg.ai"). ofNullable creates a present optional, the domain filter passes, map(User::name) produces Optional[Ravi], and upper-casing produces Optional[RAVI]. The terminal returns RAVI.

For ID 999, Map.get returns null. ofNullable creates Optional.empty, the later operations are skipped, and the fallback is NOT FOUND.

Code
RAVI
NOT FOUND

findUser converts the nullable map lookup into an explicit return contract once. Callers can compose operations without repeating user != null checks.

Flowchart of the findUser lookup: id 205 passes the filter and maps to RAVI, while id 999 gives Optional.empty and orElse returns NOT FOUND.

Reading an Optional safely without get()

Observation and extraction are different jobs. findUser(205).ifPresent(user -> System.out.println(user.name())); prints Ravi. This empty-path branch prints No user:

java
findUser(999).ifPresentOrElse(
        user -> System.out.println(user.name()),
        () -> System.out.println("No user"));

These methods are for side effects, not for manufacturing a return value inside a callback. For values, findUser(205).map(User::name).orElse("Guest") returns Ravi, while the same chain for 999 returns Guest. If absence is an error, this throws IllegalArgumentException with the message Unknown user: 999:

java
findUser(999).orElseThrow(
        () -> new IllegalArgumentException("Unknown user: 999"));

Avoid isPresent() followed by get(). It rebuilds a manual branch, and get() on empty throws NoSuchElementException. Choose map, ifPresentOrElse, a fallback, or orElseThrow according to whether you need a transformed value, a side effect, a default, or a hard failure.

orElse versus orElseGet: eager and lazy fallback

java
import java.util.Optional;

public class OptionalFallbackDemo {
    static String buildFallback() {
        System.out.println("building fallback");
        return "Guest";
    }

    public static void main(String[] args) {
        String eager = Optional.of("Ravi").orElse(buildFallback());
        System.out.println("eager=" + eager);

        String lazy = Optional.of("Ravi")
                .orElseGet(() -> buildFallback());
        System.out.println("lazy=" + lazy);

        String empty = Optional.<String>empty()
                .orElseGet(() -> buildFallback());
        System.out.println("empty=" + empty);
    }
}

The five output lines are:

Code
building fallback
eager=Ravi
lazy=Ravi
building fallback
empty=Guest

orElse evaluates its argument before the call, even when the optional is present. orElseGet invokes its supplier only for an empty optional. A constant such as "Guest" is clear with orElse. Prefer orElseGet when construction performs work, logs, reads data, or has another visible effect. Use orElseThrow when absence violates the caller's contract.

Side-by-side trace: orElse calls buildFallback even for a present Optional, while orElseGet runs its supplier only when empty.

map, flatMap and filter without nested Optional values

For ID 205, filter may keep or empty the same user, while map(User::name) changes Optional<User> into Optional<String>. Empty optionals skip both callbacks. Change the email test to endsWith("@company.com"), and Ravi fails the filter, so the exact fallback is NOT FOUND.

java
static Optional<String> kgEmail(User user) {
    return user.email().endsWith("@kg.ai")
            ? Optional.of(user.email())
            : Optional.empty();
}

findUser(205).map(OptionalUserDemo::kgEmail) produces Optional[Optional[ravi@kg.ai]]. findUser(205).flatMap(OptionalUserDemo::kgEmail) produces the desired Optional[ravi@kg.ai]. Use map when the mapper returns a plain value and flatMap when it already returns an Optional.

A mapper that returns null makes map produce Optional.empty. That is only a safe edge, not a design recommendation. A method declared to return Optional must return Optional.empty, never null.

Optional with streams: parse, flatten, filter and sort

java
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class OptionalStreamDemo {
    static Optional<Integer> parseInt(String text) {
        try {
            return Optional.of(Integer.parseInt(text));
        } catch (NumberFormatException exception) {
            return Optional.empty();
        }
    }

    public static void main(String[] args) {
        List<Integer> values = List.of("42", "", "17", "x", "68").stream()
                .map(OptionalStreamDemo::parseInt)
                .flatMap(Optional::stream)
                .filter(n -> n % 2 == 0)
                .sorted(Comparator.reverseOrder())
                .collect(Collectors.toList());

        System.out.println(values);
        System.out.println(values.stream().mapToInt(Integer::intValue).sum());
    }
}

Parsing gives [Optional[42], Optional.empty, Optional[17], Optional.empty, Optional[68]]. Flattening gives [42, 17, 68], the even filter keeps [42, 68], and reverse sorting gives [68, 42]. The addition is 68 + 42 = 110, so the program prints [68, 42] and then 110.

Optional.stream() is the zero-or-one-element bridge into a stream pipeline. Optional handles missing parse results, while the comparator controls ordering. See Sorting Algorithms: Complexity and Comparison for the sorting choices and their costs.

Optional mistakes and output-prediction exercises

Mistake

Consequence

Correction

Optional.of(nullableValue)

Throws on null

Use ofNullable at a truly nullable boundary

optional.get()

Throws when empty

Select a meaningful terminal

Returning null from findUser

Breaks the declared abstraction

Return Optional.empty

Using Optional for every field or parameter

Obscures ordinary required data

Reserve it mainly for return values that may be absent

Predict these before reading the answers:

  1. Optional.of("java").filter(s -> s.length() > 5).map(String::toUpperCase).orElse("short")

  2. The exact @kg.ai pipeline for findUser(101)

  3. Optional.<Integer>empty().map(n -> n * 2).orElse(7)

Answers: item 1 returns short because length 4 fails the filter. Item 2 returns NOT FOUND because asha@example.com fails the domain filter. Item 3 returns 7, and its mapper never runs.

Why may orElse(expensiveLookup()) waste work for a present value? Its argument is evaluated eagerly. Use orElseGet(() -> expensiveLookup()) for a lazy fallback. Time Complexity and Asymptotic Notation: Big-O is the next read for analysing work inside suppliers, mappers, and stream stages. Optional itself does not improve algorithmic complexity.

Optional in Java: the short version and next step

  • Create with of, ofNullable, or empty intentionally.

  • Return Optional, not null.

  • Transform plain values with map.

  • Flatten Optional-returning work with flatMap.

  • Choose eager, lazy, or throwing terminals deliberately.

  • Test both present and empty paths with actual values.

In the worked lookup, ID 205 becomes RAVI, while ID 999 becomes NOT FOUND, without a direct get() call. As a final check, ID 101 is present but still becomes NOT FOUND because asha@example.com fails the @kg.ai filter.

Use Java: Concepts, MCQs and Coding Questions as the structured next step for Core and Advanced Java concepts, MCQs, and coding questions. After the language fundamentals, DSA using Java: Placement Preparation Course is the later route into Java-based coding-round and technical-interview practice.