Bài về Optional ở chặng trước nói nó là kiểu trả về cho phương thức có thể không tìm thấy gì. Bài này về chỗ bạn gặp nó nhiều nhất mà không chọn: cuối một chuỗi Stream.

Bảy phép kết trả về Optional

  findFirst : Optional[Effective Java]
  max       : Optional[Java Concurrency]
  min       : Optional[Java Concurrency]
  reduce    : Optional[Clean Code | Effective Java | Java Concurrency]
  trên stream rỗng: Optional.empty

findFirst, findAny, max, min, reduce một tham số, và average của IntStream — tất cả đều trả về Optional.

Lý do rõ ràng: stream có thể rỗng. Không có Optional thì chúng phải trả null (và bạn quên kiểm) hoặc ném ngoại lệ (và bạn phải bọc try-catch cho một trường hợp bình thường).

Đừng mở hộp ngay, hãy nối tiếp

Phản xạ đầu tiên của nhiều người là lấy giá trị ra càng sớm càng tốt:

Optional<Sach> o = timTheoTen(ten);
if (o.isPresent()) {
    String tacGia = o.get().tacGia().toUpperCase();
    ...
}
  isPresent + get : Clean Code   (chỉ là if viết dài)

Đây là dùng Optional như một cái if — bạn viết dài hơn và không được gì.

Cách đúng là giữ nguyên trong hộp và nối tiếp:

timTheoTen(ten)
    .map(Sach::tacGia)
    .map(String::toUpperCase)
    .orElse("(không tìm thấy)");
  Clean Code   -> MARTIN
  Không Có     -> (không tìm thấy)

Cả hai trường hợp — tìm thấy và không — đi qua cùng một luồng mã, không có nhánh nào. Nếu Optional rỗng thì mọi map phía sau đơn giản là không chạy.

Cách hình dung: Optional là một stream có 0 hoặc 1 phần tử, và map với filter trên nó hoạt động đúng như trên Stream.

filter ngay trên Optional

  sách rẻ hơn 50 : Optional.empty
  sách rẻ hơn 60 : Optional[Effective Java]
timTheoTen("Effective Java").filter(s -> s.gia() < 50).map(Sach::ten)

Tìm thấy sách nhưng không thoả điều kiện thì Optional thành rỗng. Rất gọn cho những câu kiểu "lấy X nếu nó còn hợp lệ".

or: thử nguồn dự phòng

timTheoTen("Không Có")
    .or(() -> timTheoTen("Clean Code"))
    .map(Sach::ten);
    -> tra nguồn thứ hai
  Optional[Clean Code]

or (Java 9) nhận một Supplier<Optional> và chỉ gọi khi cái đầu rỗng — nên nguồn dự phòng chỉ được truy vấn khi cần, đúng nguyên tắc của orElseGet ở bài Optional.

Đây là mẫu rất hợp cho tra cứu nhiều tầng: bộ nhớ đệm trước, rồi CSDL, rồi API ngoài.

tuCache(id).or(() -> tuCsdl(id)).or(() -> tuApi(id)).orElseThrow(...)

Bốn dòng thay cho một chuỗi if lồng ba tầng.

ifPresentOrElse: hai nhánh, không cần if

timTheoTen("Không Có").ifPresentOrElse(
    s -> System.out.println("thấy " + s.ten()),
    () -> System.out.println("không thấy, ghi log"));
  không thấy, ghi log

Có từ Java 9. Dùng khi cả hai nhánh đều là hành động, không phải giá trị — ghi log, gửi thông báo, cập nhật số liệu.

Nếu cả hai nhánh trả về giá trị thì map(...).orElseGet(...) đọc gọn hơn.

Từ nhiều Optional về một Stream

Stream.of("Clean Code", "Không Có", "Effective Java")
      .map(OptStream::timTheoTen)      // Stream<Optional<Sach>>
      .flatMap(Optional::stream)       // Stream<Sach>, bỏ hết cái rỗng
      .map(Sach::ten).toList();
  [Clean Code, Effective Java]

Optional.stream() (Java 9) là cây cầu giữa hai thế giới. Ta đã gặp ở bài flatMap; đây là chỗ nó dùng nhiều nhất trong thực tế — tra cứu hàng loạt rồi bỏ những cái không tìm thấy.

Chiều ngược lại cũng có: stream().findFirst() biến một Stream thành Optional.

Đừng dùng orElseThrow() trần

  orElseThrow() -> No value present

Thông điệp "No value present" chẳng nói gì cho người đọc log lúc hai giờ sáng. Luôn truyền hàm tạo ngoại lệ có ngữ cảnh:

.orElseThrow(() -> new KhongTimThay("không có sách nào trong kho " + maKho))

Đúng nguyên tắc ở bài thiết kế ngoại lệ: thông điệp phải trả lời được cái gì sai, ở đâu.

Vài chỗ hay gặp trong chuỗi Stream

average trả OptionalDouble, không phải Optional<Double>. Cả họ OptionalInt, OptionalLong, OptionalDouble tồn tại để tránh đóng hộp — chúng có getAsInt, getAsDouble thay vì get, và không có map. Cần biến đổi thì .stream().boxed() hoặc dùng isPresent.

reduce hai tham số không trả Optional, vì đã có giá trị khởi tạo:

.reduce(0, Integer::sum)          // int
.reduce(Integer::sum)             // Optional<Integer>

max trên Comparator null-unfriendly sẽ ném NPE nếu có phần tử null trong stream — lọc trước bằng filter(Objects::nonNull).

findAny không phải findFirst. Với stream tuần tự chúng thường cho cùng kết quả, nhưng với song song thì findAny trả về phần tử bất kỳ tìm được sớm nhất — nhanh hơn, và không đoán trước được.

Đừng đưa Optional đi quá xa

Một cảnh báo nhắc lại từ bài trước, vì trong chuỗi Stream rất dễ vi phạm:

Map<String, Optional<Sach>> m = ...;    // đừng
List<Optional<Sach>> ds = ...;          // cũng đừng

Optional sinh ra để trả về từ một phương thức, không phải để lưu trữ. Trong collection, hãy giải quyết nó ngay bằng flatMap(Optional::stream) rồi lưu giá trị trần.

Thử ba mươi giây

Tìm trong dự án của bạn một đoạn có .isPresent() rồi .get() ở dòng sau.

Viết lại bằng .map(...).orElse(...) hoặc .ifPresent(...). Ngắn hơn, và không còn chỗ nào có thể gọi get() nhầm lúc rỗng.

Ngày mai: Stream với dữ liệu lớn — đọc file khổng lồ mà không nổ bộ nhớ, và vì sao tính lười lại là thứ làm được điều đó.