Blog

Introduction to Java 8 Streams API

One of the key features introduced in Java 8 is the Streams API. Streams allow parallel processing of Collections and provide clean and concise operators for iteration. Without Streams, transforming a Collection may require creating intermediate in-memory Collections and several multi-line loops. With Streams, however, multiple operations can be performed on a Collection within a single line of code, and the results are lazily computed only when needed. In this post, we’ll look at how to use the Streams API in several common scenarios, and compare each to a corresponding Collections-only implementation.

The basic structure of Streams

In total, a Stream consists of three parts:

  • a source
  • a series of aggregate operations
  • a terminating reduction operator

The source of the stream is simply the collection to be processed. To create a stream, call Collection.stream(), which was added to the Collection interface in Java 8. (Note: this will create a serially-processed stream. By calling .parallelStream() instead, parallelization will be used.)

The aggregate operations operate on the Collection’s stream. Some of the most commonly used operations are map, filter, and sort. These operations take Lambda expressions (see below) as arguments and will return another Stream. This allows pipelining, or chaining together of the operations, so that a mapped result could then be filtered or further sorted.

Once a series of these operations is written out as desired, we’ll want to compute and use a result. This is known as reduction. This can be done by collecting the result into a new Collection, by iterating on the result, or by calling one of the many other reduction operations included in the API (e.g. sum, average). For example, calling stream().collect(Collectors.toList()) will return the computed result as a list. Calling stream().forEach(…) allows a Lambda to be passed into the forEach function, so code can be run for each element of the result.

Quick Aside: Lambda Expressions

Before Lambda Expressions were introduced into Java 8, passing functions as method arguments was not directly possible. Instead, an anonymous class could be used to achieve a similar result. The function belonging to this class could be implemented as needed – for example, to apply a specific getInfo method to collection of Person objects:

doSomething(
   people,
   new PersonOperator() {
      @Override
      public String getInfo(Person p) {
         return p.getName() + " is " + p.getAge() + " years old.";
      }
   }
)

The above syntax is workable, but lambda expressions greatly simplify this:

doSomething(
   people,
   (Person p) -> p.getName() + " is " + p.getAge() + " years old."
)

The fat arrow (->) denotes that the “function body” on the RHS should be applied to arguments of the type specified on the LHS, returning the result. This function does not need to belong to any class: it is an anonymous function. There is more that could be said about Lambdas, but this is a simple explanation intended to make the Stream examples easier to comprehend.

Stream Examples

Now that we’ve covered the basic structure of Streams,  let’s look at some simple but powerful examples. For these examples, we’ll use the following Person class, and the object Collection<Person> people.

class Person {
//private fields...
   public getName() {
      return name;
   }   
   public getAge() {
      return age;
   }
}

.map()

Suppose we want to create a list of just the people’s names. Without Streams, our code would be:

List<String> names = new ArrayList<>();
for(Person p : people) {
   names.add(p.getName());
}

That’s not bad, but we can use the map operator of Streams to greatly simplify this. map will take a stream, apply a given function to each element, and return a stream with the outputs of that function. So in our example, we can use map to transform a stream of Person (the input) into a stream of Person.getName() Strings:

List<String> names = people.stream().map(p -> p.getName()).collect(Collectors.toList());

We’ve simply mapped each Person of the incoming Stream to its name. Collecting the Stream results in a new List of names.

.filter()

If we want to only select elements from a Collection that match a certain criteria, we’d do this without Streams as:

List<String> names = new ArrayList<>();
for(Person p : people) {
 if(p.getAge() < 25) {
  names.add(p.getName());
 }
}

This will produce a list of names of the People who are under 25 years old. We can use the filter operation to achieve the same result. Think of it as a WHERE clause in a SQL query:

List<String> names = people.stream().filter(p -> p.getAge() < 25)
   .map(p -> p.getName()).collect(Collectors.toList());

Notice that we pass a Lambda expression that returns a boolean. All elements of the stream for which the condition is true are kept in the resulting stream, while the non-matching elements are filtered out. Also notice that we have chained operations together, first filtering the Person stream by age, then mapping it to getName().

Sorting with .sorted()

Instead of calling Collections.sort(…) the Streams API includes a convenient sorted function to return a sorted stream.

List<String> names = people.stream().filter(p -> p.getAge() < 25)
   .map(p -> p.getName()).sorted().collect(Collectors.toList());

The list of names will be sorted descending alphabetically. (The default comparator order.)

Other reductions

These examples have only used collect(Collectors.toList()) as a reduction operation so far. Here are a few more useful ones. To return a comma-separated list of Strings:

String names = people.stream().filter(p -> p.getAge() < 25)
   .map(p -> p.getName()).sorted().collect(Collectors.joining(“, “));

Another common scenario is testing whether all or any of the elements in a Collection match a given condition. To do this without Streams, we could write the following code to determine if all people are 25 or older:

boolean allOlder = true;
for(Person p : people) {
   if(p.getAge() < 25) {
      allOlder = false;
      break;
   }
}
if(allOlder) {
   //do something
}

Stream’s allMatch() operator can be used much more succinctly, only returning true if the given lambda returns true for all elements:

if(people.stream().allMatch(p -> p.getAge() >= 25)) {
   //do something...
}

The Stream version is obviously much more concise and straightforward to understand than having to read through multiple control flow statements. In general one advantage of using Streams is that it is easy to tell what each operation step does without having to look through nested loop and conditional statements.

Further resources

This is merely an introduction to the most basic uses of Streams. In a future post we’ll explore more scenarios with complicated Collections and Maps, and other Stream and aggregate operations. Learn more about what is available in the Java 8 Stream API here.

Categories: Blog

Tags: , ,

25 May, 2017