Option<T>

Runnable example: OptionSample.java

What is Option<T>?

Option<T> models a value that may or may not be present. It is a sealed interface with exactly two implementations:

  • Some<T> — holds a non-null value.
  • None<T> — represents absence, with no value.

Unlike a raw null, an Option is explicit in the type system: callers are forced to handle both the present and absent cases. Unlike java.util.Optional, Option is a sealed interface and can be used in switch expressions with exhaustive pattern matching.

Creating instances

Option.some(value)

Wraps a known, non-null value. Throws NullPointerException if the argument is null.

Option<String> name = Option.some("Alice");
// name is Some("Alice")

Option.none()

Returns the singleton absent value.

Option<String> missing = Option.none();
// missing is None

Option.ofNullable(value)

Creates an Option from a value that may be null. Returns None when the argument is null, Some(value) otherwise. This is the most common factory when bridging from legacy code.

String raw = fetchFromLegacyApi(); // may return null
Option<String> result = Option.ofNullable(raw);

Option.fromOptional(optional)

Converts a java.util.Optional into an Option.

Optional<User> jdkOptional = repo.findById(id);
Option<User> dmxOption = Option.fromOptional(jdkOptional);

Converting from Result or Try

Both Result and Try can be narrowed to an Option when you only care about presence:

// Result -> Option (discards the error)
Option<User> user = userResult.toOption();
// Try -> Option (discards the exception)
Option<Config> cfg = tryReadConfig.toOption();

Checking state

Prefer structural operations (fold, map, pattern matching) over imperative state checks, but the predicates are available:

option.isDefined() // true if Some
option.isEmpty() // true if None

You can also use Java pattern matching exhaustively:

switch (option) {
case Option.Some<String>(var v) -> System.out.println("got: " + v);
case Option.None<String> _ -> System.out.println("nothing here");
}

Extracting values

MethodWhen NoneNotes
get()Throws NoSuchElementExceptionOnly safe after an isDefined() check; prefer alternatives.
getOrElse(fallback)Returns fallbackFallback is always evaluated — use getOrElseGet for expensive defaults.
getOrElseGet(supplier)Calls supplierLazily computed fallback.
getOrNull()Returns nullBridge to null-expecting APIs. Avoid propagating the null further.
getOrThrow(supplier)Throws supplied exceptionUse when absence is a programming error at that call site.
fold(onNone, onSome)Calls onNoneThe most expressive extractor: forces you to handle both branches.
// Preferred: fold makes both branches explicit
String display = option.fold(
() -> "anonymous",
name -> "Hello, " + name
);
// Lazy fallback — DB call only when None
User user = maybeUser.getOrElseGet(() -> userRepo.defaultUser());
// Throw a domain exception when absence is a bug
Config cfg = maybeConfig.getOrThrow(
() -> new IllegalStateException("Config not loaded"));

Transforming values

map(mapper)

Applies a function to the value if present. Returns None unchanged. If the mapper returns null, the result becomes None.

Option<String> upper = Option.some("hello").map(String::toUpperCase);
// Some("HELLO")
Option<String> none = Option.<String>none().map(String::toUpperCase);
// None

flatMap(mapper)

Like map, but the mapper itself returns an Option. Use this to chain operations that may also produce absence.

Option<Address> address = findUser(id) // Option<User>
.flatMap(user -> user.getPrimaryAddress()); // returns Option<Address>

filter(predicate)

Keeps the value only if it satisfies the predicate; otherwise returns None.

Option<Integer> positive = Option.some(-5).filter(n -> n > 0);
// None — value fails the predicate

peek(action)

Runs a side-effecting action on the value (e.g., logging) without altering the Option. No-op on None. Returns this for chaining.

Option<Order> order = findOrder(id)
.peek(o -> log.debug("Processing order {}", o.id()))
.filter(Order::isActive);

match(onNone, onSome)

Terminal side-effecting variant of fold. Both branches return void.

option.match(
() -> metrics.increment("cache.miss"),
value -> metrics.increment("cache.hit")
);

Composing Options

Fallback chains with orElse

Return an alternative Option when this one is None. The supplier overload is lazy — use it when computing the alternative is expensive.

// Eager alternative
Option<String> result = primary.orElse(secondary);
// Lazy alternative (computed only when primary is None)
Option<String> result = primary.orElse(() -> computeSecondary());

Zipping two or more Options

Combine multiple independent Option values. Returns None if any input is None.

// Zip to a Tuple2
Option<Tuple2<String, Integer>> pair =
Option.zip(nameOption, ageOption);
// Zip with a custom combiner
Option<Greeting> greeting =
Option.zip(nameOption, localeOption, Greeting::new);

Pairing a value with a derived Option — zipWith / flatZip

zipWith(mapper) applies a function to the current value and pairs the original value with whatever the function returns. If the mapper returns None, the whole result is None. Unlike zip(Option), the second Option is computed lazily from the first value.

flatZip is an alias that may read more naturally at the call site.

// zipWith(Function) — pair the current value with a derived Option
Option<String> name = Option.some("alice");
Option<Tuple2<String, Integer>> result =
name.zipWith(n -> lookupAge(n));
// Some(Tuple2("alice", 30)) — if lookupAge returns Some(30)
// None — if lookupAge returns None or name is None
// flatZip is an alias with the same semantics
Option<Tuple2<String, Integer>> same =
name.flatZip(n -> lookupAge(n));
// Contrast with zip(Option) — the second Option is pre-evaluated
Option<Integer> age = Option.some(30);
Option<Tuple2<String, Integer>> fromZip = name.zip(age);
// Both are None when name is None, but zip always evaluates age regardless

Collecting a list with sequence

Converts a List<Option<T>> into an Option<List<T>>. Returns None as soon as any element is None.

List<Option<User>> lookups = ids.stream()
.map(userRepo::findById)
.toList();
Option<List<User>> allOrNone = Option.sequence(lookups);

Collecting a stream with sequenceCollector

Option.sequenceCollector() is the stream Collector counterpart to sequence. It reduces a Stream<Option<V>> to a single Optional<List<V>> — present only if every element is Some, empty if any element is None.

// All Some → Optional containing all values
Optional<List<String>> result = Stream.of(
Option.some("alice"), Option.some("bob"))
.collect(Option.sequenceCollector());
// Optional.of(["alice", "bob"])
// Any None → Optional.empty()
Optional<List<String>> missing = Stream.of(
Option.some("alice"), Option.<String>none())
.collect(Option.sequenceCollector());
// Optional.empty()
// Empty stream → Optional.of([])
Optional<List<String>> empty = Stream.<Option<String>>empty()
.collect(Option.sequenceCollector());
// Optional.of([])
// Typical use: look up all IDs, fail if any is missing
Optional<List<User>> allFound = ids.stream()
.map(userRepo::findById) // Stream<Option<User>>
.collect(Option.sequenceCollector()); // Optional<List<User>>

Stream integration with stream()

Turns an Option into a one-element or empty stream. Useful for integrating into Stream pipelines without filter + map.

long count = options.stream()
.flatMap(Option::stream) // flatten Nones out
.count();

Interoperability

ConversionMethod
OptionOptionaloption.toOptional()
OptionResultoption.toResult(errorSupplier)
OptionTryoption.toTry(exceptionSupplier)
OptionEitheroption.toEither(leftSupplier)
ResultOptionresult.toOption()
TryOptiontryVal.toOption()
OptionalOptionOption.fromOptional(optional)
// Promote to Result when absence should be an error
Result<User, String> result = findUser(id)
.toResult(() -> "User not found: " + id);
// Demote to JDK Optional for older APIs
Optional<User> opt = findUser(id).toOptional();

See the Combining Types page for the full conversion matrix and composition patterns.

Option vs null vs Optional

nullOptional<T>Option<T>
Null-safetyNone — NPE at runtimeGood — explicitGood — explicit
Exhaustive handlingNoNo (runtime only)Yes — sealed, switch patterns
Use in records / genericsYes (fragile)Discouraged (not serializable)Yes — record-based, serializable
Compose with Result / Try / EitherNoNoYes — first-class conversions
Holds null valuesYesNoNo

Common pitfalls

Nested Options

Option<Option<T>> almost always indicates a mistake. Use flatMap instead of map when the mapper itself returns an Option.

// Bad: creates Option<Option<Address>>
Option<Option<Address>> nested = user.map(User::getAddress);
// Good: flat result
Option<Address> address = user.flatMap(User::getAddress);

Mapper returning null

map treats a null return from the mapper as None. This is intentional for legacy interop, but can be surprising if you expect a non-null.

// Silently becomes None if getName() returns null
Option<String> name = option.map(User::getName);

Calling get() unconditionally

get() throws NoSuchElementException on None. Use getOrElse, fold, or pattern matching instead.

Option as a method parameter

Prefer overloaded methods or a nullable parameter over an Option-typed parameter. Option is designed for return types.

Real-world example

A typical service layer pattern: look up a user, find their primary address, format it, and fall back to a default — all without a single null check or try/catch.

public String resolveShippingLabel(long userId) {
return userRepo.findById(userId) // Option<User>
.flatMap(User::primaryAddress) // Option<Address>
.filter(Address::isVerified) // discard unverified
.map(AddressFormatter::format) // Option<String>
.getOrElse("Default Warehouse, 1 Main St");
}

Each step propagates None silently. No intermediate nulls, no nested conditionals.