Terminal Stream Operations and reduce in Java
A terminal operation runs the pipeline and produces a result, and reduce is the general purpose way to fold a stream into one value.
- What makes an operation terminal
- Collecting the result
- Matching and finding
- Counting, min and max
- reduce
- One argument: an accumulator only
- Two arguments: an identity and an accumulator
- Three arguments: identity, accumulator and combiner
- Visualising a fold
- Prefer the specialised operation
- forEach and its ordering
- Numeric summaries
- Common mistakes
- Best practices
- Practice
- Conclusion
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
What makes an operation terminal
A terminal operation consumes the stream and produces a result or a side effect. Once it has run, the stream cannot be reused.
| Operation | Returns | Short circuits |
|---|---|---|
forEach, forEachOrdered | nothing | No |
toList, collect, toArray | A collection or array | No |
count | long | No |
min, max | Optional<T> | No |
reduce | A single value | No |
anyMatch, allMatch, noneMatch | boolean | Yes |
findFirst, findAny | Optional<T> | Yes |
sum, average, summaryStatistics | Numeric, primitive streams only | No |
Collecting the result
List<String> titles = notes.stream().map(Note::title).toList(); // Java 16
List<String> mutable = notes.stream().map(Note::title)
.collect(Collectors.toList()); // modifiable
Set<String> unique = notes.stream().map(Note::category)
.collect(Collectors.toSet());
String[] array = notes.stream().map(Note::title).toArray(String[]::new);toList()returns an unmodifiable list. When the result must be modified afterwards, usecollect(Collectors.toList())or wrap it in a newArrayList.
Matching and finding
boolean anyPopular = notes.stream().anyMatch(note -> note.views() > 500);
boolean allPublished = notes.stream().allMatch(Note::published);
boolean noneEmpty = notes.stream().noneMatch(note -> note.title().isBlank());
Optional<Note> first = notes.stream().filter(Note::published).findFirst();
Optional<Note> any = notes.parallelStream().filter(Note::published).findAny();All five stop as soon as the answer is settled. Note the vacuous truth rule: allMatch returns true on an empty stream, and anyMatch returns false.
Counting, min and max
long published = notes.stream().filter(Note::published).count();
Optional<Note> mostViewed = notes.stream()
.max(Comparator.comparingInt(Note::views));
Optional<Note> oldest = notes.stream()
.min(Comparator.comparing(Note::created));reduce
reduce folds a stream into a single value by combining elements two at a time. It has three forms.
One argument: an accumulator only
Optional<Integer> total = Stream.of(4, 8, 15, 16)
.reduce((a, b) -> a + b); // Optional[43]An Optional is returned because an empty stream has no result.
Two arguments: an identity and an accumulator
int total = Stream.of(4, 8, 15, 16)
.reduce(0, (a, b) -> a + b); // 43, and 0 for an empty stream
String joined = Stream.of("a", "b", "c")
.reduce("", (a, b) -> a + b); // abcThe identity must be a true neutral element: combining it with any value must return that value unchanged. Zero for addition, one for multiplication, an empty string for concatenation.
Three arguments: identity, accumulator and combiner
int totalLength = notes.parallelStream()
.reduce(0,
(sum, note) -> sum + note.title().length(), // per element
Integer::sum); // merge partial resultsThe combiner merges results from parallel sub tasks. In a sequential stream it is never called, which is why a wrong combiner can pass tests and fail in parallel.
Visualising a fold
reduce(0, (a, b) -> a + b) over 4, 8, 15
0 + 4 = 4
4 + 8 = 12
12 + 15 = 27Prefer the specialised operation
// Works, but boxes every element
int total = notes.stream().map(Note::views).reduce(0, Integer::sum);
// Clearer and faster
int total = notes.stream().mapToInt(Note::views).sum();sum, average, count, min and max are all reductions with better names. Use reduce when nothing more specific exists.
forEach and its ordering
notes.stream().forEach(System.out::println); // order not guaranteed in parallel
notes.parallelStream().forEachOrdered(System.out::println); // encounter order preserved// Wrong: side effect into an external list
List<String> titles = new ArrayList<>();
notes.stream().forEach(note -> titles.add(note.title())); // unsafe in parallel
// Right
List<String> titles = notes.stream().map(Note::title).toList();Numeric summaries
IntSummaryStatistics stats = notes.stream()
.mapToInt(Note::views)
.summaryStatistics();
System.out.println(stats.getCount());
System.out.println(stats.getSum());
System.out.println(stats.getMin());
System.out.println(stats.getMax());
System.out.println(stats.getAverage());One pass produces all five values, which is better than five separate pipelines.
Common mistakes
- Trying to modify the list returned by
toList(). - Using an identity that is not neutral, such as 1 for a sum.
- Using
forEachto build a collection instead ofcollect. - Supplying a combiner inconsistent with the accumulator, which only fails in parallel.
- Forgetting that
allMatchistruefor an empty stream. - Calling
get()on theOptionalfromminormaxwithout checking.
Best practices
- Prefer
toList()unless a modifiable list is genuinely required. - Use the specialised reductions before reaching for
reduce. - Keep the accumulator associative and free of side effects.
- Use
summaryStatisticswhen several numbers are wanted. - Handle the
OptionalfromfindFirst,minandmaxproperly.
Practice
- Compute the product of a list of integers with
reduce, choosing the correct identity. - Why does
reducewith one argument return anOptional? - Find the longest title in a list of notes, and handle the empty case.
- Replace a
forEachthat fills an external list with a proper collection step. - Explain why
allMatchon an empty stream returnstrue, and when that matters.
Conclusion
Terminal operations run the pipeline and end the stream. Reach for the specific one first, use reduce when folding into a custom result, and keep every function you pass associative and side effect free.