Java Collections and Streams
Jakob Jenkov |
The Java Stream API provides a more functional programming approach to iterating and processing elements of e.g. a collection. The Java Stream API was added to Java in Java 8. This tutorial is only intended to explain how to use the Java Stream API in the context of the Java Collection API. For a more in-depth explanation of the Java Stream API, see my Java Stream API Tutorial.
Streams are designed to work with Java lambda expressions. Many of the examples in this text will use lambda expressions, so if you don't already know them, you should read up on them before reading this text.
Obtaining a Stream From a Collection
You obtain a stream from a collection by calling the stream()
method of the given collection.
Here is an example of obtaining a stream from a collection:
List<String> items = new ArrayList<String>(); items.add("one"); items.add("two"); items.add("three"); Stream<String> stream = items.stream();
First a List
of strings is created and three strings are added to it.
Then a Stream
of strings is obtained by calling the items.stream()
method. This is similar to how you obtain an Iterator
by calling the items.iterator()
method, but a Stream
is a different animal than an Iterator
.
Stream Processing Phases
Once you have obtained a Stream
instance from a Collection
instance, you use that
stream to process the elements in the collection.
Processing the elements in the stream happens in two steps / phases:
- Configuration
- Processing
First the stream is configured. The configuration can consist of filters and mappings. These are also referred to as non-terminal operations.
Second, the stream is processed. The processing consists of doing something to the filtered and mapped objects. No processing takes place during the configuring calls. Not until a processing method is called on the stream. The stream processing methods are also referred to as terminal operations.
Stream.filter()
You filter a stream using the filter()
method. Here is a stream filtering example:
stream.filter( item -> item.startsWith("o") );
The filter()
method takes a Predicate
as parameter.
The Predicate
interface contains a function called test()
which
the lambda expression passed as parameter above
is matched against. In other words, the lambda expression implements the Predicate.test()
method.
The test()
method is defined like this:
boolean test(T t)
It takes a single parameter and returns a boolean
. If you look at the lambda expression above,
you can see that it takes a single parameter item
and returns a boolean - the result of
the item.startsWith("o")
method call.
When you call the filter()
method on a Stream
, the filter passed as parameter
to the filter()
method is stored internally. No filtering takes place yet.
The parameter passed to the filter()
function determines what items in the stream should
be processed, and which that should be excluded from the processing. If the Predicate.test()
method of the parameter passed to filter()
returns true
for an item, that
means it should be processed. If false
is returned, the item is not processed.
Stream.map()
It is possible to map the items in a collection to other objects. In other words, for each item in the collection you create a new object based on that item. How the mapping is done is up to you. Here is a simple Java stream mapping example:
items.stream() .map( item -> item.toUpperCase() )
This example maps all strings in the items
collection to their uppercase equivalents.
Again, this example doesn't actually perform the mapping. It only configures the stream for mapping. Once one of the stream processing methods are invoked, the mapping (and filtering) will be performed.
Stream.collect()
The collect()
method is one of the stream processing methods on the Stream
interface.
When this method is invoked, the filtering and mapping will take place and the object resulting from those
actions will be collected. Here is a stream.collect()
example:
List<String> filtered = items.stream() .filter( item -> item.startsWith("o") ) .collect(Collectors.toList());
This example creates a stream, adds a filter, and collects all object accepted by the filter
in a List
. The filter only accepts items (strings) which start with the character
o
. The resulting List
thus contains all strings from the items
collection which starts with the character o
.
Stream.min() and Stream.max()
The min()
and max()
methods are stream processing methods. Once these are called,
the stream will be iterated, filtering and mapping applied, and the minimum or maximum value in the stream
will be returned.
Here is a Java Stream.min()
example:
String shortest = items.stream() .min(Comparator.comparing(item -> item.length())) .get();
The min()
and max()
methods return an Optional
instance which
has a get()
method on, which you use to obtain the value. In case the stream has no elements
the get()
method will return null.
The min()
and max()
methods take a Comparator
as parameter.
The Comparator.comparing()
method creates a Comparator
based on the
lambda expression passed to it. In fact, the comparing()
method takes a Function
which is a functional interface suited for lambda expressions. It takes one parameter and returns a value.
Stream.count()
The count()
method simply returns the number of elements in the stream after filtering has
been applied. Here is an example:
long count = items.stream() .filter( item -> item.startsWith("t")) .count();
This example iterates the stream and keeps all elements that start with the character t
,
and then counts these elements.
The count()
method returns a long
which is the count of elements in the stream
after filtering etc.
Stream.reduce()
The reduce()
method can reduce the elements of a stream to a single value.
Here is an example:
String reduced2 = items.stream() .reduce((acc, item) -> acc + " " + item) .get();
The reduce()
method takes a BinaryOperator
as parameter, which can easily
be implemented using a lambda expression. The BinaryOperator.apply()
method is the
method implemented by the lambda expression above. This method takes two parameters. The acc
which is the accumulated value, and item
which is an element from the stream. Thus,
the value created by the reduce()
function is the accumulated value after processing
the last element in the stream. In the example above, each item is concatenated to the accumulated
value. This is done by the lambda expression implementing the BinaryOperator
.
The reduce()
method taking a BinaryOperator
as parameter returns an
Optional
. In case the stream contains no elements, the Optional.get()
returns null. Otherwise it returns the reduced value.
There is another reduce()
method which takes two parameters. It takes an initial value
for the accumulated value, and then a BinaryOperator
. Here is an example:
String reduced = items.stream() .reduce("", (acc, item) -> acc + " " + item);
This example takes an empty string as initial value, and then the same lambda expression as the
previous example. This version of the reduce()
method returns the accumulated value
directly, and not an Optional
. If the stream contains no elements, the initial value
will be returned.
The reduce()
method can be combined with the filter()
method too.
Here is an example:
String reduced = items.stream() .filter( item -> item.startsWith("o")) .reduce("", (acc, item) -> acc + " " + item);
This example keeps all elements that start with the character o
, and then reduce these
elements into a single value.
Tweet | |
Jakob Jenkov |