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

A terminal operation consumes the stream and produces a result or a side effect. Once it has run, the stream cannot be reused.

OperationReturnsShort circuits
forEach, forEachOrderednothingNo
toList, collect, toArrayA collection or arrayNo
countlongNo
min, maxOptional<T>No
reduceA single valueNo
anyMatch, allMatch, noneMatchbooleanYes
findFirst, findAnyOptional<T>Yes
sum, average, summaryStatisticsNumeric, primitive streams onlyNo

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, use collect(Collectors.toList()) or wrap it in a new ArrayList.

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);      // abc

The 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 results

The 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 = 27

Prefer 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 forEach to build a collection instead of collect.
  • Supplying a combiner inconsistent with the accumulator, which only fails in parallel.
  • Forgetting that allMatch is true for an empty stream.
  • Calling get() on the Optional from min or max without 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 summaryStatistics when several numbers are wanted.
  • Handle the Optional from findFirst, min and max properly.

Practice

  1. Compute the product of a list of integers with reduce, choosing the correct identity.
  2. Why does reduce with one argument return an Optional?
  3. Find the longest title in a list of notes, and handle the empty case.
  4. Replace a forEach that fills an external list with a proper collection step.
  5. Explain why allMatch on an empty stream returns true, 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.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.