The Stream API offers an alternative, "pipes-and-filters" approach to collections processing.
Prior to Java 8, you manipulated collections manually, typically by iterating over the collection and operating on each element in turn. This explicit looping required a lot of boilerplate, plus it’s difficult to grasp the for-loop structure until you reach the body of the loop.
The Stream API gives you a way of processing data more efficiently, by performing a single run over that data - regardless of the amount of data you’re processing, or whether you're performing multiple computations.
The Stream API processes data by carrying values from a source, through a series of computational steps, known as a stream pipeline. A stream pipeline is composed of the following:
Collection
, array, or generator function.Stream.filter()
on a data source merely sets up the stream pipeline; no filtering actually occurs until you call the terminal operation. This makes it possible to string multiple operations together, and then perform all of these computations in a single pass of the data. Intermediate operations produce a new stream (for example, filter
will produce a stream containing the filtered elements) without modifying the data source, so you’re free to use the original data elsewhere in your project, or create multiple streams from the same source.Stream.forEach
. When you invoke the terminal operation, all of your intermediate operations will run and produce a new stream. A stream isn’t capable of storing elements, so as soon as you invoke a terminal operation, that stream is considered "consumed" and is no longer usable. If you do want to revisit the elements of a stream, then you’ll need to generate a new stream from the original data source.There are various ways of obtaining a stream from a data source.
Stream.of()
creates a stream from individual values:
Stream<String> stream = Stream.of("A", "B", "C");
IntStream.range()
creates a stream from a range of numbers:
IntStream i = IntStream.range(0, 20);
There are a ton of operations that you can use to perform functional-style computations on your streams. In this section, I’m going to cover just a few of the most commonly used stream operations.
Map
The map()
operation takes a lambda expression as its only argument, and uses this expression to transform the value or the type of every element in the stream. For example, the following gives us a new stream, where every String
has been converted to uppercase:
Stream<String> myNewStream = myStream.map(s -> s.toUpperCase());
Filter
The filter(Predicate<T>)
operation lets you define filtering criteria using a lambda expression. This lambda expression must return a boolean
value that determines whether each element should be included in the resulting stream. For example, if you had an array of strings and wanted to filter out any strings that contained less than three characters, you’d use the following:
Stream.of(myArray) .filter(s -> s.length() > 3) .forEach(System.out::println);
Sorted
This operation sorts the elements of a stream. For example, the following returns a stream of numbers arranged in ascending order:
List<Integer> list = Arrays.asList(10, 11, 8, 9, 22); Stream.of(list) .sorted() .forEach(System.out::println);
You collect the results from a stream using a terminal operation, which is always the last element in a chain of stream methods, and always returns something other than a stream.
There are a few different types of terminal operations that return various types of data. Let's look at two of the most commonly used terminal operations.
The Collect
operation gathers all the processed elements into a container, such as a List
or Set
. Java 8 provides a Collectors
utility class, so you don’t need to worry about implementing the Collectors
interface, plus factories for many common collectors, including toList()
, toSet()
, and toCollection()
.
The following code will produce a List
containing red shapes only:
Stream.of(shapes) .filter(s -> s.getColor().equals("red")) .collect(Collectors.toList());
The forEach()
operation performs some action on each element of the stream, making it the Stream API’s equivalent of a for-each statement.
If you had an items
list, then you could use forEach
to print all the items that are included in this List:
items.forEach(item->System.out.println(item));
In the above example we’re using a lambda expression, so it's possible to perform the same work in less code, using a method reference:
items.forEach(System.out::println);
The Lightweight-Stream-API library backports the Stream API to Java 7 and lower by rewriting the API using iterators. The library provides many of the stream operators in original Java 8 implementation plus it also includes several new ones like: sortBy
, groupBy
, chunkBy
, sample
, slidingWindow
, and many more.
Including the library into your Android project is a matter of adding a single line to the dependencies in your build.gradle file:
android { compileSdkVersion 26 ... compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { ... compile 'com.annimon:stream:1.1.9' }
Example
import com.annimon.stream.Stream; ... Stream.of(Arrays.asList(10, 11, 8, 9, 22)).sorted().forEach(System.out::println);