---
title: '`Optional`'
source: 'https://academia.sh/en/courses/java-standard-library/optional'
course: 'The Standard Library and Streams'
language: en
updated: '2026-08-17T18:09:42+00:00'
license: 'CC BY-SA 4.0'
---

# `Optional`

Expressing absence as a type does not move the caller's obligation to compile time: all four unchecked calls — both in the null-returning signature and the Optional-returning signature — compile unchecked, and one falls at runtime. The gain is visibility in the signature; this gain disappears in a field and a parameter, and of() falls immediately on null while ofNullable() does not.

The previous lesson measured which promises the container
accumulating at the end of a stream chain gets built with. Collecting
always produces a value — but once a single item gets sought, what
happens if that item does not exist in the source at all? Some methods
of the standard library return null in this case, some return a
container called `Optional<T>` that expresses absence **as a type**.
`Optional` itself is not an interface, it is a single class; this
course's "who gives the guarantee" question gets asked here in a third
form: does carrying absence into the type genuinely eliminate the
caller's "check first, then read" obligation, or does it only make it
**visible**? Once absence moves into the type, does the caller's
obligation genuinely move to compile time, or does the check still
stay at runtime?

## Same Work, Two Signatures: The Check Still Stays at Runtime

- **FJ27** — The same lookup work gets written with two signatures:
  one a `String` that can return null, the other an `Optional<String>`.
  Both get called with an existing and a missing input, with both the
  unchecked and the safe idiom.

```java
// Same.java - the same work, a null-returning signature versus an Optional-returning one: the check left to the caller
import java.util.*;

public class Same {
    static final Map<String, String> COLORS = Map.of("pear", "green");

    static String findColor(String fruit) { return COLORS.get(fruit); }
    static Optional<String> findColorOptional(String fruit) { return Optional.ofNullable(COLORS.get(fruit)); }

    public static void main(String[] args) {
        int checkedCalls = 0, uncheckedCalls = 0;

        try {
            int len1 = findColor("pear").length();
            uncheckedCalls++;
            System.out.println("null-returning signature, existing input : " + len1 + " (compiled unchecked)");
        } catch (NullPointerException e) { System.out.println("unexpected exception"); }

        try {
            int len2 = findColor("apple").length();
            System.out.println("null-returning signature, missing input  : " + len2);
        } catch (NullPointerException e) {
            uncheckedCalls++;
            System.out.println("null-returning signature, missing input  -> exception: "
                    + e.getClass().getSimpleName() + " (compiled unchecked, fell at runtime)");
        }

        try {
            int len3 = findColorOptional("pear").get().length();
            uncheckedCalls++;
            System.out.println("Optional-returning signature, existing input : " + len3 + " (compiled unchecked)");
        } catch (NoSuchElementException e) { System.out.println("unexpected exception"); }

        try {
            int len4 = findColorOptional("apple").get().length();
            System.out.println("Optional-returning signature, missing input  : " + len4);
        } catch (NoSuchElementException e) {
            uncheckedCalls++;
            System.out.println("Optional-returning signature, missing input  -> exception: "
                    + e.getClass().getSimpleName() + " (compiled unchecked, fell at runtime)");
        }

        System.out.println("safe idiom, null-returning signature   : "
                + (findColor("apple") != null ? findColor("apple") : "unknown"));
        checkedCalls++;
        System.out.println("safe idiom, Optional-returning signature: " + findColorOptional("apple").orElse("unknown"));
        checkedCalls++;

        System.out.println();
        System.out.println("calls that compiled unchecked (all four of four): " + uncheckedCalls);
        System.out.println("checked calls (written with the safe idiom)     : " + checkedCalls);
    }
}
```

```
null-returning signature, existing input : 5 (compiled unchecked)
null-returning signature, missing input  -> exception: NullPointerException (compiled unchecked, fell at runtime)
Optional-returning signature, existing input : 5 (compiled unchecked)
Optional-returning signature, missing input  -> exception: NoSuchElementException (compiled unchecked, fell at runtime)
safe idiom, null-returning signature   : unknown
safe idiom, Optional-returning signature: unknown

calls that compiled unchecked (all four of four): 4
checked calls (written with the safe idiom)     : 2
```

All four unchecked calls compile — whether the signature is `String`
or `Optional<String>`, the compiler raises no objection to any of
them. Called with a missing input, both fail at runtime, only the
exception's name changes: `NoSuchElementException` instead of
`NullPointerException`. The measured number is plain here: the count
of calls that can pass unchecked, left to the caller by two signatures
doing the same work, is **equal**. The `Optional` return type does not
drop this count to zero; when the check does not get written, it
still falls at runtime.

The gain is somewhere else: in the **signature itself**. Someone
reading the signature `String findColor(String)` cannot know the
returned value might be null — they only learn this from the
documentation or by reading the source. The signature
`Optional<String> findColorOptional(String)`, though, states the
possibility of absence **directly**; the caller has to look at
neither the documentation nor the source to see this. The check
itself is still optional, but **whether** a check is needed is now
written in the type.

This distinction can be compared to the checked exception built in the
Object-Oriented Java course: there, the exception a method could throw
was written in the signature, and the caller had to handle it —
otherwise compilation stopped; not catching it was an option too, but
the option was at least visible. The `Optional` return type provides
similar visibility, but has no mandatory side: while code that handles
a checked exception nowhere at all does not compile, code that calls
`get()` on an `Optional` with no check at all compiles without a
hitch, as seen in the fourth line. Both mechanisms carry absence or
the error into the signature; only one makes it mandatory, the other
only makes it visible.

## Chaining Does Not Eliminate the Check, It Defers It

`Optional` is not just a container; it offers a chaining similar to a
stream chain, with methods like `map`, `orElse`, `orElseGet`. This
chaining does not turn a null value into a check that never gets
written, but it at least eliminates an `if (x != null)` block repeated
at every step.

- **FJ28** — `findColorOptional` gets called with the same two inputs
  (`"pear"`, `"apple"`), and the result gets uppercased with `map` and
  bound to a default string with `orElse`; no explicit check gets
  written at any intermediate step.

```java
// Chain.java - how Optional chaining removes the intermediate check
import java.util.*;

public class Chain {
    static final Map<String, String> COLORS = Map.of("pear", "green");
    static Optional<String> findColorOptional(String fruit) { return Optional.ofNullable(COLORS.get(fruit)); }

    public static void main(String[] args) {
        String upperExisting = findColorOptional("pear").map(s -> s.toUpperCase(Locale.ROOT)).orElse("NONE");
        String upperMissing = findColorOptional("apple").map(s -> s.toUpperCase(Locale.ROOT)).orElse("NONE");
        System.out.println("existing input, chain result   : " + upperExisting);
        System.out.println("missing input, chain result    : " + upperMissing);
    }
}
```

```
existing input, chain result   : GREEN
missing input, chain result    : NONE
```

The `map(s -> s.toUpperCase(Locale.ROOT))` call never runs if the
container is empty — the "interface guarantee" measured in the
Predicate and Function lessons holds here too: `map`'s own promise is
never calling the function on an empty container. This is why, once
`findColorOptional("apple")` returns an empty container,
`s.toUpperCase(...)` never runs and no exception falls; the chain
drops straight to `orElse("NONE")`. The gain here is real: had we
written the same work with null, a separate `if` would be needed at
every step.

But this chaining has a trap of its own. `orElse` and `orElseGet` look
like they do the same work — both say "give this if it is missing" —
but **when** they compute their argument differs.

- **FJ29** — The same expensive default-value producer gets called on
  both a full and an empty container, with both `orElse` and
  `orElseGet`; how many times the producer runs gets counted.

```java
// OrElse.java - orElse always computes the expensive default, orElseGet only when needed
import java.util.*;

public class OrElse {
    static int callCount = 0;

    static String expensiveDefault() {
        callCount++;
        return "default";
    }

    public static void main(String[] args) {
        Optional<String> full = Optional.of("pear");

        callCount = 0;
        full.orElse(expensiveDefault());
        System.out.println("orElse() on a full container, default called how many times   : " + callCount);

        callCount = 0;
        full.orElseGet(OrElse::expensiveDefault);
        System.out.println("orElseGet() on a full container, default called how many times: " + callCount);

        Optional<String> empty = Optional.empty();
        callCount = 0;
        empty.orElse(expensiveDefault());
        System.out.println("orElse() on an empty container, default called how many times : " + callCount);

        callCount = 0;
        empty.orElseGet(OrElse::expensiveDefault);
        System.out.println("orElseGet() on an empty container, default called how many times: " + callCount);
    }
}
```

```
orElse() on a full container, default called how many times   : 1
orElseGet() on a full container, default called how many times: 0
orElse() on an empty container, default called how many times : 1
orElseGet() on an empty container, default called how many times: 1
```

`orElse` gets called once in **both** of the two rows where its
container is full — even though the container is full. The reason is
in the syntax: when `orElse(expensiveDefault())` gets written, the
`expensiveDefault()` call, as the argument of a Java method call, gets
evaluated **before** `orElse` itself runs — the producer runs even
when the container is full, `orElse` just does not use the result.
`orElseGet(OrElse::expensiveDefault)`, though, takes a producer
**reference** and calls it only when the container is empty; on a
full container, the producer never runs. This repeats, here, the
laziness measurement from the previous topic: **when** an operation
runs gets understood not by looking at the name of the method
wrapping it, but by **running** it.

## The Gain Disappears in a Field and a Parameter

The visibility `Optional` earns only works once it gets used as a
**return type**. Once it gets used as a field or parameter type, the
same gain disappears.

- **FJ30** — A class carrying a field of type `Optional<String>` gets
  built, and that field gets assigned null directly — the compiler
  allows this, because `Optional` is an ordinary reference-typed class
  too.

```java
// Field.java - does the gain disappear when Optional gets used in a field
import java.util.*;

public class Field {
    static class Entry {
        Optional<String> label;
    }

    public static void main(String[] args) {
        Entry e = new Entry();
        e.label = null;
        System.out.println("can the field itself be null       : " + (e.label == null));
        try {
            boolean present = e.label.isPresent();
            System.out.println("isPresent() on the field : " + present);
        } catch (NullPointerException ex) {
            System.out.println("isPresent() on the field -> exception: " + ex.getClass().getSimpleName());
        }
    }
}
```

```
can the field itself be null       : true
isPresent() on the field -> exception: NullPointerException
```

Assigning `null` directly to the `Optional<String> label` field
**compiles**. `Optional` is an ordinary class carrying a type
parameter; it is itself a reference, and like every reference, it can
be null. Calling `isPresent()` while the field is null this time
raises `NullPointerException` — exactly the same exception `Optional`
exists to prevent, just **one layer outside**.

This is the lesson's boundary measurement: `Optional`'s gain only
holds in the narrow context of getting used as a method's **return
type**. Used in a field or a parameter, a **doubled** possibility of
absence gets born — the field itself can be null, and the value the
field carries can be null — and the visibility earned turns into a
complexity that takes back more than it earned. The standard
library's own documentation counts this use as not recommended; what
gets measured here is the **reasoning** behind that recommendation.

The reason becomes clear once it joins with the first section's
measurement: `Optional`'s only protection is a **reminder** sitting in
the signature, not a rule enforced at runtime. Used as a return type,
this reminder shows up again at every call site — everyone calling
the method sees the returned value is `Optional` and decides. Used as
a field, though, the reminder shows up only once, in the class's
**definition**; code using the class does not re-read that reminder
every time it accesses the field, because the access looks like an
ordinary field read, and `Optional` itself becomes invisible.
Visibility's gain ends exactly where visibility disappears.

## of Falls Immediately, ofNullable Does Not

`Optional` has two main ways to build a container: `Optional.of(value)`
and `Optional.ofNullable(value)`. Both return a container of the same
type, but they come apart once called with a null value.

- **FJ31** — `Optional.of(null)` and `Optional.ofNullable(null)` get
  tried separately; `get()` gets called on the second one's result to
  measure when the check falls.

```java
// OfOfNullable.java - of falls immediately, ofNullable does not
import java.util.*;

public class OfOfNullable {
    public static void main(String[] args) {
        try {
            Optional<String> direct = Optional.of(null);
            System.out.println("building of(null): fell through");
        } catch (NullPointerException e) {
            System.out.println("building of(null) -> exception: " + e.getClass().getSimpleName());
        }

        Optional<String> nullable = Optional.ofNullable(null);
        System.out.println("building ofNullable(null): did not fall, result=" + nullable);
        try {
            nullable.get();
            System.out.println("ofNullable(null).get(): succeeded");
        } catch (NoSuchElementException e) {
            System.out.println("ofNullable(null).get() -> exception: " + e.getClass().getSimpleName());
        }
    }
}
```

```
building of(null) -> exception: NullPointerException
building ofNullable(null): did not fall, result=Optional.empty
ofNullable(null).get() -> exception: NoSuchElementException
```

`Optional.of(null)` falls **at the moment of construction**: an `of`
call means "I know this value is not null," and once called with a
null value, this claim gets falsified immediately.
`Optional.ofNullable(null)`, though, does not fall — it already
accepts the value might be null, and returns an empty container
(`Optional.empty()`). The break does not happen there, it happens once
the container's **content gets read**: the `get()` call falls with
`NoSuchElementException`. Both methods can end up at the same point
eventually — calling `get()` on an empty container — but `of` catches
this immediately, at the construction line, while `ofNullable` carries
this risk along with the container itself, and defers the check to the
line where the container gets used.

This deferral is not a defect, it is exactly `Optional`'s job: at the
point `ofNullable` gets called, no one may yet know how the container
will get used — maybe it will go straight into a `map` chain, maybe
it will get assigned to a field, maybe it will never get read at all.
`of` assumes the opposite: it claims, right here, at exactly this
line, that the value is full, and does not wait to confirm this claim.
The difference between the two gives a small answer to the
caller's-obligation question measured at the start of the lesson: the
obligation never disappears, only **when** it falls changes with the
method the caller chooses. Which one to choose is the caller's
decision: if certain the input cannot be null, `of` builds an
early-warning system; if already knowing the input might be null,
`ofNullable` represents this not as an exception, but as an
**ordinary case**.

## Summary

- A null-returning signature and an `Optional`-returning signature are
  equal in the count of calls left unchecked: all four unchecked
  calls compile, one falls at runtime. `Optional` does not reduce
  this count.
- What gets gained is the signature's visibility: the `Optional<T>`
  return type shows the possibility of absence directly in the
  signature, without looking at the documentation.
- `Optional` supports chaining: `map` never runs on an empty
  container, no separate null check needs writing at intermediate
  steps. But `orElse` always computes its argument even if the
  container is full; `orElseGet` only runs when the container is
  empty — though the two look like they do the same work, their costs
  differ.
- Boundary measurement: the gain disappears once `Optional` gets used
  as a field or a parameter — the field itself can be null, and this
  carries the exception meant to be prevented one layer outward.
- `Optional.of(null)` falls with `NullPointerException` at the moment
  of construction; `Optional.ofNullable(null)` does not fall, it
  returns an empty container and the check gets deferred to the
  `get()` call. The choice is the caller's.

## Next Step

Across this topic, what a single type — the functional interface, the
lambda, the stream chain, the collector, `Optional` — promises the
library got measured, and every time, the promise was exactly as
narrow as what the compiler checks: single abstract method gets held,
local capture gets frozen, order mostly comes from the source, the
container's type is mostly ambiguous. `Optional` is the last example
of that same narrow promise — absence got stated as a type, the
obligation got carried into the signature, but the check still stayed
at runtime. The next topic asks the same question no longer over the
language's own types, but at the boundary built with the outside
world: it measures what a byte, a file, a clock, and text promise the
standard library.
