Java Stream collect
last modified July 20, 2024
In this article we show how to do reduction operations using collectors.
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.
The collect method
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.
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.
Collectors.toList
The Collectors.toList
returns a collector that accumulates the
input elements into a new list.
import java.util.List; import java.util.stream.Collectors; void main() { 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.
$ java Main.java [coin, lion]
These two words have four characters.
Collectors.joining
The Collectors.joining
returns a Collector
that
concatenates the input elements into a string, in encounter order.
import java.util.List; import java.util.stream.Collectors; void main() { 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.
$ java Main.java Joined string: marble,coin,forest,falcon,sky,cloud,eagle,lion
Collectors.counting
The Collectors.counting
retuns a Collector
that counts
the number of elements in the stream.
import java.util.List; import java.util.stream.Collectors; void main() { 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.
Collectors.summintInt
The Collectors.summintInt
returns a Collector
that
produces the sum of a integer-valued function applied to the input elements.
import java.util.List; import java.util.stream.Collectors; void main() { 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.age())); System.out.printf("Sum of cat ages: %d%n", ageSum); } record Cat(String name, int age) { }
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.
Collectors.collectingAndThen
The Collectors.collectingAndThen
adapts a Collector
to
perform an additional finishing transformation.
import java.text.NumberFormat; import java.util.List; import java.util.Locale; import java.util.stream.Collectors; void main() { 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(Locale.of("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 Main.java The average price is $222.00
Collector.of
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.
import java.util.List; import java.util.StringJoiner; import java.util.stream.Collector; void main() { 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.name()), // accumulator (j1, j2) -> j1.merge(j2), // combiner StringJoiner::toString); // finisher String names = persons .stream() .collect(personNameCollector); System.out.println(names); } record User(String name, int 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.
$ java Main.java Robert | Peter | Lucy | David
Collectors.groupingBy
With the Collectors.groupingBy
method we can separate the stream
elements into groups based on the specified criterion.
import java.math.BigDecimal; import java.util.List; import java.util.Map; import java.util.stream.Collectors; void main() { Map<String, List<Product>> productsByCategories = products().stream().collect( Collectors.groupingBy(Product::category)); productsByCategories.forEach((k, v) -> { System.out.println(k); for (var name : v) { System.out.println(name); } }); } private 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")) ); } record Product(String name, String category, BigDecimal price) { }
We have a list of products. With the Collectors.groupingBy
,
we separate the products into groups based on their category.
$ java Main.java 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}
Collectors.partitioningBy
Partitioning is a special case of grouping. Partitioning operation divides the stream into two groups based on the given predicate function.
import java.util.List; import java.util.Map; import java.util.stream.Collectors; void main() { Map<Boolean, List<User>> statuses = users().stream().collect( Collectors.partitioningBy(User::single)); statuses.forEach((k, v) -> { if (k) { System.out.println("Single: "); } else { System.out.println("In a relationship:"); } v.forEach(System.out::println); }); } private 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) ); } record User(String name, boolean single) { }
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::single));
The Collectors.partitioningBy
takes the single
predicate, which returns a boolean value indicating the status of the user.
$ java Main.java 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
In this article we have have worked with Java Stream predefined and custom collectors.
Author
List all Java tutorials.