ZetCode

Java Stream collect

last modified January 27, 2024

Java Stream collect tutorial shows how to do reduction operations using collectors.

Java Stream

Java Stream is a sequence of elements from a source that supports aggregate operations. Streams do not store elements; the elements are computed on demand. Elements are consumed from data sources such as collections, arrays, or I/O resources.

Java Stream collect

Java Stream collect is a terminal stream operation. It performs a mutable reduction operation on the elements of the stream. Reduction operations can be performed either sequentially or in parallel.

Collectors

The Collectors class contains predefined collectors to perform common mutable reduction tasks. The collectors accumulate elements into collections, reduce elements into a single value (such as min, max, count, or sum), or group elements by a criteria.

Set<String> uniqueVals = vals.collect(Collectors.toSet());

The toSet method returns a Collector that accumulates the input elements into a new Set.

// can be replaced with min()
Optional<Integer> min = vals.stream().collect(Collectors.minBy(Integer::compareTo));

The minBy returns a Collector that produces the minimal element according to a given Comparator.

Map<Boolean, List<User>> usersByStatus =
    users().stream().collect(Collectors.groupingBy(User::isSingle));

With groupingBy we select users who have single status into a group.

Map<Boolean, List<User>> statuses =
    users().stream().collect(Collectors.partitioningBy(User::isSingle));

With partitioningBy we separate the users into two groups based on their status attribute.

Collector

The Collector interface defines a set of methods which are used during the reduction process. The following is the interface signature with the five methods it declares.

public interface Collector<T,A,R> {
    Supplier<A> supplier();
    BiConsumer<A,T> accumulator();
    BinaryOperator<A> combiner();
    Function<A,R> finisher();
    Set<Characteristics> characteristics();
}

A Collector is specified by four functions that work together to accumulate entries into a mutable result container, and optionally perform a final transform on the result.

The T is the type of elements in the stream to be collected. The A the type of the accumulator. The R is the type of the result returned by the collector.

The supplier returns a function which creates a new result container. The accumulator returns a function which performs the reduction operation. It accepts two arguments: the first ist the mutable result container (accumulator) and the second the stream element that is folded into the result container.

When the stream is collected in parallel the combiner returns a function which merges two accumulators. The finisher returns a function which performs the final transformation from the intermediate result container to the final result of type R. The finisher returns an identity function when the accumulator already represents the final result.

Note: In math, the identity function is one in which the output of the function is equal to its input. In Java, the identity method of a Function returns a Function that always returns its input arguments.

The characteristics method returns an immutable set of Characteristics which define the behavior of the collector. It can be used to do some optimizations during the reduction process. For example, if the set contains CONCURRENT, then the collection process can be performed in parallel.

Java Stream collect to list

The Collectors.toList returns a collector that accumulates the input elements into a new list.

com/zetcode/JavaCollectToListEx.java
package com.zetcode;

import java.util.List;
import java.util.stream.Collectors;

public class JavaCollectToListEx {

    public static void main(String[] args) {

        var words = List.of("marble", "coin", "forest", "falcon",
                "sky", "cloud", "eagle", "lion");

        // filter all four character words into a list
        var words4 = words.stream().filter(word -> word.length() == 4)
                .collect(Collectors.toList());

        System.out.println(words4);
    }
}

The example filters a list of strings and transforms the stream into a list. We filter the list to include only strings whose length is equal to four.

var words4 = words.stream().filter(word -> word.length() == 4)
    .collect(Collectors.toList());

With the stream method, we create a Java Stream from a list of strings. On this stream, we apply the filter method. The filter method accepts an anonymous function that returns a boolean true for all elements of the stream whose length is four. We create a list back from the stream with the collect method.

[coin, lion]

These two words have four characters.

Java Stream collect join into string

The Collectors.joining returns a Collector that concatenates the input elements into a string, in encounter order.

com/zetcode/JavaStreamFilterRemoveNulls.java
package com.zetcode;

import java.util.List;
import java.util.stream.Collectors;

public class JavaCollectJoinEx {

    public static void main(String[] args) {

        var words = List.of("marble", "coin", "forest", "falcon",
                "sky", "cloud", "eagle", "lion");

        // can be replaced with String.join
        var joined = words.stream().collect(Collectors.joining(","));

        System.out.printf("Joined string: %s", joined);
    }
}

We have a list of words. We transform the list into a string where the words are separated with comma.

Joined string: marble,coin,forest,falcon,sky,cloud,eagle,lion

Java Stream collect count

The Collectors.counting retuns a Collector that counts the number of elements in the stream.

com/zetcode/JavaCollectCountEx.java
package com.zetcode;

import java.util.List;
import java.util.stream.Collectors;

public class JavaCollectCountEx {

    public static void main(String[] args) {

        var vals = List.of(1, 2, 3, 4, 5);

        // can be replaced with count
        var n = vals.stream().collect(Collectors.counting());

        System.out.println(n);
    }
}

The example counts the number of elements in the list.

Java Stream collect sum

The Collectors.summintInt returns a Collector that produces the sum of a integer-valued function applied to the input elements.

com/zetcode/Cat.java
package com.zetcode;

public class Cat {

    private String name;
    private int age;

    public Cat(String name, int age) {

        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {

        var sb = new StringBuilder("Cat{");
        sb.append("name='").append(name).append('\'');
        sb.append(", age=").append(age);
        sb.append('}');
        return sb.toString();
    }
}

In the example, we use this Cat class.

com/zetcode/JavaCollectSumAgeEx.java
package com.zetcode;

import java.util.List;
import java.util.stream.Collectors;

public class JavaCollectSumAgeEx {

    public static void main(String[] args) {

        var cats = List.of(
                new Cat("Bella", 4),
                new Cat("Othello", 2),
                new Cat("Coco", 6)
        );

        // can be replaced with mapToInt().sum()
        var ageSum = cats.stream().collect(Collectors.summingInt(cat -> cat.getAge()));

        System.out.printf("Sum of cat ages: %d%n", ageSum);
    }
}

The example sums the age of the cats.

var ageSum = cats.stream().collect(Collectors.summingInt(cat -> cat.getAge()));

The parameter of the summingInt method is a mapper function which extracts the property to be summed.

Java Stream collectingAndThen

The Collectors.collectingAndThen adapts a Collector to perform an additional finishing transformation.

com/zetcode/JavaCollectAndThenEx.java
package com.zetcode;

import java.text.NumberFormat;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;


public class JavaCollectAndThenEx {

    public static void main(String[] args) {

        var vals = List.of(230, 210, 120, 250, 300);

        var avgPrice = vals.stream().collect(Collectors.collectingAndThen(
                Collectors.averagingInt(Integer::intValue),
                avg -> {
                    var nf = NumberFormat.getCurrencyInstance(new Locale("en", "US"));
                    return nf.format(avg);
                })
        );

        System.out.printf("The average price is %s%n", avgPrice);
    }
}

The example calculates an average price and then formats it.

Java Stream custom collector

The Collector.of returns a new Collector described by the given supplier, accumulator, combiner, and finisher functions.

In the following example, we create a custom collector.

com/zetcode/CustomCollector.java
package com.zetcode;

import java.util.List;
import java.util.StringJoiner;
import java.util.stream.Collector;

public class CustomCollector {

    public static void main(String[] args) {

        List<User> persons = List.of(
                new User("Robert", 28),
                new User("Peter", 37),
                new User("Lucy", 23),
                new User("David", 28));

        Collector<User, StringJoiner, String> personNameCollector =
                Collector.of(
                        () -> new StringJoiner(" | "), // supplier
                        (j, p) -> j.add(p.getName()),  // accumulator
                        (j1, j2) -> j1.merge(j2),      // combiner
                        StringJoiner::toString);       // finisher

        String names = persons
                .stream()
                .collect(personNameCollector);

        System.out.println(names);
    }
}

class User {

    private String name;
    private int age;

    User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {

        return this.name;
    }

    @Override
    public String toString() {

        return String.format("%s is %d years old", name, age);
    }
}

In the example, we collect the names from the list of user objects.

Collector<User, StringJoiner, String> personNameCollector =
...

The Collector<User, StringJoiner, String> has three types. The first is the type of input elements for the new collector. The second is the type for the intermediate result and the third for the final result.

Collector.of(
        () -> new StringJoiner(" | "), // supplier
        (j, p) -> j.add(p.getName()),  // accumulator
        (j1, j2) -> j1.merge(j2),      // combiner
        StringJoiner::toString);       // finisher

We create our custom collector. First, we build an initial result container. In our case it is a StringJoiner. The accumulator simply adds the name from the current user object to the StringJoiner. The combiner merges two partial results in case of parallel processing. Finally, the finisher turns the StringJoiner into a plain string.

Robert | Peter | Lucy | David

Java Stream collect group by

With the Collectors.groupingBy method we can separate the stream elements into groups based on the specified criterion.

com/zetcode/Product.java
package com.zetcode;

import java.math.BigDecimal;

public class Product {

    private String name;
    private String category;
    private BigDecimal price;

    public Product(String name, String category, BigDecimal price) {

        this.name = name;
        this.category = category;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCategory() {
        return category;
    }

    public void setCategory(String category) {
        this.category = category;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    @Override
    public String toString() {

        var sb = new StringBuilder("Product{");
        sb.append("name='").append(name).append('\'');
        sb.append(", category='").append(category).append('\'');
        sb.append(", price=").append(price);
        sb.append('}');
        return sb.toString();
    }
}

We will group Product objects.

com/zetcode/Product.java
package com.zetcode;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class JavaCollectGroupByCategory {

    public static void main(String[] args) {

        Map<String, List<Product>> productsByCategories =
                products().stream().collect(
                    Collectors.groupingBy(Product::getCategory));

        productsByCategories.forEach((k, v) -> {

            System.out.println(k);

            for (var name : v) {
                System.out.println(name);
            }
        });

    }

    private static List<Product> products() {

        return List.of(
                new Product("apple", "fruit", new BigDecimal("4.50")),
                new Product("banana", "fruit", new BigDecimal("3.76")),
                new Product("carrot", "vegetables", new BigDecimal("2.98")),
                new Product("potato", "vegetables", new BigDecimal("0.92")),
                new Product("garlic", "vegetables", new BigDecimal("1.32")),
                new Product("ginger", "vegetables", new BigDecimal("2.45")),
                new Product("white bread", "bakery", new BigDecimal("1.50")),
                new Product("roll", "bakery", new BigDecimal("0.08")),
                new Product("bagel", "bakery", new BigDecimal("0.15"))
        );
    }
}

We have a list of products. With the Collectors.groupingBy, we separate the products into groups based on their category.

bakery
Product{name='white bread', category='bakery', price=1.50}
Product{name='roll', category='bakery', price=0.08}
Product{name='bagel', category='bakery', price=0.15}
fruit
Product{name='apple', category='fruit', price=4.50}
Product{name='banana', category='fruit', price=3.76}
vegetables
Product{name='carrot', category='vegetables', price=2.98}
Product{name='potato', category='vegetables', price=0.92}
Product{name='garlic', category='vegetables', price=1.32}
Product{name='ginger', category='vegetables', price=2.45}

Java Stream collect partition by

Partitioning is a special case of grouping. Partitioning operation divides the stream into two groups based on the given predicate function.

com/zetcode/User.java
package com.zetcode;

public class User {

    private String name;
    private boolean single;

    public User(String name, boolean single) {

        this.name = name;
        this.single = single;
    }

    public boolean isSingle() {

        return single;
    }

    @Override
    public String toString() {

        var sb = new StringBuilder("User{");
        sb.append("name='").append(name).append('\'');
        sb.append(", single=").append(single);
        sb.append('}');
        return sb.toString();
    }
}

We will group user objects.

com/zetcode/JavaCollectPartitionByEx.java
package com.zetcode;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class JavaCollectPartitionByEx {

    public static void main(String[] args) {

        Map<Boolean, List<User>> statuses =
                users().stream().collect(
                    Collectors.partitioningBy(User::isSingle));

        statuses.forEach((k, v) -> {

            if (k) {

                System.out.println("Single: ");
            } else {

                System.out.println("In a relationship:");
            }

            v.forEach(System.out::println);
        });
    }

    private static List<User> users() {

        return List.of(
                new User("Julia", false),
                new User("Jake", false),
                new User("Mike", false),
                new User("Robert", true),
                new User("Maria", false),
                new User("Peter", true)
        );
    }
}

In the example, we partition the stream into two groups based on the single attribute.

Map<Boolean, List<User>> statuses =
    users().stream().collect(Collectors.partitioningBy(User::isSingle));

The Collectors.partitioningBy takes the isSingle predicate, which returns a boolean value indicating the status of the user.

In a relationship:
User{name='Julia', single=false}
User{name='Jake', single=false}
User{name='Mike', single=false}
User{name='Maria', single=false}
Single: 
User{name='Robert', single=true}
User{name='Peter', single=true}

Source

Java Stream documentation

In this article we have have worked with Java Stream predefined and custom collectors.

Author

My name is Jan Bodnar and I am a passionate programmer with many years of programming experience. I have been writing programming articles since 2007. So far, I have written over 1400 articles and 8 e-books. I have over eight years of experience in teaching programming.

List all Java tutorials.