Before Java 8, the only way to represent "no value" was null. This led to the most common Java runtime error:
// Classic null-check hell
String name = customer.getMemberCard().getDiscountCode().toUpperCase();
// ☠️ NullPointerException if getMemberCard() or getDiscountCode() returns nullTo avoid it, developers had to write deeply nested null checks:
if (customer.getMemberCard() != null) {
MemberCard card = customer.getMemberCard();
if (card.getDiscountCode() != null) {
String code = card.getDiscountCode().toUpperCase();
}
}This is verbose, error-prone, and hides business logic inside defensive checks.
Optional<T> solves this by making the possibility of absence part of the type system. The compiler forces you to handle the missing case.
✅ Use
Optionalas a return type for methods that might not return a value.
❌ Do NOT useOptionalas a field type, constructor parameter, or method parameter.
// ✅ CORRECT — return type
public Optional<MemberCard> getMemberCard() { ... }
// ❌ WRONG — field type
private Optional<MemberCard> memberCard; // Don't do this
// ❌ WRONG — method parameter
public void process(Optional<Customer> customer) { ... } // Don't do thisOptional<Integer> discount = Optional.of(10);
// Throws NullPointerException if value is null!Optional<MemberCard> card = Optional.ofNullable(memberCard); // safe even if nullOptional<Integer> noDiscount = Optional.empty();int discount = getDiscountPercentage(card).orElse(0);// orElse always evaluates the default expression, even if value is present
// orElseGet only evaluates the supplier when the Optional IS empty
// ⚠️ orElse — fetchFromDB() is ALWAYS called, even if discount is present
int discount = getDiscount().orElse(fetchFromDB());
// ✅ orElseGet — fetchFromDB() is called ONLY when Optional is empty
int discount = getDiscount().orElseGet(() -> fetchFromDB());💡 Real project tip: Always prefer
orElseGetwhen the default is expensive to compute (DB calls, HTTP calls, heavy object creation).
// Bad — generic NPE with no context
Customer customer = findById(id).get(); // ❌ throws NoSuchElementException
// Good — explicit, meaningful exception
Customer customer = findById(id)
.orElseThrow(() -> new CustomerNotFoundException("Customer not found for id: " + id));// If memberCard is present, get its points; otherwise 0
int points = customer.getMemberCard()
.map(MemberCard::getPoints)
.orElse(0);// getDiscountPercentageAsInteger returns Optional<Integer>
// Without flatMap, you'd get Optional<Optional<Integer>>
// flatMap "flattens" it to Optional<Integer>
Optional<Integer> discount = customer.getMemberCard()
.flatMap(this::getDiscountPercentageAsInteger);💡 Rule of thumb: Use
mapwhen the transformation returns a plain value. UseflatMapwhen the transformation returns anotherOptional.
// Get member card only if it has enough points for gold status
Optional<MemberCard> goldCard = customer.getMemberCard()
.filter(card -> card.getPoints() >= 200);This is cleaner than:
// Old way
if (card != null && card.getPoints() >= 200) { ... }customer.getMemberCard()
.ifPresent(card -> sendRewardEmail(card));
// or with method reference
customer.getMemberCard().ifPresent(this::sendRewardEmail);customer.getMemberCard()
.ifPresentOrElse(
card -> sendRewardEmail(card), // present: send email
() -> sendSignupPromotionEmail() // absent: encourage to sign up
);💡 This is much cleaner than
if (optional.isPresent()) { ... } else { ... }.
// Try to find customer in primary DB, fall back to archive DB
Optional<Customer> customer = findInPrimaryDB(id)
.or(() -> findInArchiveDB(id));Approach 1: filter + map (Java 8)
List<Integer> ages = names.stream()
.map(Person::findByName) // Stream<Optional<Person>>
.filter(Optional::isPresent) // keep only present ones
.map(Optional::get) // unwrap
.map(Person::getAge)
.collect(toList());Approach 2: flatMap(Optional::stream) (Java 9+ — preferred)
List<Integer> ages = names.stream()
.map(Person::findByName) // Stream<Optional<Person>>
.flatMap(Optional::stream) // flatten: skips empty optionals
.map(Person::getAge)
.collect(toList());💡
Optional::streamconverts anOptional<T>into aStream<T>with 0 or 1 elements. UsingflatMapwith it is the cleanest way to filter out absent values from a stream.
This is the core pattern from DiscountService.java in this project:
public String getDiscountLine(Customer customer) {
return customer.getMemberCard() // Optional<MemberCard>
.flatMap(this::getDiscountPercentageAsInteger) // Optional<Integer>
.map(d -> customer.getName() + " gets Discount% " + d) // Optional<String>
.orElse(customer.getName() + " gets Discount% 0"); // String (default)
}What this achieves:
- If customer has no card →
getMemberCard()returnsempty()→ falls through toorElse - If card has no qualifying points →
getDiscountPercentageAsIntegerreturnsempty()→ falls through toorElse - If card qualifies → builds the discount message
Zero null checks. Zero if statements. Fully readable business logic.
// ❌ This is just a verbose null check — defeats the purpose of Optional
if (optional.isPresent()) {
String value = optional.get();
process(value);
}
// ✅ Use ifPresent instead
optional.ifPresent(this::process);// ❌ Null defeats the entire purpose
public Optional<MemberCard> getMemberCard() {
return null; // Never do this
}
// ✅ Always return Optional.empty() for absence
public Optional<MemberCard> getMemberCard() {
return Optional.empty();
}// ❌ This always calls expensiveDefault(), even when value is present
return optional.orElse(expensiveDefault());
// ✅ orElseGet is lazy — only called when Optional is empty
return optional.orElseGet(() -> expensiveDefault());// ❌ Never nest Optionals
Optional<Optional<MemberCard>> nested = ...; // Ugly and useless
// ✅ Use flatMap to stay flat
Optional<MemberCard> flat = ...;| Scenario | Method to use |
|---|---|
| Value might be null | Optional.ofNullable(value) |
| Guaranteed non-null | Optional.of(value) |
| No value | Optional.empty() |
| Get value or default | .orElse(defaultValue) |
| Get value or compute default (lazy) | .orElseGet(() -> compute()) |
| Get value or throw exception | .orElseThrow(() -> new Ex(...)) |
| Transform value | .map(fn) |
| Transform where fn returns Optional | .flatMap(fn) |
| Keep value conditionally | .filter(predicate) |
| Side effect if present | .ifPresent(consumer) |
| Handle both present/absent | .ifPresentOrElse(consumer, runnable) |
| Fallback to another Optional | .or(() -> otherOptional) |
| Use in Stream pipeline | .flatMap(Optional::stream) |
| Check presence (use sparingly) | .isPresent() / .isEmpty() |
- Code:
src/main/java/optionaldemoone/—Customer,MemberCard,DiscountService - Tests:
src/test/java/optionaldemoone/DiscountServiceTest.java - Tests:
src/test/java/optionaldemoWithPerson/OptionalScenariosDemoTest.java - Advanced Tests:
src/test/java/optionaldemoWithPerson/AdvancedOptionalScenariosTest.java