Topic 10. Stream API, Optional
Stream API and Optional solve two frequent problems:
- expressing data transformations as readable pipelines,
- handling absence of values explicitly.
Stream API model
A Stream is not a data container. It is a computation pipeline over data source.
Key properties:
- stream does not own storage,
- source collection is usually not mutated,
- intermediate operations are lazy,
- execution starts at terminal operation.
Pipeline structure
- Source:
list.stream(),Stream.of(...),Files.lines(...). - Intermediate operations:
filter,map,flatMap,sorted,distinct,limit. - Terminal operation:
toList,collect,reduce,count,forEach,findFirst.
Example 1. Filter + map + sort
List<String> names = List.of("ann", "bob", "alex", "anna");
List<String> result = names.stream()
.filter(n -> n.length() > 3)
.map(String::toUpperCase)
.sorted()
.toList();
System.out.println(result); // [ALEX, ANNA]
Example 2. Aggregation (reduce, collectors)
List<Integer> prices = List.of(100, 250, 80, 70);
int total = prices.stream()
.reduce(0, Integer::sum);
double avg = prices.stream()
.collect(java.util.stream.Collectors.averagingInt(Integer::intValue));
System.out.println(total); // 500
System.out.println(avg); // 125.0
Example 3. Flatten nested data with flatMap
List<List<String>> tags = List.of(
List.of("java", "backend"),
List.of("backend", "spring")
);
List<String> unique = tags.stream()
.flatMap(List::stream)
.distinct()
.sorted()
.toList();
System.out.println(unique); // [backend, java, spring]
Laziness
Stream<String> s = names.stream().filter(n -> n.startsWith("a"));
// no real execution yet
long count = s.count();
Lazy model allows JVM optimization and avoids unnecessary work.
When streams are good vs when loops are better
Streams fit when:
- transformation steps are linear,
- filtering + aggregation dominate,
- side effects are minimal.
Loops can be better when:
- branching logic is complex,
- step-by-step debugging is critical,
- hot-path imperative code is clearer.
Parallel streams
parallelStream() may help CPU-bound workloads, but not always.
Use cautiously:
- avoid shared mutable state inside lambdas,
- account for parallelization overhead,
- measure before adopting.
Optional: explicit absence contract
Optional<T> means value may or may not be present.
Optional<String> email = findEmailByUserId(10L);
String value = email.orElse("[email protected]");
This forces explicit caller handling instead of hidden null assumptions.
Core Optional operations
isPresent,isEmpty,orElse,orElseGet,orElseThrow,map,flatMap,filter,ifPresent,ifPresentOrElse.
Example 4. Null-safe chain
Optional<User> user = findUser(id);
String city = user
.map(User::getAddress)
.map(Address::getCity)
.filter(c -> !c.isBlank())
.orElse("Unknown");
orElse vs orElseGet
String a = opt.orElse(expensiveFallback()); // always evaluates fallback
String b = opt.orElseGet(() -> expensiveFallback()); // lazy fallback
For expensive fallback logic, prefer orElseGet.
Where Optional is useful and where it is not
Useful:
- service/repository return values that may be missing,
- API boundaries requiring explicit absence semantics.
Usually not useful:
- JPA entity fields and DTO internals,
- method arguments in most cases.
Common mistakes
- very long stream chains with dense business logic,
- side effects inside
map/filter, Optional.get()without safety checks,- using
parallelStream()by default without benchmarks.
Practical workflow
- Define transformation steps first.
- Use streams for linear pipelines.
- Use loops for branch-heavy logic.
- Return Optional at boundaries for missing-value semantics.
- Keep readability above cleverness.
Key takeaway
Stream API improves composability and clarity of data transformations. Optional makes missing values explicit and safer.