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 NoneOption.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 nullOption<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 Someoption.isEmpty() // true if NoneYou 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
| Method | When None | Notes |
|---|---|---|
get() | Throws NoSuchElementException | Only safe after an isDefined() check; prefer alternatives. |
getOrElse(fallback) | Returns fallback | Fallback is always evaluated — use getOrElseGet for expensive defaults. |
getOrElseGet(supplier) | Calls supplier | Lazily computed fallback. |
getOrNull() | Returns null | Bridge to null-expecting APIs. Avoid propagating the null further. |
getOrThrow(supplier) | Throws supplied exception | Use when absence is a programming error at that call site. |
fold(onNone, onSome) | Calls onNone | The most expressive extractor: forces you to handle both branches. |
// Preferred: fold makes both branches explicitString display = option.fold( () -> "anonymous", name -> "Hello, " + name);
// Lazy fallback — DB call only when NoneUser user = maybeUser.getOrElseGet(() -> userRepo.defaultUser());
// Throw a domain exception when absence is a bugConfig 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);// NoneflatMap(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 predicatepeek(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 alternativeOption<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 Tuple2Option<Tuple2<String, Integer>> pair = Option.zip(nameOption, ageOption);
// Zip with a custom combinerOption<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 OptionOption<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 semanticsOption<Tuple2<String, Integer>> same = name.flatZip(n -> lookupAge(n));
// Contrast with zip(Option) — the second Option is pre-evaluatedOption<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 regardlessCollecting 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 valuesOptional<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 missingOptional<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
| Conversion | Method |
|---|---|
Option → Optional | option.toOptional() |
Option → Result | option.toResult(errorSupplier) |
Option → Try | option.toTry(exceptionSupplier) |
Option → Either | option.toEither(leftSupplier) |
Result → Option | result.toOption() |
Try → Option | tryVal.toOption() |
Optional → Option | Option.fromOptional(optional) |
// Promote to Result when absence should be an errorResult<User, String> result = findUser(id) .toResult(() -> "User not found: " + id);
// Demote to JDK Optional for older APIsOptional<User> opt = findUser(id).toOptional();See the Combining Types page for the full conversion matrix and composition patterns.
Option vs null vs Optional
null | Optional<T> | Option<T> | |
|---|---|---|---|
| Null-safety | None — NPE at runtime | Good — explicit | Good — explicit |
| Exhaustive handling | No | No (runtime only) | Yes — sealed, switch patterns |
| Use in records / generics | Yes (fragile) | Discouraged (not serializable) | Yes — record-based, serializable |
| Compose with Result / Try / Either | No | No | Yes — first-class conversions |
| Holds null values | Yes | No | No |
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 resultOption<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 nullOption<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.