Stream & Optional API Enhancements (Java 9–11)
Stream API — New Methods (Java 9)
Java 9 added four new instance methods to Stream<T>. All are terminal-ish or intermediate operations that make common patterns expressible without workarounds.
takeWhile(Predicate)
Returns elements from the beginning of the stream as long as the predicate holds, then stops. The first element that fails the predicate (and all subsequent elements) are discarded.
Stream.of(1, 2, 3, 4, 5, 1, 2)
.takeWhile(n -> n < 4)
.forEach(System.out::println);
// 1
// 2
// 3
// Stops at 4; the trailing 1 and 2 are never seen
This is fundamentally different from filter():
// filter — tests every element; keeps matching ones regardless of position
Stream.of(1, 2, 3, 4, 5, 1, 2)
.filter(n -> n < 4)
.forEach(System.out::println);
// 1, 2, 3, 1, 2 — 4 and 5 removed but trailing 1,2 kept
// takeWhile — stops at first failure
Stream.of(1, 2, 3, 4, 5, 1, 2)
.takeWhile(n -> n < 4)
.forEach(System.out::println);
// 1, 2, 3 — stops when it hits 4
On unordered streams, the behaviour is non-deterministic — use takeWhile only on ordered sources (lists, arrays, ranges).
Practical uses
// Read sorted log entries up to first ERROR
List<LogEntry> upToFirstError = logEntries.stream()
.takeWhile(e -> e.level() != Level.ERROR)
.collect(Collectors.toList());
// Process prices up to budget limit (sorted ascending)
var affordable = prices.stream()
.sorted()
.takeWhile(p -> p <= budget)
.collect(Collectors.toList());
dropWhile(Predicate)
Discards elements from the beginning of the stream as long as the predicate holds, then passes all remaining elements.
Stream.of(1, 2, 3, 4, 5, 1, 2)
.dropWhile(n -> n < 4)
.forEach(System.out::println);
// 4
// 5
// 1
// 2
dropWhile and takeWhile are complementary. Together they partition a stream at the first failing element:
var all = List.of(1, 2, 3, 4, 5, 1, 2);
var before = all.stream().takeWhile(n -> n < 4).collect(Collectors.toList()); // [1, 2, 3]
var from = all.stream().dropWhile(n -> n < 4).collect(Collectors.toList()); // [4, 5, 1, 2]
Practical uses
// Skip header lines of a CSV (lines starting with #)
Files.lines(Path.of("data.csv"))
.dropWhile(line -> line.startsWith("#"))
.forEach(this::processDataLine);
// Skip past a known starting marker in a log
logLines.stream()
.dropWhile(line -> !line.contains("START OF BATCH"))
.skip(1) // skip the marker line itself
.forEach(this::processLine);
Stream.ofNullable(T)
Returns a stream containing a single element if the argument is non-null, or an empty stream if it is null. This avoids null checks when building pipelines.
Stream.ofNullable("hello").count() // 1
Stream.ofNullable(null).count() // 0
Why it matters
Before Java 9, null-safe stream construction required:
// Java 8 — verbose null guard
Stream<String> stream = value != null ? Stream.of(value) : Stream.empty();
With ofNullable:
Stream<String> stream = Stream.ofNullable(value);
Practical uses
// Flatmap over a nullable field
users.stream()
.flatMap(user -> Stream.ofNullable(user.getMiddleName()))
.forEach(System.out::println);
// Combine nullable optional values into a stream
Stream.of(user.getEmail(), user.getPhone(), user.getSlack())
.flatMap(Stream::ofNullable)
.forEach(notificationService::send);
Stream.iterate() with Predicate (Java 9)
Java 8’s Stream.iterate(seed, UnaryOperator) produced an infinite stream. Java 9 adds a three-argument version with a termination predicate — analogous to a for loop:
// Java 8 — infinite, must be limited externally
Stream.iterate(0, n -> n + 1)
.limit(10)
.forEach(System.out::println);
// Java 9 — finite with built-in stop condition
Stream.iterate(0, n -> n < 10, n -> n + 1)
.forEach(System.out::println);
// 0, 1, 2, ..., 9
Signature: Stream.iterate(T seed, Predicate<T> hasNext, UnaryOperator<T> next)
Practical uses
// Generate powers of 2 up to 1024
Stream.iterate(1, n -> n <= 1024, n -> n * 2)
.forEach(System.out::println);
// 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024
// Traverse a linked list
Stream.iterate(head, node -> node != null, node -> node.next)
.map(node -> node.value)
.forEach(System.out::println);
// Date sequence
Stream.iterate(LocalDate.of(2024, 1, 1),
date -> date.isBefore(LocalDate.of(2024, 2, 1)),
date -> date.plusDays(1))
.forEach(System.out::println);
Optional API Enhancements (Java 9–11)
Optional received five new methods across three releases, making it significantly more composable.
ifPresentOrElse(Consumer, Runnable) — Java 9
Executes the consumer if a value is present, or the runnable if the Optional is empty. This replaces the common if (opt.isPresent()) { ... } else { ... } pattern.
Optional<User> user = findUser(id);
// Java 8
if (user.isPresent()) {
sendWelcomeEmail(user.get());
} else {
log.warn("User {} not found", id);
}
// Java 9
user.ifPresentOrElse(
this::sendWelcomeEmail,
() -> log.warn("User {} not found", id)
);
or(Supplier) — Java 9
Returns the same Optional if it has a value, or produces a new Optional by calling the supplier if it is empty. Enables lazy Optional chaining without unwrapping:
Optional<User> user = findInCache(id)
.or(() -> findInDatabase(id))
.or(() -> findInLdap(id));
The key difference from orElse(T) and orElseGet(Supplier<T>):
orElse(T)— returns a value (not an Optional); always evaluates the default.orElseGet(Supplier<T>)— returns a value (not an Optional); lazily evaluates.or(Supplier<Optional<T>>)— returns an Optional; enables chaining.
// or() keeps the Optional context — useful when each fallback is also Optional-returning
Optional<String> result = lookupLocal(key)
.or(() -> lookupRemote(key))
.or(() -> Optional.of("DEFAULT"));
stream() — Java 9
Converts an Optional<T> to a Stream<T> of zero or one element. This enables clean integration of Optional-returning methods into stream pipelines.
// Java 8 — verbose flatMap workaround
List<User> found = ids.stream()
.map(this::findUser) // Stream<Optional<User>>
.filter(Optional::isPresent)
.map(Optional::get) // Stream<User>
.collect(Collectors.toList());
// Java 9 — flatMap with Optional.stream()
List<User> found = ids.stream()
.map(this::findUser) // Stream<Optional<User>>
.flatMap(Optional::stream) // Stream<User> — empties are dropped
.collect(Collectors.toList());
More examples
// Collect only successfully parsed integers
List<String> inputs = List.of("42", "bad", "17", "", "99");
List<Integer> numbers = inputs.stream()
.map(this::tryParseInt) // Optional<Integer>
.flatMap(Optional::stream) // Stream<Integer> — parse failures dropped
.collect(Collectors.toList());
// [42, 17, 99]
Optional<Integer> tryParseInt(String s) {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
orElseThrow() (no-arg) — Java 10
Java 8 had orElseThrow(Supplier<X>) that throws a supplied exception. Java 10 adds a no-argument version that throws NoSuchElementException:
// Java 8 — requires a supplier even for a standard exception
String value = opt.orElseThrow(NoSuchElementException::new);
// Java 10 — shorter
String value = opt.orElseThrow(); // throws NoSuchElementException if empty
orElseThrow() expresses intent more clearly than get():
get()— suggests “give me the value” with an implicit assumption it is present.orElseThrow()— makes explicit that absence is an error condition.
For this reason, IntelliJ and SonarQube flag Optional.get() as a warning and suggest orElseThrow().
isEmpty() — Java 11
The logical complement of isPresent(). Returns true if the Optional has no value.
Optional<String> empty = Optional.empty();
empty.isEmpty() // true
empty.isPresent() // false
Optional<String> valued = Optional.of("hello");
valued.isEmpty() // false
valued.isPresent() // true
Useful for guard clauses and conditions:
// Java 8
if (!findUser(id).isPresent()) {
throw new UserNotFoundException(id);
}
// Java 11
if (findUser(id).isEmpty()) {
throw new UserNotFoundException(id);
}
Combining the Enhancements
All these additions compose cleanly:
// Process a batch of IDs: find each user, send notifications only to active ones,
// log misses; collect names of notified users
var notified = ids.stream()
.map(this::findUser) // Stream<Optional<User>>
.peek(opt -> opt.ifPresentOrElse( // side effect: log misses
u -> {},
() -> log.warn("User not found")))
.flatMap(Optional::stream) // Stream<User> — drop empties
.filter(User::isActive)
.peek(this::sendNotification)
.map(User::getName)
.collect(Collectors.toUnmodifiableList());