- Java Collections Tutorial
- Java Collections - Overview
- Java Iterator
- Java Iterable
- Java Collection
- Java Collections - Generic Collections
- Java Collections
- Java List
- Java Set
- Java Collections - SortedSet
- Java Collections - NavigableSet
- Java Map
- Java Collections - SortedMap
- Java Collections - NavigableMap
- Java Queue
- Java Deque
- Java Stack
- Java Collections - hashCode() and equals()
- Java Collections - Sorting
- Java Collections and Streams
Java Collections and Streams
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();
List of strings is created and three strings are added to it.
Stream of strings is obtained by calling the
method. This is similar to how you obtain an
Iterator by calling the
method, but a
Stream is a different animal than an
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:
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.
You filter a stream using the
filter() method. Here is a stream filtering example:
stream.filter( item -> item.startsWith("o") );
filter() method takes a
Predicate as parameter.
Predicate interface contains a function called
the lambda expression passed as parameter above
is matched against. In other words, the lambda expression implements 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
item.startsWith("o") method call.
When you call the
filter() method on a
Stream, the filter passed as parameter
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
method of the parameter passed to
true for an item, that
means it should be processed. If
false is returned, the item is not processed.
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.
collect() method is one of the stream processing methods on the
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
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
List. The filter only accepts items (strings) which start with the character
o. The resulting
List thus contains all strings from the
collection which starts with the character
Stream.min() and Stream.max()
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
String shortest = items.stream() .min(Comparator.comparing(item -> item.length())) .get();
max() methods return an
Optional instance which
get() method on, which you use to obtain the value. In case the stream has no elements
get() method will return null.
max() methods take a
Comparator as parameter.
Comparator.comparing() method creates a
Comparator based on the
lambda expression passed to it. In fact, the
comparing() method takes a
which is a functional interface suited for lambda expressions. It takes one parameter and returns a value.
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
and then counts these elements.
count() method returns a
long which is the count of elements in the stream
after filtering etc.
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();
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
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
reduce() method taking a
BinaryOperator as parameter returns an
Optional . In case the stream contains no elements, the
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.
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.