Table of Contents


Terminal Operations

Terminal operations produce a final result from a pipeline. Let’s walk through the common ones.


Iteration

Use forEach to iterate a stream.

List<Integer> list = List.of(3, 2, 1, 5, 7);
list.stream().forEach(System.out::println);

forEach does not guarantee order on parallel streams. Use forEachOrdered when you need ordering.

List<Integer> list = List.of(3, 1, 2);

// output order may vary
list.parallelStream().forEach(System.out::println);

// output order is consistent
list.parallelStream().forEachOrdered(System.out::println);


Reduce

reduce processes all elements into a single result. It has three overloads.

// form 1
Optional<T> reduce(BinaryOperator<T> accumulator); 

// form 2
T reduce(T identity, BinaryOperator<T> accumulator);

// form 3
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator,
            BinaryOperator<U> combiner);

The one-argument form takes a BinaryOperator and returns an Optional.

List<Integer> list = List.of(1, 2, 3);
Optional<Integer> result = list.stream().reduce((a, b) -> a + b); // 6
// list.stream().reduce(Integer::sum);

The two-argument form accepts an identity value.

List<Integer> list = List.of(1, 2, 3);
Integer result = list.stream().reduce(1, Integer::sum);

// 7
System.out.println(result);

The three-argument form uses an identity, an accumulator, and a combiner. The combiner merges partial results from parallel execution.

List<Integer> list = List.of(3, 7, 9);
Integer result = list.parallelStream()
        .reduce(1, Integer::sum, (a, b) -> {
            System.out.println("in combiner");
            return a + b;
        });

System.out.println(result);
// output
// in combiner a:8 b:10
// in combiner a:4 b:18
// 22

In a sequential stream, the combiner does not run. The result is computed as: (1+3=4, 1+9=10, 1+7=8) then combined (8+10=18, 4+18=22).


Min, Max, Sum, Average

You can compute min/max directly.

// returns Optional
OptionalDouble min = DoubleStream.of(4.1, 3.4, -1.3, 3.9, -5.7).min();
min.ifPresent(System.out::println);

// 5
int max = IntStream.of(2, 4, 5, 3).max().getAsInt();

Count elements:

// result 4
long count = IntStream.of(2, 4, 1, 3).count()

Sum or average:

// result 7.1
double sum = DoubleStream.of(3.1, 2.6, 1.4).sum();

// returns Optional
OptionalDouble average = IntStream.of(3, 2, 1).average();

// result 2.0
average.ifPresent(System.out::println);


Collect

Collect converts stream results into collections or maps. Assume this class:

class Food {
    public Food(String name, int cal) {
        this.name = name;
        this.cal = cal;
    }

    private String name;
    private int cal;
    
    @Override
    public String toString() {
        return String.format("name: %s, cal: %s", name, cal);
    }

    // getters and setters omitted
}

List<Food> list = new ArrayList<>();
list.add(new Food("burger", 520));
list.add(new Food("chips", 230));
list.add(new Food("coke", 143));
list.add(new Food("soda", 143));


  • Collectors.toList: collect into a list
List<String> nameList = list.stream()
        .map(Food::getName) // extract name
        .collect(Collectors.toList());


  • Sum, average, and statistics
// sum of name lengths
Integer summingName = list.stream()
        .collect(Collectors.summingInt(s -> s.getName().length()));
    
// sum of calories
int sum = list.stream().mapToInt(Food::getCal).sum();

// average: averagingInt
Double averageInt = list.stream()
        .collect(Collectors.averagingInt(Food::getCal));

// average: averagingDouble
Double averageDouble = list.stream()
        .collect(Collectors.averagingDouble(Food::getCal));

Use summarizingInt to get all stats at once.

IntSummaryStatistics summaryStatistics = list.stream()
        .collect(Collectors.summarizingInt(Food::getCal));

summaryStatistics.getAverage(); // average
summaryStatistics.getCount(); // count
summaryStatistics.getMax(); // max
summaryStatistics.getMin(); // min
summaryStatistics.getSum(); // sum


  • Join into a single string
// without arguments
String defaultJoining = list.stream()
        .map(Food::getName).collect(Collectors.joining());

// burgerchipscokesoda
System.out.println(defaultJoining);

With a delimiter:

String delimiterJoining = list.stream()
        .map(Food::getName).collect(Collectors.joining(","));

// burger,chips,coke,soda
System.out.println(delimiterJoining);

With delimiter, prefix, and suffix:

String combineJoining = list.stream()
        .map(Food::getName).collect(Collectors.joining(",", "[", "]"));

// [burger,chips,coke,soda]
System.out.println(combineJoining);


  • Group by a key
// group by calories
Map<Integer, List<Food>> calMap = list.stream()
        .collect(Collectors.groupingBy(Food::getCal));

// { 230=[name: chips, cal: 230],
//   520=[name: burger, cal: 520],
//   143=[name: coke, cal: 143, name: soda, cal: 143]}
System.out.println(calMap);


  • Partition into true/false

partitioningBy takes a Predicate and splits into two groups.

// group by calories > 200
Map<Boolean, List<Food>> partitionMap = list.stream()
        .collect(Collectors.partitioningBy(o -> o.getCal() > 200));

// { false=[name: coke, cal: 143, name: soda, cal: 143],
//   true=[name: burger, cal: 520, name: chips, cal: 230]}
System.out.println(partitionMap);


  • Collect into a Map

Map calories to names:

// throws Exception!
Map<Integer, String> map = list.stream()
        .collect(Collectors.toMap(
                o -> o.getCal(),
                o -> o.getName()
        ));
System.out.println(map);

If a key is duplicated, toMap throws IllegalStateException. Add a merge function to resolve conflicts.

// for duplicate keys, keep the new value
Map<Integer, String> map = list.stream()
        .collect(Collectors.toMap(
                o -> o.getCal(),
                o -> o.getName(),
                (oldValue, newValue) -> newValue));

// {230=chips, 520=burger, 143=soda}
System.out.println(map);


  • Post-process after collect

collectingAndThen runs an extra operation after collection.

// return the max-calorie item
Food food = list.stream()
        .collect(Collectors.collectingAndThen(
                Collectors.maxBy(Comparator.comparing(Food::getCal)),
                (Optional<Food> o) -> o.orElse(null)));

// name: burger, cal: 520
System.out.println(food);


  • Build a custom Collector
// build a custom collector
Collector<Food, StringJoiner, String> foodNameCollector = Collector.of(
        () -> new StringJoiner(" | "), // supplier
        (a, b) -> a.add(b.getName()), // accumulator
        (a, b) -> a.merge(b), // combiner
        StringJoiner::toString); // finisher
        
// apply the collector
String foodNames = list.stream().collect(foodNameCollector);

// burger | chips | coke | soda
System.out.println(foodNames);


Matching

You can check whether elements satisfy a predicate.

  • Any match? (anyMatch)
// any item over 300 calories?
boolean anyMatch = list.stream()
        .anyMatch(food -> food.getCal() > 300);
  • All match? (allMatch)
// all over 100 calories?
boolean allMatch = list.stream()
        .allMatch(food -> food.getCal() > 100);
  • None match? (noneMatch)
// none over 1000 calories?
boolean noneMatch = list.stream()
        .noneMatch(food -> food.getCal() < 1000);


Next

We covered how to compute results from stream pipelines. The next post provides more practical stream examples.