Chuyển tới nội dung chính

Optional Class

Mục tiêu bài học

Sau bài này, bạn sẽ:

  • Hiểu được vấn đề NullPointerException và cách Optional giải quyết
  • Sử dụng được các cách tạo Optional: of(), ofNullable(), empty()
  • Nắm được các operations: orElse(), orElseGet(), orElseThrow(), ifPresent()
  • Biết cách dùng map(), flatMap(), filter() với Optional
  • Áp dụng Optional trong method return types và tránh anti-patterns

Bài trước: Method References — Đã học 4 loại method references và cách sử dụng. Bài này sẽ học cách xử lý null-safety theo functional style với Optional.

Vấn đề NullPointerException

NullPointerException (NPE) là một trong những exception phổ biến nhất trong Java, được gọi là "billion-dollar mistake" bởi Tony Hoare (người phát minh ra null reference).

// ❌ Traditional null handling
public class TraditionalNullHandling {
public static String getUserCity(User user) {
if (user != null) {
Address address = user.getAddress();
if (address != null) {
City city = address.getCity();
if (city != null) {
return city.getName();
}
}
}
return "Unknown";
}

public static void main(String[] args) {
User user = findUser("123");
String city = getUserCity(user); // Phải null-check nhiều lần
}
}

Vấn đề:

  • Verbose: Quá nhiều null checks
  • Error-prone: Dễ quên check null
  • Not expressive: Không rõ ràng method có thể return null
  • NPE risk: Một lần quên check → crash

Optional là gì?

Optional<T> là một đối tượng bao bọc (container) có thể chứa hoặc không chứa một giá trị non-null. Nó được giới thiệu từ Java 8 để xử lý an toàn null (null-safety) theo functional style.

import java.util.Optional;

// ✅ With Optional
public class OptionalExample {
public static Optional<String> getUserCity(User user) {
return Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getCity)
.map(City::getName);
}

public static void main(String[] args) {
User user = findUser("123");
String city = getUserCity(user).orElse("Unknown");
}
}
Lợi ích của Optional
  1. Explicit intent: Rõ ràng method có thể không trả về giá trị
  2. Null-safety: Buộc caller phải xử lý trường hợp thiếu giá trị
  3. Functional style: Dễ dàng chain các operations
  4. Readable: Code ngắn gọn, dễ đọc hơn

Tạo Optional

1. Optional.of() - Value chắc chắn non-null

// ✅ Value non-null
String name = "John";
Optional<String> opt1 = Optional.of(name);

// ❌ Value là null → NullPointerException
String nullName = null;
Optional<String> opt2 = Optional.of(nullName); // ❌ Throws NPE
Chú ý

Optional.of() ném NPE nếu value là null. Chỉ dùng khi chắc chắn value non-null.

2. Optional.ofNullable() - Value có thể null

// ✅ Value non-null → Optional with value
String name = "John";
Optional<String> opt1 = Optional.ofNullable(name);

// ✅ Value null → Empty Optional
String nullName = null;
Optional<String> opt2 = Optional.ofNullable(nullName); // ✅ OK, empty Optional
Recommended

Dùng Optional.ofNullable() khi không chắc value có null hay không. Đây là cách an toàn nhất.

3. Optional.empty() - Empty Optional

// Tạo empty Optional
Optional<String> emptyOpt = Optional.empty();

System.out.println(emptyOpt.isPresent()); // false

Bảng tổng hợp

MethodInput null?Kết quảUse Case
of(value)❌ Throws NPEOptional with valueChắc chắn non-null
ofNullable(value)✅ OKOptional with value or emptyKhông chắc null/non-null
empty()N/AEmpty OptionalTạo empty Optional

Kiểm tra giá trị

1. isPresent() - Có giá trị?

Optional<String> opt1 = Optional.of("Hello");
Optional<String> opt2 = Optional.empty();

System.out.println(opt1.isPresent()); // true
System.out.println(opt2.isPresent()); // false

// ❌ Old style (not recommended)
if (opt1.isPresent()) {
String value = opt1.get();
System.out.println(value);
}

2. isEmpty() - Rỗng? (Java 11+)

Optional<String> opt = Optional.empty();

System.out.println(opt.isEmpty()); // true (Java 11+)

// Equivalent to
System.out.println(!opt.isPresent()); // true (Java 8+)

3. get() - Lấy giá trị

Optional<String> opt1 = Optional.of("Hello");
Optional<String> opt2 = Optional.empty();

String value1 = opt1.get(); // ✅ "Hello"

// ❌ get() trên empty Optional → NoSuchElementException
String value2 = opt2.get(); // ❌ Throws NoSuchElementException
Nguy hiểm của get()

get() ném exception nếu Optional empty. Tránh dùng get() trực tiếp, dùng các methods an toàn hơn như orElse(), orElseGet(), ifPresent().

Functional Style Operations

1. orElse() - Default value

Trả về giá trị trong Optional, hoặc default value nếu empty.

Optional<String> opt1 = Optional.of("Hello");
Optional<String> opt2 = Optional.empty();

String value1 = opt1.orElse("Default"); // "Hello"
String value2 = opt2.orElse("Default"); // "Default"

// Ví dụ thực tế
String username = Optional.ofNullable(getUsernameFromDB())
.orElse("Anonymous");
orElse() luôn evaluate — Eager Evaluation Trap

orElse() luôn evaluate default value, ngay cả khi Optional có giá trị. Đây là một trong những lỗi phổ biến nhất khi dùng Optional.

Optional<String> opt = Optional.of("Hello");
String value = opt.orElse(expensiveOperation()); // ❌ expensiveOperation() vẫn chạy!

Vì sao nguy hiểm? Khi default value là một method call tốn kém (database query, API call, heavy computation), nó sẽ luôn được thực thi bất kể Optional có giá trị hay không.

// ❌ BAD: Database query luôn chạy, kể cả khi cache hit
Optional<User> cachedUser = getCachedUser(id);
User user = cachedUser.orElse(queryDatabaseForUser(id)); // Query LUÔN chạy!

// ✅ GOOD: Database query chỉ chạy khi cache miss
User user = cachedUser.orElseGet(() -> queryDatabaseForUser(id)); // Query chỉ chạy nếu cache empty

Benchmark minh họa:

import java.time.Duration;
import java.time.Instant;
import java.util.Optional;

public class OrElseVsOrElseGetBenchmark {
private static String expensiveQuery() {
try {
Thread.sleep(100); // Giả lập database query
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "ExpensiveResult";
}

public static void main(String[] args) {
Optional<String> presentOpt = Optional.of("CachedValue");

// Test orElse() — LUÔN chạy expensive operation
Instant start1 = Instant.now();
for (int i = 0; i < 10; i++) {
String result = presentOpt.orElse(expensiveQuery()); // ❌ expensiveQuery() chạy 10 lần
}
Duration elapsed1 = Duration.between(start1, Instant.now());
System.out.println("orElse(): " + elapsed1.toMillis() + "ms"); // ~1000ms

// Test orElseGet() — KHÔNG chạy vì Optional present
Instant start2 = Instant.now();
for (int i = 0; i < 10; i++) {
String result = presentOpt.orElseGet(() -> expensiveQuery()); // ✅ expensiveQuery() KHÔNG chạy
}
Duration elapsed2 = Duration.between(start2, Instant.now());
System.out.println("orElseGet(): " + elapsed2.toMillis() + "ms"); // ~0ms
}
}

Kết quả:

orElse(): ~1000ms      (expensiveQuery() chạy 10 lần dù không cần)
orElseGet(): ~0ms (expensiveQuery() không chạy vì Optional có giá trị)

Quy tắc đơn giản:

  • orElse(value): Dùng khi default value là literal hoặc constant (orElse(0), orElse("default"))
  • orElseGet(() -> value): Dùng khi default value là method call hoặc cần tính toán

2. orElseGet() - Lazy default value

Trả về giá trị trong Optional, hoặc giá trị từ Supplier nếu empty. Lazy evaluation.

Optional<String> opt1 = Optional.of("Hello");
Optional<String> opt2 = Optional.empty();

String value1 = opt1.orElseGet(() -> "Default"); // "Hello"
String value2 = opt2.orElseGet(() -> "Default"); // "Default"

// Ví dụ với expensive operation
String value = opt.orElseGet(() -> {
System.out.println("Computing default...");
return expensiveOperation(); // ✅ Chỉ chạy khi Optional empty
});

So sánh orElse() vs orElseGet():

MethodEvaluationUse Case
orElse(value)Eager (luôn evaluate)Default value đơn giản, cheap
orElseGet(supplier)Lazy (chỉ evaluate khi cần)Default value expensive, phức tạp

3. orElseThrow() - Throw exception

Trả về giá trị trong Optional, hoặc throw exception nếu empty.

Optional<String> opt1 = Optional.of("Hello");
Optional<String> opt2 = Optional.empty();

// Java 10+: orElseThrow() without argument
String value1 = opt1.orElseThrow(); // "Hello"
String value2 = opt2.orElseThrow(); // ❌ Throws NoSuchElementException

// With custom exception
String value3 = opt2.orElseThrow(() -> new IllegalArgumentException("Value not found"));

// Ví dụ thực tế
User user = findUserById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));

4. ifPresent() - Thực hiện action nếu có giá trị

Optional<String> opt1 = Optional.of("Hello");
Optional<String> opt2 = Optional.empty();

// ifPresent: Consumer<T>
opt1.ifPresent(value -> System.out.println(value)); // Prints "Hello"
opt2.ifPresent(value -> System.out.println(value)); // Không làm gì

// Ví dụ thực tế
findUserById(id).ifPresent(user -> {
System.out.println("Found user: " + user.getName());
user.setLastLogin(LocalDateTime.now());
saveUser(user);
});

5. ifPresentOrElse() - Action nếu có hoặc không có giá trị (Java 9+)

Optional<String> opt1 = Optional.of("Hello");
Optional<String> opt2 = Optional.empty();

// ifPresentOrElse: Consumer<T>, Runnable
opt1.ifPresentOrElse(
value -> System.out.println("Value: " + value), // If present
() -> System.out.println("Empty") // If empty
);

opt2.ifPresentOrElse(
value -> System.out.println("Value: " + value),
() -> System.out.println("Empty") // Prints "Empty"
);

6. map() - Transform giá trị

Transform giá trị trong Optional sang giá trị khác.

Optional<String> opt = Optional.of("hello");

// String → Integer (length)
Optional<Integer> length = opt.map(String::length);
System.out.println(length.orElse(0)); // 5

// String → String (uppercase)
Optional<String> upper = opt.map(String::toUpperCase);
System.out.println(upper.orElse("")); // "HELLO"

// Empty Optional
Optional<String> empty = Optional.empty();
Optional<Integer> emptyLength = empty.map(String::length);
System.out.println(emptyLength.isPresent()); // false

// Ví dụ thực tế
String userName = Optional.ofNullable(user)
.map(User::getName)
.map(String::toUpperCase)
.orElse("UNKNOWN");

7. flatMap() - Transform sang Optional khác

Dùng khi transformation trả về Optional (tránh Optional<Optional<T>>).

class User {
private String name;
private Address address;

public Optional<Address> getAddress() {
return Optional.ofNullable(address);
}
}

class Address {
private String city;

public Optional<String> getCity() {
return Optional.ofNullable(city);
}
}

// ❌ map: Optional<Optional<String>>
Optional<User> userOpt = Optional.of(user);
Optional<Optional<String>> cityOpt1 = userOpt
.map(User::getAddress) // Optional<Optional<Address>>
.map(addr -> addr.getCity()); // Optional<Optional<Optional<String>>> - nested!

// ✅ flatMap: Optional<String>
Optional<String> cityOpt2 = userOpt
.flatMap(User::getAddress) // Optional<Address>
.flatMap(Address::getCity); // Optional<String>

String city = cityOpt2.orElse("Unknown");

So sánh map() vs flatMap():

MethodFunction returnsResultUse Case
map()ROptional<R>Transform sang non-Optional value
flatMap()Optional<R>Optional<R>Transform sang Optional (flatten)

8. filter() - Lọc giá trị

Giữ giá trị nếu thỏa predicate, ngược lại trả về empty Optional.

Optional<Integer> opt = Optional.of(42);

// filter: giữ nếu thỏa điều kiện
Optional<Integer> evenOpt = opt.filter(n -> n % 2 == 0);
System.out.println(evenOpt.isPresent()); // true

Optional<Integer> oddOpt = opt.filter(n -> n % 2 != 0);
System.out.println(oddOpt.isPresent()); // false (filtered out)

// Ví dụ thực tế
String username = Optional.ofNullable(user)
.filter(u -> u.isActive()) // Chỉ giữ active users
.filter(u -> u.getAge() >= 18) // Chỉ giữ adults
.map(User::getName)
.orElse("Invalid user");

9. or() - Alternative Optional (Java 9+)

Trả về Optional hiện tại nếu có giá trị, ngược lại trả về alternative Optional.

Optional<String> opt1 = Optional.of("Hello");
Optional<String> opt2 = Optional.empty();

// or: Supplier<Optional<T>>
Optional<String> result1 = opt1.or(() -> Optional.of("Alternative"));
System.out.println(result1.orElse("")); // "Hello"

Optional<String> result2 = opt2.or(() -> Optional.of("Alternative"));
System.out.println(result2.orElse("")); // "Alternative"

// Ví dụ thực tế: fallback chain
Optional<User> user = findInCache(id)
.or(() -> findInDatabase(id))
.or(() -> findInBackupSystem(id));

10. stream() - Chuyển Optional thành Stream (Java 9+)

Chuyển Optional thành Stream có 0 hoặc 1 phần tử. Rất hữu ích khi kết hợp Optional với Stream API:

Optional<String> opt1 = Optional.of("Hello");
Optional<String> opt2 = Optional.empty();

opt1.stream().forEach(System.out::println); // "Hello"
opt2.stream().forEach(System.out::println); // Không in gì

// ✅ Use case thực tế: Lọc Optional trong Stream
List<Optional<String>> optionals = List.of(
Optional.of("Java"),
Optional.empty(),
Optional.of("Python"),
Optional.empty(),
Optional.of("Go")
);

// Trước Java 9: phức tạp
List<String> values1 = optionals.stream()
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());

// Java 9+: thanh lịch hơn
List<String> values2 = optionals.stream()
.flatMap(Optional::stream) // Optional → Stream (0 or 1 element)
.collect(Collectors.toList());
// Kết quả: [Java, Python, Go]

OptionalInt, OptionalLong, OptionalDouble

Java cung cấp 3 lớp primitive-specialized Optional để tránh boxing overhead khi làm việc với kiểu nguyên thủy:

  • OptionalInt — cho int
  • OptionalLong — cho long
  • OptionalDouble — cho double

Tại sao cần primitive Optional?

// ❌ Optional<Integer> — boxing overhead
Optional<Integer> boxed = Optional.of(42); // int → Integer (boxing)
int value = boxed.get(); // Integer → int (unboxing)

// ✅ OptionalInt — no boxing
OptionalInt primitive = OptionalInt.of(42); // Trực tiếp lưu int
int value = primitive.getAsInt(); // Trực tiếp lấy int

Khác biệt với Optional<T>

Điểm khác biệt quan trọng

OptionalInt/Long/Double KHÔNG extend Optional — chúng là các lớp hoàn toàn riêng biệt. Vì vậy:

  • Không có map(), flatMap(), filter() methods
  • Chỉ có các methods cơ bản: isPresent(), isEmpty(), getAsInt()/getAsLong()/getAsDouble(), orElse(), orElseGet(), ifPresent()

API so sánh

MethodOptional<Integer>OptionalInt
TạoOptional.of(42)OptionalInt.of(42)
Kiểm traisPresent(), isEmpty()isPresent(), isEmpty()
Lấy giá trịget()getAsInt()
DefaultorElse(0)orElse(0)
Default lazyorElseGet(() -> 0)orElseGet(() -> 0)
Throw exceptionorElseThrow()orElseThrow()
ActionifPresent(v -> {...})ifPresent(v -> {...})
Transformmap(v -> v * 2)❌ Không có
Filterfilter(v -> v > 0)❌ Không có
FlatMapflatMap(...)❌ Không có

Ví dụ sử dụng

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

public class PrimitiveOptionalExample {
public static void main(String[] args) {
// 1. Tạo OptionalInt từ Stream
List<Person> people = List.of(
new Person("Alice", 25),
new Person("Bob", 30),
new Person("Charlie", 20)
);

OptionalInt maxAge = people.stream()
.mapToInt(Person::getAge) // IntStream
.max(); // OptionalInt

maxAge.ifPresent(age -> System.out.println("Max age: " + age)); // 30

// 2. OptionalDouble từ average()
OptionalDouble avgAge = people.stream()
.mapToInt(Person::getAge)
.average();

double avg = avgAge.orElse(0.0);
System.out.println("Average age: " + avg); // 25.0

// 3. orElseGet() với IntSupplier
OptionalInt emptyOpt = OptionalInt.empty();
int value = emptyOpt.orElseGet(() -> {
System.out.println("Computing default...");
return 42;
});

// 4. orElseThrow() với custom exception
OptionalInt result = findMaxScore();
int score = result.orElseThrow(() -> new IllegalStateException("No scores found"));
}

static OptionalInt findMaxScore() {
int[] scores = {85, 92, 78, 95};
return IntStream.of(scores).max();
}
}

class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public int getAge() { return age; }
}

Hạn chế và lưu ý

// ❌ KHÔNG thể dùng OptionalInt thay cho Optional<Integer>
public void processValue(Optional<Integer> opt) { /* ... */ }

OptionalInt primitiveOpt = OptionalInt.of(42);
processValue(primitiveOpt); // ❌ Compile error! Type mismatch

// ✅ Phải convert: OptionalInt → Optional<Integer>
Optional<Integer> boxedOpt = primitiveOpt.isPresent()
? Optional.of(primitiveOpt.getAsInt())
: Optional.empty();
processValue(boxedOpt); // ✅ OK

// Hoặc dùng stream (Java 9+)
Optional<Integer> boxedOpt2 = primitiveOpt.stream()
.boxed()
.findFirst();

Khi nào dùng primitive Optional?

  • Khi làm việc với Stream operations trên primitive streams (IntStream, LongStream, DoubleStream)
  • Khi performance quan trọng và muốn tránh boxing overhead
  • Khi chỉ cần các operations đơn giản (không cần map(), flatMap(), filter())

Khi nào dùng Optional<Integer/Long/Double>?

  • Khi cần functional operations như map(), flatMap(), filter()
  • Khi API yêu cầu Optional<T> (không thể dùng primitive Optional)
  • Khi boxing overhead không đáng kể (ít operations)

Chaining Optional Operations

import java.util.Optional;

class User {
private String name;
private Address address;

public String getName() { return name; }
public Optional<Address> getAddress() { return Optional.ofNullable(address); }
}

class Address {
private String street;
private City city;

public String getStreet() { return street; }
public Optional<City> getCity() { return Optional.ofNullable(city); }
}

class City {
private String name;
private String zipCode;

public String getName() { return name; }
public String getZipCode() { return zipCode; }
}

public class OptionalChainingExample {
public static void main(String[] args) {
Optional<User> userOpt = findUser("123");

// Chain operations
String cityName = userOpt
.flatMap(User::getAddress) // Optional<Address>
.flatMap(Address::getCity) // Optional<City>
.map(City::getName) // Optional<String>
.filter(name -> !name.isEmpty()) // Filter empty names
.map(String::toUpperCase) // Transform to uppercase
.orElse("UNKNOWN"); // Default value

System.out.println("City: " + cityName);

// With side effects
userOpt
.flatMap(User::getAddress)
.ifPresent(address -> {
System.out.println("Street: " + address.getStreet());
address.getCity().ifPresent(city -> {
System.out.println("City: " + city.getName());
System.out.println("Zip: " + city.getZipCode());
});
});
}
}

Optional trong thiết kế API

Khi nào nên trả về Optional, khi nào trả về null, và khi nào ném exception?

Hướng dẫn thiết kế

Tình huốngCách xử lýLý do
Kết quả tìm kiếm có thể không cóOptional<T>Caller buộc phải xử lý trường hợp rỗng
Lỗi nghiệp vụ (user not found)Optional<T> hoặc exceptionTùy ngữ cảnh: query → Optional, command → exception
Lỗi lập trình (null argument)NullPointerException / IllegalArgumentExceptionFail-fast, bug phải sửa
Collection rỗngTrả về collection rỗng, KHÔNG OptionalCollections.emptyList() thay vì Optional<List<T>>
Kiểu nguyên thủyOptionalInt, OptionalLong, OptionalDoubleTránh boxing
// ✅ Tốt: Optional cho single-value lookup
public Optional<User> findById(String id) { ... }

// ✅ Tốt: Collection rỗng cho multi-value lookup
public List<User> findByDepartment(String dept) { ... } // Trả về empty list, không Optional

// ✅ Tốt: Exception cho precondition violations
public User getById(String id) {
return findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}

// ❌ Xấu: Optional wrapping collection
public Optional<List<User>> findUsers() { ... } // Không cần — dùng empty list

Joshua Bloch's rule (Effective Java)

"Không bao giờ trả về null từ method có return type Optional."

Nếu method trả về Optional<T>, hãy luôn trả về Optional.of(value) hoặc Optional.empty() — KHÔNG BAO GIỜ trả về null.

// ❌ TUYỆT ĐỐI KHÔNG
public Optional<String> getName() {
return null; // Phá vỡ hợp đồng của Optional!
}

// ✅ Đúng
public Optional<String> getName() {
return Optional.ofNullable(name);
}

Optional trong Method Return Types

✅ DO - Dùng Optional cho return types

// ✅ Good: Rõ ràng method có thể không tìm thấy user
public Optional<User> findUserById(String id) {
User user = database.query(id);
return Optional.ofNullable(user);
}

// Usage
findUserById("123")
.ifPresent(user -> System.out.println(user.getName()));

// ✅ Good: Chaining
public Optional<String> getUserEmail(String userId) {
return findUserById(userId)
.map(User::getEmail);
}

❌ DON'T - Các anti-patterns

Anti-patterns chi tiết

1. Optional.get() không kiểm tra — Lỗi phổ biến nhất

// ❌ Nguy hiểm như dùng null mà không kiểm tra
Optional<User> userOpt = findUser(id);
User user = userOpt.get(); // NoSuchElementException nếu empty!

// ✅ Dùng orElseThrow() — tường minh hơn
User user = userOpt.orElseThrow(() -> new UserNotFoundException(id));

IntelliJ IDEA và SonarQube đều cảnh báo khi gặp .get() không có .isPresent(). Từ Java 10, orElseThrow() không tham số là cách thay thế rõ ràng hơn cho .get().

2. Optional làm tham số method

// ❌ Buộc caller phải tạo Optional
public void sendEmail(Optional<String> address) { ... }
// Caller: sendEmail(Optional.ofNullable(email));

// ✅ Overload hoặc dùng @Nullable
public void sendEmail(String address) { ... }
public void sendEmail() { /* default behavior */ }

Lý do: Optional được thiết kế cho return type, không phải parameter. Khi dùng làm parameter, bạn thêm một lớp wrapping không cần thiết và buộc caller phải biết tạo Optional.

3. Optional làm field trong class

// ❌ Optional không implement Serializable
public class User {
private Optional<String> middleName; // Không serialize được!
}

// ✅ Field nullable, getter trả về Optional
public class User {
private String middleName; // Có thể null

public Optional<String> getMiddleName() {
return Optional.ofNullable(middleName);
}
}

4. Stream<Optional<T>> — Dùng flatMap thay vì filter + map

Khi bạn có một Stream<Optional<T>> (ví dụ từ .map() trả về Optional), đừng dùng .filter(Optional::isPresent).map(Optional::get) — dùng .flatMap(Optional::stream) (Java 9+):

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

public class StreamOptionalAntiPattern {
// Method trả về Optional
static Optional<String> findEmail(String username) {
Map<String, String> emailDb = Map.of(
"alice", "[email protected]",
"bob", "[email protected]"
);
return Optional.ofNullable(emailDb.get(username));
}

public static void main(String[] args) {
List<String> usernames = List.of("alice", "charlie", "bob", "david");

// Stream<Optional<String>> — một số users không có email
Stream<Optional<String>> optionalEmails = usernames.stream()
.map(StreamOptionalAntiPattern::findEmail);

// ❌ Verbose và dễ lỗi
List<String> emails1 = usernames.stream()
.map(StreamOptionalAntiPattern::findEmail)
.filter(Optional::isPresent) // Giữ Optional có giá trị
.map(Optional::get) // Unwrap Optional (nguy hiểm nếu filter sai)
.collect(Collectors.toList());

// ✅ Clean với flatMap (Java 9+)
List<String> emails2 = usernames.stream()
.map(StreamOptionalAntiPattern::findEmail)
.flatMap(Optional::stream) // Optional → Stream (0 or 1 element)
.collect(Collectors.toList());

System.out.println(emails2); // [[email protected], [email protected]]
}
}

Giải thích:

  • Optional.stream() (Java 9+) chuyển Optional<T> thành Stream<T> có 0 (nếu empty) hoặc 1 phần tử (nếu present)
  • flatMap(Optional::stream) flatten Stream<Optional<T>> thành Stream<T>, tự động loại bỏ empty Optionals
  • Code ngắn gọn hơn và tránh nguy cơ dùng .get() sau khi filter

Ví dụ khác: Parse integers an toàn

List<String> inputs = List.of("123", "abc", "456", "xyz", "789");

// ❌ Try-catch lộn xộn
List<Integer> numbers1 = inputs.stream()
.map(s -> {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.<Integer>empty();
}
})
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());

// ✅ Clean với flatMap + stream()
List<Integer> numbers2 = inputs.stream()
.map(s -> {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.<Integer>empty();
}
})
.flatMap(Optional::stream)
.collect(Collectors.toList());

System.out.println(numbers2); // [123, 456, 789]

Tóm tắt:

  • Stream<Optional<T>> + .filter(Optional::isPresent).map(Optional::get) → ❌ Verbose
  • Stream<Optional<T>> + .flatMap(Optional::stream) → ✅ Clean (Java 9+)

Best Practices

✅ DO

// 1. Use Optional for return types
public Optional<User> findUser(String id) { /* ... */ }

// 2. Chain operations functionally
String city = userOpt
.flatMap(User::getAddress)
.map(Address::getCity)
.orElse("Unknown");

// 3. Use orElseGet() for expensive defaults
user.orElseGet(() -> createDefaultUser());

// 4. Use ifPresent() for side effects
userOpt.ifPresent(user -> sendEmail(user));

// 5. Filter before mapping
userOpt
.filter(User::isActive)
.map(User::getEmail)
.ifPresent(this::sendEmail);

❌ DON'T

// 1. Don't use get() without check
userOpt.get(); // ❌ Dangerous!

// 2. Don't use isPresent() + get()
if (userOpt.isPresent()) { // ❌ Old style
User user = userOpt.get();
// ...
}
// ✅ Better:
userOpt.ifPresent(user -> {
// ...
});

// 3. Don't return null from Optional-returning method
public Optional<User> findUser(String id) {
return null; // ❌ Never!
}
// ✅ Better:
return Optional.empty();

// 4. Don't use Optional.of() with nullable values
Optional<String> opt = Optional.of(nullableValue); // ❌ Can throw NPE
// ✅ Better:
Optional<String> opt = Optional.ofNullable(nullableValue);

Ví dụ thực tế

1. Configuration Reader

import java.util.*;

public class ConfigReader {
private Map<String, String> config = new HashMap<>();

public ConfigReader() {
config.put("app.name", "MyApp");
config.put("app.version", "1.0.0");
// "app.port" not set
}

public Optional<String> getProperty(String key) {
return Optional.ofNullable(config.get(key));
}

public String getPropertyOrDefault(String key, String defaultValue) {
return getProperty(key).orElse(defaultValue);
}

public int getIntProperty(String key, int defaultValue) {
return getProperty(key)
.map(Integer::parseInt)
.orElse(defaultValue);
}

public static void main(String[] args) {
ConfigReader reader = new ConfigReader();

// Get property with default
String appName = reader.getProperty("app.name").orElse("Unknown");
System.out.println("App: " + appName); // "MyApp"

// Get missing property
int port = reader.getIntProperty("app.port", 8080);
System.out.println("Port: " + port); // 8080

// Chain operations
reader.getProperty("app.version")
.filter(v -> v.startsWith("1."))
.ifPresent(v -> System.out.println("Version 1.x: " + v));
}
}

2. User Service

import java.util.*;

class User {
private String id;
private String name;
private String email;

// Constructor, getters...
}

class UserService {
private Map<String, User> users = new HashMap<>();

public Optional<User> findById(String id) {
return Optional.ofNullable(users.get(id));
}

public Optional<String> getUserEmail(String userId) {
return findById(userId)
.map(User::getEmail)
.filter(email -> !email.isEmpty());
}

public void sendEmailToUser(String userId, String message) {
findById(userId)
.map(User::getEmail)
.filter(email -> email.contains("@"))
.ifPresentOrElse(
email -> System.out.println("Sending email to: " + email),
() -> System.out.println("Cannot send email: user not found or invalid email")
);
}

public User getUserOrCreateDefault(String userId) {
return findById(userId)
.orElseGet(() -> {
System.out.println("User not found, creating default...");
return new User(userId, "Guest", "[email protected]");
});
}
}
OCP Exam Tips
  • orElse() LUÔN evaluate argument, orElseGet() chỉ evaluate khi empty — đề thi hay hỏi điều này qua side effect:
Optional.of("value").orElse(expensiveCall());     // expensiveCall() VẪN CHẠY!
Optional.of("value").orElseGet(() -> expensiveCall()); // expensiveCall() KHÔNG chạy
  • Optional.of(null) ném NullPointerException — dùng ofNullable() khi không chắc
  • flatMap() dùng khi mapper trả về Optional<T>, map() dùng khi mapper trả về T
  • Optional không implement Serializable — không nên dùng làm field
  • orElseThrow() không tham số (Java 10+) ném NoSuchElementException — giống get() nhưng rõ ý đồ hơn

Tóm tắt

  • Optional là container for nullable values, tránh NullPointerException
  • Tạo Optional: of() (non-null), ofNullable() (nullable), empty()
  • Check: isPresent(), isEmpty() (Java 11+)
  • Get value: orElse(), orElseGet() (lazy), orElseThrow()
  • Functional operations: map(), flatMap(), filter()
  • Side effects: ifPresent(), ifPresentOrElse() (Java 9+)
  • Best practices: Dùng cho return types, tránh get() trực tiếp, không dùng cho parameters/fields

Bài tập

Bài 1: Optional Basics

// 1. Tạo Optional từ nullable value
String nullableValue = getNullableValue();
Optional<String> opt = // TODO

// 2. Get value với default
String value = // TODO: get value hoặc "default"

// 3. Transform to uppercase nếu có giá trị
Optional<String> upper = // TODO

Bài 2: Optional Chaining

class Order {
private Customer customer;
public Optional<Customer> getCustomer() { return Optional.ofNullable(customer); }
}

class Customer {
private Address address;
public Optional<Address> getAddress() { return Optional.ofNullable(address); }
}

class Address {
private String city;
public String getCity() { return city; }
}

// TODO: Lấy city name từ order, hoặc "Unknown" nếu không có
Optional<Order> orderOpt = findOrder("123");
String city = // Complete this chain

Bài 3: Refactor với Optional

Refactor code sau dùng Optional:

public String getUserEmail(String userId) {
User user = findUser(userId);
if (user != null) {
String email = user.getEmail();
if (email != null && !email.isEmpty()) {
return email.toLowerCase();
}
}
return "[email protected]";
}

// TODO: Refactor dùng Optional

Bài tiếp theo: Method References →

Đọc thêm

Tài liệu chính thức

Sách và bài viết

  • Effective Java (3rd Edition) — Joshua Bloch
    • Item 55: Return optionals judiciously — Hướng dẫn khi nào dùng Optional, anti-patterns, và best practices
  • Modern Java in Action — Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft
    • Chapter 11: Optional as a better alternative to null — Chi tiết về Optional patterns và use cases

Cross-references

OpenJDK Enhancement Proposals