What Is Optional in Java? OrElse vs OrElseGet Explained
Java Optional is a return type for a result that may be absent. Creating one, orElse versus orElseGet, map and flatMap chains, and where it does not belong.
- Course: Java study plan
- Module: Streams
- Kind: Lesson
- Reading time: 13 min
- Runtime: OpenJDK 18
What is Optional in Java?
Optional<T> in Java is a container that holds either one non-null value or nothing, added in Java 8 as the return type for results that may be absent, such as findFirst or a findById lookup. Callers read it with orElse, orElseGet, orElseThrow or ifPresent and chain map and flatMap, so the empty case cannot be forgotten the way null is.
Lesson
Optional<T> is a box that holds one value or nothing. It arrived with streams because findFirst, max and the one-argument reduce needed an honest way to say "there may be no answer", and it has since become the standard return type for lookups that can fail. It is not a general replacement for null, and most of the ugliness in Optional-heavy code comes from using it where a plain value, a plain null or an exception was right. This lesson covers the API, the idioms, and the six anti-patterns interviewers watch for.
Creating one
Optional<String> some = Optional.of("x"); // throws NullPointerException if the argument is null
Optional<String> maybe = Optional.ofNullable(value); // empty if value is null
Optional<String> none = Optional.empty();
of says "this is definitely a value"; ofNullable is the bridge from a nullable API. Reaching for ofNullable everywhere hides bugs — if a value must not be null, let of fail early.
Reading it: the ladder from worst to best
Optional<User> u = repo.findByEmail(email);
if (u.isPresent()) { use(u.get()); } // 1. the null check wearing a costume — avoid
u.ifPresent(this::use); // 2. do something if present
u.ifPresentOrElse(this::use, this::signUpPrompt); // 3. Java 9: either branch
User user = u.orElse(User.GUEST); // 4. a default value
User user = u.orElseGet(User::guest); // 5. a default computed only when needed
User user = u.orElseThrow(); // 6. Java 10: NoSuchElementException if empty
User user = u.orElseThrow(() -> new NotFoundException(email)); // 7. your exception
get() on an empty Optional throws NoSuchElementException — the very failure the type was meant to prevent, so get() is effectively deprecated in style guides; use orElseThrow(), which says the same thing honestly.
orElse versus orElseGet: orElse(expensive()) evaluates expensive() every time, present or not, because arguments are evaluated before the call. orElseGet(() -> expensive()) runs it only when empty. For a constant, orElse; for a method call, orElseGet.
Transforming: map, flatMap, filter, or
Optional<String> city = repo.findByEmail(email)
.map(User::address) // Optional<Address>, or empty
.map(Address::city) // Optional<String>
.filter(c -> !c.isBlank()); // empty if the predicate fails
Optional<String> zip = repo.findByEmail(email)
.flatMap(User::maybeAddress) // maybeAddress returns Optional<Address>: flatMap avoids Optional<Optional<…>>
.map(Address::zip);
Optional<Config> cfg = local().or(() -> remote()); // Java 9: first non-empty wins
map when the function returns a plain value; flatMap when it returns an Optional. This chain is the payoff: five null checks become one readable sentence, and the "empty" travels through automatically.
Optional and streams
Stream<String> zeroOrOne = opt.stream(); // Java 9
List<String> present = optionals.stream().flatMap(Optional::stream).toList(); // drop the empties
Optional.stream() turns a list of Optionals into a stream of the present values in one flatMap — cleaner than filter(Optional::isPresent).map(Optional::get).
The primitive variants
OptionalInt, OptionalLong, OptionalDouble come back from IntStream.max(), average() and friends. They have getAsInt(), orElse, orElseGet, orElseThrow, ifPresent — but no map or filter; convert with boxed() earlier in the pipeline if you need to chain.
Where Optional belongs — and where it does not
Use it as the return type of a method that may legitimately have no result: findById, max, parseOrEmpty, a cache lookup.
Do not use it:
- As a field. It is not
Serializable, doubles the object count, and the class's own methods know how to handle a missing value. A nullable field with a documentedOptional-returning getter is the idiom. - As a method parameter.
void f(Optional<String> name)forces every caller to wrap; overload or acceptnull/a default instead. - Wrapping a collection. An empty list already means "nothing";
Optional<List<T>>has two ways to be empty. isPresent()+get(). That is the null check with more characters.Optional.of(x).orElse(y)on a value you already have.x != null ? x : y— orObjects.requireNonNullElse(x, y).- Storing
Optionalin a collection or map value. Use the absence of the key.
Optional is a return-type tool. When you find one anywhere else, ask what it is protecting against.
Optional and equals
Optional.of("a").equals(Optional.of("a")) is true — equality is by content, and two empties are equal. But Optional is a value-based class: never synchronise on one and never compare with ==.
Interview angle
- "What problem does Optional solve?" An honest return type for "maybe no result", so the caller cannot forget the empty case the way they forget
null. - "
orElseversusorElseGet?" The argument oforElseis always evaluated;orElseGet's supplier only when empty. - "
mapversusflatMapon Optional?"flatMapwhen the function itself returns anOptional. - "Should a field be an Optional?" No — not serialisable, wasteful, and the class controls access anyway; return one from the getter.
- "What does
get()do on empty?" ThrowsNoSuchElementException; preferorElseThrow().
Key takeaways
of(never null),ofNullable(maybe null),empty().- Read with
ifPresent,orElse,orElseGet,orElseThrow— neverisPresent()+get(). - Chain with
map/flatMap/filter/or; the empty propagates for you. Optional::streamin aflatMapdrops the empties from a collection of Optionals.- A return type, not a field, parameter, collection wrapper or null substitute.
Common questions
What is the difference between orElse and orElseGet?
orElse(value) evaluates its argument every time, even when the Optional holds a value, because arguments are evaluated before the call. orElseGet(supplier) runs the supplier only when the Optional is empty. Use orElse for a constant and orElseGet for a method call.
Why is Optional.get() discouraged?
On an empty Optional, get() throws NoSuchElementException — the very failure the type exists to prevent — and isPresent() followed by get() is just a null check with more characters. Use orElseThrow(), which fails the same way but says so, or orElse, orElseGet or ifPresent.
Should Optional be used as a field or method parameter?
No. As a field it is not Serializable and adds an object per value; keep the field nullable and return an Optional from the getter. As a parameter it forces every caller to wrap values; overload the method or accept a default instead.
What is the difference between map and flatMap on Optional?
Use map when the function returns a plain value; the result is wrapped in an Optional for you. Use flatMap when the function itself returns an Optional, so the result is Optional<Address> rather than Optional<Optional<Address>>.
What is the difference between Optional.of and Optional.ofNullable?
Optional.of(x) requires a non-null value and throws NullPointerException if x is null. Optional.ofNullable(x) returns an empty Optional for null, bridging from nullable APIs. Use of when null would be a bug, so it fails early.
Exercises
Lookup ladder
Read an integer n and n lines key value into a map, then an integer m and m query keys. For each query print <key> -> <value> when the key exists and <key> -> (none) otherwise, going through Optional.ofNullable(map.get(key)) — no if, no containsKey. Compute the default with orElseGet and a supplier that increments a counter every time it runs; after the queries print defaults=<counter>. The counter proves that orElseGet is evaluated only for the misses.
Example: 2 then a 1, b 2, then 3 then a x b →
a -> 1
x -> (none)
b -> 2
defaults=1Chain through the empties
Read an integer n and n records name city zip, where - means the field is missing. For each record print <name>: <result>, where the result comes from one Optional chain and no if:
start from Optional.ofNullable(city) (treat - as null), flatMap to the zip (again - is empty), filter to zips of exactly 5 digits, map to "<CITY in upper case> <zip>", and orElse("unknown").
Finally print located=<how many records produced a value> — count them with Optional::stream in a flatMap, not with a counter.
Example: 3 then ann oslo 01234, bob - 99999, cy rome 12 →
ann: OSLO 01234
bob: unknown
cy: unknown
located=1In this module: Streams
- Streams — pipelines over data
- Intermediate operations — map, filter, flatMap and friends
- Terminal operations — reduce, match, find, count
- Collectors — toMap, joining, groupingBy, partitioningBy
- Optional — a return type, not a null replacement (this lesson)
- Primitive streams — IntStream, LongStream, DoubleStream
- Parallel streams, ordering and performance
- Checkpoint — Streams
← Collectors — toMap, joining, groupingBy, partitioningBy · Primitive streams — IntStream, LongStream, DoubleStream →