How to add Java 8 Stream to your Android legacy project

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:

  • A source, such as a Collection, array, or generator function.
  • Zero or more intermediate operations. Intermediate operations don’t start processing elements until you invoke a terminal operation - which is why they’re considered lazy. For example, calling 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.
  • A terminal operation, such as 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&lt;T&gt;) 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);

Also you can find some interesting examples here and here.