A lambda is a small piece of behavior that you pass to another operation. A stream describes a sequence of data-processing steps. Together they make operations such as filtering completed lessons and adding their durations concise without hiding the underlying rules. Everything here is available in Java 21 without preview features.
Start with behavior, not special syntax
A functional interface has one abstract method. Predicate<T> tests a value and returns a boolean, Function<T, R> transforms a value, Consumer<T> performs an action, and Supplier<T> produces a value. A lambda supplies the method's implementation.
lesson -> lesson.complete() is a lambda. Lesson::complete is a method reference expressing the same operation here. The compiler uses the expected interface type to determine parameter and result types. A captured local variable must be final or effectively final, meaning you do not reassign it after initialization. This rule does not make an object reached through that variable immutable.
Run a complete pipeline
Save as StreamReport.java, then run javac --release 21 StreamReport.java and java StreamReport.
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class StreamReport {
record Lesson(String title, String module, int minutes, boolean complete) {}
public static void main(String[] args) {
List<Lesson> lessons = List.of(
new Lesson("Types", "basics", 20, true),
new Lesson("Files", "io", 35, true),
new Lesson("Streams", "io", 30, false));
Predicate<Lesson> completed = Lesson::complete;
List<String> titles = lessons.stream()
.filter(completed)
.sorted(Comparator.comparing(Lesson::title))
.map(Lesson::title)
.toList();
int minutes = lessons.stream()
.filter(completed)
.mapToInt(Lesson::minutes)
.sum();
Map<String, Integer> byModule = lessons.stream()
.filter(completed)
.collect(Collectors.groupingBy(
Lesson::module, TreeMap::new,
Collectors.summingInt(Lesson::minutes)));
String next = lessons.stream()
.filter(lesson -> !lesson.complete())
.map(Lesson::title)
.findFirst()
.orElse("All done");
System.out.println(titles);
System.out.println(minutes);
System.out.println(byModule);
System.out.println(next);
}
}
Expected output:
[Files, Types]
55
{basics=20, io=35}
Streams
The first pipeline retains completed lessons, orders them by title, then extracts their titles. The second uses an IntStream to add numeric values without boxing each duration. The third groups completed durations by module, using TreeMap to make key order explicit. These are separate traversals of a tiny collection, chosen to make each result understandable.
Intermediate and terminal operations
filter, map and sorted are intermediate operations: they describe another stream stage. toList, sum, collect and findFirst are terminal operations that request a result. Stream processing is lazy; declaring intermediate steps does not by itself traverse the source.
A stream is single-use. After a terminal operation, obtain a new stream from the collection for another report. A stream is not another storage container and does not automatically copy its source. Do not change the source while its pipeline runs.
map produces one output per input; flatMap flattens nested results, such as converting a stream of lists into a stream of their elements. reduce combines elements using an associative operation and a suitable identity when supplied. For a sum the identity is zero; subtraction is not associative and is a poor general reduction operation. Large integer sums still need an overflow policy.
Stream.toList() returns an unmodifiable list. If later code needs to add or remove items, collect explicitly with Collectors.toCollection(ArrayList::new). Do not assume every collector promises a specific collection implementation or mutability.
Missing results and side effects
findFirst returns an Optional, which represents either a value or its absence. orElse supplies a fallback. Avoid calling get() without checking presence. Use orElseGet when constructing the fallback is expensive: orElse evaluates its argument even when a value exists. Optional describes absence; it is not an exception-recovery mechanism.
Prefer pipelines that return values over lambdas that mutate outside lists or counters. Side effects make evaluation order harder to understand and can become unsafe under parallel execution. peek is not a dependable place for required work because an implementation may optimize away stages that do not affect the result.
Parallel streams are not an automatic speed improvement. They add coordination, need safe operations, and commonly share execution resources. Start sequentially, measure a real bottleneck, and keep blocking network calls out of a casual parallel-stream transformation. An ordinary loop is often clearer when processing needs several stateful steps or checked-error recovery.
Exercise and solution guidance
Report the titles of incomplete lessons lasting at least 25 minutes, sorted alphabetically. Return a list and separately compute their total duration. With the sample input, expect [Streams] and 30.
Compose a predicate using !lesson.complete() and the duration comparison. Apply it to fresh streams for both results. Test an empty input and an all-completed input; the outputs should be an empty list and zero. Then rewrite the calculation as a loop and explain which form you find easier to maintain and why.
Official references
Previous: Exceptions, Files and Resources. Next: Concurrency.

