Skip to main content

Ch 14.3 The Optional Class

One of the most notorious errors in Java development is the NullPointerException (NPE). It occurs when you try to access a field or invoke a method on an object reference that is null.

Java 8 introduced Optional<T> — a container (wrapper) class that safely handles potentially null values. It is especially useful for stream pipeline results that might be empty, and for single values that may be absent.

1. Creating Optional Objects

Optional objects are created using static factory methods, not the new keyword.

import java.util.Optional;

public class OptionalCreationExample {
public static void main(String[] args) {
// 1. When you are certain the value is not null
Optional<String> opt1 = Optional.of("Hello!");
System.out.println(opt1); // Optional[Hello!]

// 2. When the value might be null (most common usage)
String nullableStr = null;
Optional<String> opt2 = Optional.ofNullable(nullableStr);
System.out.println(opt2); // Optional.empty

String realStr = "Java";
Optional<String> opt3 = Optional.ofNullable(realStr);
System.out.println(opt3); // Optional[Java]

// 3. Explicitly creating an empty Optional
Optional<String> opt4 = Optional.empty();
System.out.println(opt4); // Optional.empty

// Check presence
System.out.println(opt1.isPresent()); // true
System.out.println(opt2.isPresent()); // false
System.out.println(opt2.isEmpty()); // true (Java 11+)
}
}
warning

Never use Optional.of(value) when value might be null. It will immediately throw a NullPointerException. Use Optional.ofNullable(value) when null is possible.

2. Retrieving Values Safely

Using plain .get() defeats the purpose of Optional — if the value is absent, it throws NoSuchElementException. Use these safe retrieval methods instead.

Providing a Default Value: orElse() and orElseGet()

import java.util.Optional;

public class OrElseExample {
public static void main(String[] args) {
String name = null;
Optional<String> optName = Optional.ofNullable(name);

// orElse(default): returns value if present, otherwise returns the given default
String result1 = optName.orElse("Guest");
System.out.println(result1); // Guest

// The default expression in orElse() is ALWAYS evaluated, even if value is present
String result2 = Optional.of("Alice").orElse(expensiveDefault());
// expensiveDefault() is called even though "Alice" is present!

// orElseGet(Supplier): default is only evaluated when value is absent (lazy)
String result3 = optName.orElseGet(() -> "Guest user from DB");
System.out.println(result3); // Guest user from DB

// Best practice: use orElseGet for expensive default computations
String result4 = Optional.of("Alice").orElseGet(() -> expensiveDefault());
// expensiveDefault() is NOT called because "Alice" is present
System.out.println(result4); // Alice
}

static String expensiveDefault() {
System.out.println("Computing expensive default...");
return "DefaultUser";
}
}

Throwing an Exception When Absent: orElseThrow()

Use this when an absent value is an error condition. Commonly used in REST API services when a resource is not found.

import java.util.Optional;

public class OrElseThrowExample {
record User(int id, String name, String email) {}

static Optional<User> findUserById(int id) {
if (id == 1) return Optional.of(new User(1, "Alice", "alice@example.com"));
return Optional.empty();
}

public static void main(String[] args) {
// User found
User user = findUserById(1)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
System.out.println("Found: " + user.name()); // Found: Alice

// User not found -> exception thrown
try {
User missing = findUserById(99)
.orElseThrow(() -> new IllegalArgumentException("User not found: id=99"));
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage()); // Error: User not found: id=99
}

// Java 10+: orElseThrow() with no argument throws NoSuchElementException
// User missing2 = findUserById(99).orElseThrow();
}
}

Execute Only When Present: ifPresent() and ifPresentOrElse()

import java.util.Optional;

public class IfPresentExample {
public static void main(String[] args) {
Optional<String> email = Optional.ofNullable("admin@example.com");
Optional<String> noEmail = Optional.empty();

// ifPresent(Consumer): runs the lambda only if value is present
email.ifPresent(e -> System.out.println("Sending email to: " + e));
// Sending email to: admin@example.com

noEmail.ifPresent(e -> System.out.println("This will not print"));
// (nothing printed)

// Java 9+: ifPresentOrElse(Consumer, Runnable)
email.ifPresentOrElse(
e -> System.out.println("Email: " + e),
() -> System.out.println("No email address found")
);
// Email: admin@example.com

noEmail.ifPresentOrElse(
e -> System.out.println("Email: " + e),
() -> System.out.println("No email address found")
);
// No email address found
}
}

3. Transforming Optional Values: map() and flatMap()

map() — Transform the Value Inside Optional

import java.util.Optional;

public class OptionalMapExample {
record User(String name, String email) {}

public static void main(String[] args) {
Optional<User> optUser = Optional.of(new User("Alice", "alice@example.com"));
Optional<User> emptyUser = Optional.empty();

// map: transform the value if present, otherwise return empty Optional
Optional<String> optName = optUser.map(User::name);
System.out.println(optName); // Optional[Alice]

Optional<String> emptyName = emptyUser.map(User::name);
System.out.println(emptyName); // Optional.empty

// Chain map operations
Optional<Integer> nameLength = optUser
.map(User::name)
.map(String::length);
System.out.println(nameLength); // Optional[5]

// map with orElse
String email = optUser
.map(User::email)
.map(String::toUpperCase)
.orElse("NO EMAIL");
System.out.println(email); // ALICE@EXAMPLE.COM
}
}

flatMap() — For Nested Optionals

import java.util.Optional;

public class OptionalFlatMapExample {
record Address(String city, String zipCode) {}
record User(String name, Optional<Address> address) {}

public static void main(String[] args) {
User alice = new User("Alice", Optional.of(new Address("Seoul", "04566")));
User bob = new User("Bob", Optional.empty()); // no address

// Without flatMap: would return Optional<Optional<Address>>
// Optional<Optional<Address>> wrong = Optional.of(alice).map(User::address);

// With flatMap: returns Optional<Address> (flattened)
Optional<String> aliceCity = Optional.of(alice)
.flatMap(User::address)
.map(Address::city);
System.out.println(aliceCity); // Optional[Seoul]

Optional<String> bobCity = Optional.of(bob)
.flatMap(User::address)
.map(Address::city);
System.out.println(bobCity); // Optional.empty
}
}

4. Filtering Optional Values: filter()

import java.util.Optional;

public class OptionalFilterExample {
public static void main(String[] args) {
Optional<Integer> score = Optional.of(85);

// filter: if condition is true, keep the Optional; otherwise return empty
Optional<Integer> passing = score.filter(s -> s >= 60);
System.out.println(passing); // Optional[85]

Optional<Integer> failing = Optional.of(45).filter(s -> s >= 60);
System.out.println(failing); // Optional.empty

// Chaining filter + map + orElse
String grade = Optional.of(92)
.filter(s -> s >= 0 && s <= 100)
.map(s -> {
if (s >= 90) return "A";
if (s >= 80) return "B";
if (s >= 70) return "C";
return "F";
})
.orElse("Invalid score");
System.out.println("Grade: " + grade); // Grade: A
}
}

5. Optional as a Stream: stream()

Java 9+ allows converting an Optional to a Stream, useful when working with stream pipelines.

import java.util.*;
import java.util.stream.*;

public class OptionalStreamExample {
static Optional<String> findName(int id) {
Map<Integer, String> db = Map.of(1, "Alice", 2, "Bob", 3, "Charlie");
return Optional.ofNullable(db.get(id));
}

public static void main(String[] args) {
List<Integer> ids = List.of(1, 99, 2, 42, 3);

// Without stream(): verbose null checking
List<String> names1 = new ArrayList<>();
for (int id : ids) {
Optional<String> opt = findName(id);
if (opt.isPresent()) {
names1.add(opt.get());
}
}

// With stream(): clean and concise
List<String> names2 = ids.stream()
.map(OptionalStreamExample::findName)
.flatMap(Optional::stream) // convert Optional to Stream (empty Optional -> empty stream)
.collect(Collectors.toList());
System.out.println(names2); // [Alice, Bob, Charlie]
}
}

6. Practical Example: Safe Data Processing Pipeline

import java.util.*;
import java.util.stream.*;

public class OptionalPracticalExample {
record Product(String name, String category, Integer discountPercent) {}

static List<Product> products = List.of(
new Product("Laptop Pro", "Electronics", 10),
new Product("Desk Lamp", "Furniture", null), // no discount
new Product("Keyboard", "Electronics", 5),
new Product("Coffee Mug", "Kitchen", null),
new Product("Monitor", "Electronics", 15)
);

// Find product by name
static Optional<Product> findProduct(String name) {
return products.stream()
.filter(p -> p.name().equals(name))
.findFirst();
}

// Calculate discounted price
static Optional<Double> getDiscountedPrice(String productName, double basePrice) {
return findProduct(productName)
.map(Product::discountPercent) // Optional<Integer> (may be null -> empty)
.filter(discount -> discount > 0) // ignore zero discounts
.map(discount -> basePrice * (1 - discount / 100.0));
}

public static void main(String[] args) {
// Safe chaining with Optional
String result1 = findProduct("Laptop Pro")
.map(p -> p.name() + " [" + p.category() + "]")
.orElse("Product not found");
System.out.println(result1); // Laptop Pro [Electronics]

String result2 = findProduct("Unknown Item")
.map(p -> p.name() + " [" + p.category() + "]")
.orElse("Product not found");
System.out.println(result2); // Product not found

// Get discount
getDiscountedPrice("Laptop Pro", 1_000_000)
.ifPresent(price -> System.out.printf("Discounted price: %,.0f%n", price));
// Discounted price: 900,000

getDiscountedPrice("Desk Lamp", 50_000)
.ifPresentOrElse(
price -> System.out.printf("Discounted price: %,.0f%n", price),
() -> System.out.println("Desk Lamp has no discount")
);
// Desk Lamp has no discount

// Collect all discounted products as strings
List<String> discountedProducts = products.stream()
.map(p -> Optional.ofNullable(p.discountPercent())
.map(d -> p.name() + " (-" + d + "%)")
.orElse(null))
.filter(Objects::nonNull)
.collect(Collectors.toList());
System.out.println("Products with discount: " + discountedProducts);
// Products with discount: [Laptop Pro (-10%), Keyboard (-5%), Monitor (-15%)]
}
}

7. Best Practices and Anti-Patterns

What to Do

// Return type of a method that might return no result
public Optional<User> findUserByEmail(String email) {
// ... database lookup
return Optional.ofNullable(result);
}

// Safe value access
String name = findUserByEmail("test@example.com")
.map(User::getName)
.orElse("Unknown");

What to Avoid

// Anti-pattern 1: Using Optional as a field
class User {
private Optional<String> email; // BAD: unnecessary overhead, serialization issues
private String email; // GOOD: use null or empty string instead
}

// Anti-pattern 2: Using Optional as a method parameter
void sendEmail(Optional<String> email) {} // BAD: complicates callers
void sendEmail(String email) {} // GOOD: let the caller decide

// Anti-pattern 3: Calling .get() without checking
Optional<String> opt = Optional.empty();
String value = opt.get(); // NoSuchElementException! Same as raw null

// Anti-pattern 4: Using isPresent() + get() (defeats the purpose)
if (opt.isPresent()) {
String val = opt.get(); // verbose, no better than null check
}
// Better:
opt.ifPresent(val -> System.out.println(val));
String val = opt.orElse("default");
tip

Pro tip: Use Optional only as a return type for methods that may legitimately produce no result. It acts as an explicit contract saying "this method might return nothing." For fields and method parameters, use null or a sentinel value with clear documentation instead.