Stream API in Java 8

Continuing the series of Java 8 language features tutorial, after introducing lambda expressions in one of the previous posts, let me present the new Stream API in Java 8 in this post.

What is the Stream API

Among the many new language features, Java 8 also brings the Stream API. Streams represent an abstraction layer allowing the developer to process the underlying data in a declarative way.

  • A stream is a sequence of elements (objects, primitive types) from the stream source. Therefore, stream is not a data structure and it doesn’t store the elements it works with.
  • An operation on a stream produces a result without modifying its source. For example filtering a stream creates a new stream with only the filtered elements in it.
  • Many of the stream operations are lazily-evaluated. This allows for automatic code optimizations and a short-circuit evaluation.
  • Streams can be of a finite or infinite size.
  • Streams are consumable. This means that the elements of a stream are always visited only once, similar to an Iterator.
  • Streams allow effortless code parallelism.

Let’s see two simple code examples now. We have a list of integers and want to transform it to a sorted list of strings of maximal size of five elements containing string representation of numbers larger or equal 10.

The first uses traditional approach of iterating the collection manually:

List<Integer> numbers = new ArrayList<>();
numbers.addAll(Arrays.asList(1, 20, 3, 10, 20, 30, 4, 50, 80, 1, 2));

int i = 0;
List<String> number_str = new ArrayList<>(5);
for (Integer num : numbers) {
    if (i >= 5)
        break;

    if (num >= 10) {
        number_str.add(String.format("Number %d", num));
        i++;
    }
}

number_str.sort(Comparator.naturalOrder());

Now, let’s rewrite the code using Java’s 8 Stream API:
List<Integer> numbers = new ArrayList<>();
numbers.addAll(Arrays.asList(1, 20, 3, 10, 20, 30, 4, 50, 80, 1, 2));

List<String> number_str = numbers.stream()
        .filter(num -> num >= 10)
        .limit(5)
        .sorted()
        .map(num -> String.format("Number %d", num))
        .collect(Collectors.toList());

Creating a stream

From the example above, you have seen that a stream can be created from any Collection using the stream() or parallelStream() methods. When the underlying data structure is an array, use Arrays.stream(Object[]) instead.

Stream class provides several static factory methods to easily create a new stream: Stream.of(Object[]), IntStream.range(int, int), Stream.iterate(Object, UnaryOperator), Stream.generate(Supplier<T>) or Stream.empty(). They allow creating a stream from elements of an array, from a range of numbers, by repeatedly applying an operator to an object, by supplying an object generating values or simply creating an empty stream respectively.

Let’s see a few examples of stream creation:

// A stream from an array:
Stream<String> s = Arrays.stream(new String[] {"A", "B", "C"});

// A stream from elements given as arguments:
Stream<String> s = Stream.of("A", "B", "C");

// A stream of integers from 0 to 10:
IntStream i = IntStream.range(0, 10);

// An infinite stream of integers from 0 increasing by one:
Stream<Integer> s = Stream.iterate(0, n -> n + 1);

// An infinite stream of randomly generated numbers:
Stream<Double> s = Stream.generate(Math::random);

Apart from these, a number of methods that yield streams have been added to the Java API. For instance, the static method Files.lines() returns a stream of all lines in a file or Pattern class now contains method splitAsStream() that splits a string by a regular expression and returns a stream.

Let’s have a look at what operations can be applied to a stream to transform it and get results from it.

Transforming a stream – intermediate operations

Transforming stream operations read data from it and put the transformed data into a new stream. Let’s divide the operations into two categories and describe them separately.

Filtering

There are several operations that can be used to filter elements from a stream. Let’s make a list of the most common ones and then see their usage in code:

  • filter(Predicate<T>) – takes a predicate as an argument (a function from T to boolean) and returns a stream containing all the elements that match given predicate;
  • distinct – returns a stream with unique elements;
  • limit(long) – returns a stream with maximal size of given argument;
  • skip(long) – returns a stream with the first N elements (given as argument) discarded.

To demonstrate, let’s see all of the described operations in action. We have a stream of names and want to filter them like this: skip the first one, filter out all that don’t start with letter B, keep only unique names and limit their count to 3. Finally, the filtered names are to be stored in a list of strings. The resulting list will contain three elements: “Barbara”, “Bob”, “Britney”.

Stream<String> s = Stream.of("Amy", "Emily", "Barbara", "Bob", "Barbara", "Britney", "Billy");
List<String> filtered = s.skip(1)
        .filter(n -> n.startsWith("B"))
        .distinct()
        .limit(3)
        .collect(Collectors.toList());

Mapping

Often, you’ll want to transform the values in a stream. For that purpose, there is the map operation which takes the transformation function as its parameter. The function is applied to each element of the stream, producing a new transformed stream as a result.

As an example, we use it to get information about the length of each string in a stream and save them into a list:

Stream<String> s = Stream.of("Java", "C++", "Python");
List<Integer> len = s.map(String::length).collect(Collectors.toList());

Getting results from a stream – terminal operations

To obtain the final results from a (possibly transformed) stream, terminal operations can be used. Terminal operations are of different types, returning various types of data (boolean value, object or elements contained in a collection). Let’s have a look at the categories of terminal operations Java 8 provides.

Reducing and collecting

We have already seen the usage of the collect operation which combines all elements in a stream into a list. However, there is another technique of collecting values from a stream available. The operation is called reduce. It repeatedly applies an operation on each element until a final result is produced. The arguments of reduce are: an initial value and a BinaryOperator<T> to combine two elements and produce a new value.

Let’s see some examples of its usage:

Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
int sum = s.reduce(0, (a, b) -> a + b);

Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
int product = s.reduce(1, (a, b) -> a * b);

Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
int max = s.reduce(1, Integer::max);

Another simple example of getting a result from a stream is the method count. Primitive type streams such as IntStream, LongStream and DoubleStream provide other convenient methods such as average, sum or summaryStatistics.

Finding and matching

It is quite a common requirement to determine whether some elements match a given property. With Stream API in Java, you can use the anyMatch, allMatch, and noneMatch operations to do this. They all take a predicate as an argument and return a boolean value as a result. Let’s see an example operation on a stream that checks that all the numbers in the stream are larger than 0 (which they are):

Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
boolean positive = s.allMatch(n -> n > 0);

Additionally, the Stream API provides two operations for finding an element in a stream. They are findFirst and findAny. They can be conveniently used together with other stream operations, such as filter. Both of the finding operations return an instance of Optional class (more on the Optional class can be found in the following subsection).

Optional class

An Optional<T> object is a wrapper for either an object of type T or for no object. It can be understood as a substitution for a null value of an object. However, it provides methods that, when used correctly, make it a much safer alternative.

Let’s see some examples to better understand the correct usage of Optional objects. For example, it allows adding a value to result list only when the value is present:

optionalValue.ifPresent(v -> results.add(v));

Another example is substituting the missing value with a default one:
String result = optionalValue.orElse("NONE");

The last example prints the first value in a filtered stream found. If there is no such value, nothing is printed out:
numbers.stream()
    .filter(n -> n > 0)
    .findFirst()
    .ifPresent(System.out::println);

Parallel streams

Stream API makes working with data in parallel simple and error-prone. There are however a few rules you have to follow in order to benefit from parallel execution.

First of all, a stream has to be either created as parallel or converted to it. When creating a stream from a collection, you can use Collection.parallelStream() to create a parallel stream. Otherwise, the parallel method converts any sequential stream into a parallel one. For example:

Stream<String> parStream = Stream.of(wordArray).parallel();

When the terminal method executes, all lazy intermediate stream operations are run in parallel. It is important however, that the operations have to be stateless and can be executed in arbitrary order.

Some operations can be optimized to run in parallel even more when ordering of the values is of no importance to you. You can mark the operation so by calling the unordered method on a stream.

Conclusion

In this post, Stream API from Java 8 has been introduced. Streams have been described in detail as well as several ways of how to create them. The main intermediate operations (filtering and mapping) allowing to transform a stream have been presented. Terminal operations such as reduction, matching and collecting have been described. Parallel execution of operations on streams has been presented.

Next, you can read one of my previous posts about lambda expressions in Java 8 and lambda expressions in Android.

Sources

Share this: