Introducing StreamsJ8 Home « Introducing Streams

The Stream API was introduced in Java8 and is the biggest addition to the language since the introduction of generics for type safety in Java5.

So what is a stream? A stream can be thought of as a sequence of elements of specific type that can have aggregate operations applied to them. The input source to a stream can be from Collections, Arrays or I/O resources.

Although on the surface a stream might seem similar to a collection, allowing retrieval and transformation of data, there are major differences between the two. Before we elaborate further on this lets look at some code where we iterate over a collection within the Employee class and then do the same thing declaratively using a stream version so we can see some stream code in action.

Employee Class Top

Following is an Employee class we will use to test our streams in this section of the site.


package info.java8;

import java.time.LocalDate;
import java.time.chrono.IsoChronology;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/* Employee class for testing our streams */
public class Employee {

    public enum Sex {
        MALE, FEMALE, NON_BINARY
    }

    private final String name;
    private final Sex gender;
    private final LocalDate dateOfBirth;
    private final double salary;
    private final List<String> roles;

    // Constructor
    public Employee(String name, Sex gender, LocalDate dateOfBirth, double salary, List<String> roles) {
        this.name = name;
        this.gender = gender;
        this.dateOfBirth = dateOfBirth;
        this.salary = salary;
        this.roles = roles;
    }

    // Getters
    public String getName() {
        return name;
    }

    public Sex getGender() {
        return gender;
    }

    public LocalDate getDateOfBirth() {
        return dateOfBirth;
    }

    public double getSalary() {
        return salary;
    }

    public List<String> getRoles() {
        return roles;
    }

    // Other methods
    public int getAge() {
        return dateOfBirth
                .until(IsoChronology.INSTANCE.dateNow())
                .getYears();
    }

    public static List<Employee> listOfStaff() {

        List<Employee> staff = new ArrayList<>();
        staff.add(new Employee("Jack", Employee.Sex.MALE,
                IsoChronology.INSTANCE.date(1954, 12, 2), 49999.00,
                new ArrayList<>(Arrays.asList("Manager", "Director"))));
        staff.add(new Employee("Jill", Employee.Sex.FEMALE,
                IsoChronology.INSTANCE.date(1995, 10, 25), 24999.00,
                new ArrayList<>(Arrays.asList("Secretary", "Manager", "Personnel"))));
        staff.add(new Employee("Dorothy", Employee.Sex.FEMALE,
                IsoChronology.INSTANCE.date(1972, 4, 7), 21999.00,
                new ArrayList<>(Arrays.asList("Secretary", "Receptionist"))));
        staff.add(new Employee("Bert", Employee.Sex.MALE,
                IsoChronology.INSTANCE.date(1968, 11, 5), 21999.00,
                new ArrayList<>(Arrays.asList("Clerk", "Receptionist"))));
        staff.add(new Employee("Mary", Employee.Sex.FEMALE,
                IsoChronology.INSTANCE.date(2001, 3, 3), 16999.00,
                new ArrayList<>(Arrays.asList("Trainee", "Receptionist"))));
        staff.add(new Employee("Derek", Employee.Sex.NON_BINARY,
                IsoChronology.INSTANCE.date(1987, 7, 17), 32499.00,
                new ArrayList<>(Arrays.asList("Manager", "Sales"))));
        staff.add(new Employee("Jane", Employee.Sex.FEMALE,
                IsoChronology.INSTANCE.date(1960, 12, 3), 56999.00,
                new ArrayList<>(Arrays.asList("Director", "Accountant"))));
        staff.add(new Employee("Matthew", Employee.Sex.MALE,
                IsoChronology.INSTANCE.date(2002, 4, 7), 12999.00,
                new ArrayList<>(Arrays.asList("Personnel", "Receptionist"))));

        return staff;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", gender=" + gender +
                ", dateOfBirth=" + dateOfBirth +
                ", salary=" + salary +
                ", roles=" + roles +
                '}';
    }
}

Building the Employee class produces the following output:

Build Employee class
Screenshot 1. Building the Employee class.

As you can see the Employee class builds fine.

Our First Stream Top

Ok, lets look at some code where we iterate over a collection within the Employee class and then do the same thing declaratively using a stream version so we can see some stream code in action.

Following is a TestEmployeeAge class to demonstrate this:


package info.java8;

/*
  Test our Employee class
*/
public class TestEmployeeAge {

    public static void main(String[] args) {

        // Test iteratively
        int count = 0;

        for(Employee e : Employee.listOfStaff()) {
            if (e.getAge() > 21) {
                count++;
            }
        }
        System.out.println("Iterative count of employees: " + count);


        // Test declaratively
        long count2 = Employee.listOfStaff().stream()
                .filter(e -> e.getAge() > 21)
                .count();

        System.out.println("Declarative count of employees: " + count2);
    }
}

The above code uses the stream() method of the Collection<E> interface which can be used to turn any collection into a stream. There are other ways to stream data such as for arrays or I/O and these will be discussed in subsequent lessons.

Building and running the TestEmployeeAge class produces the following output:

Run test employee age class
Screenshot 2. Running the TestEmployeeAge class.

As you can see from the screenshot the results are the same but the stream version is certainly easier to read than the iterative version as we are not having to search within the code for the filtering and counting operations as they are the actual names of the methods used, these being filter() and count().

We used the filter() intermediate operator which returns a stream consisting of the elements of this stream that match the given predicate. In our example this was staff older than 21.

We also used the count() terminal operator which returns the count of elements in this stream.

We will go into more detail about intermediate and terminal operators in the Stream Operations Overview leson.

Streams & Collections Differences Top

Now we have had a first look at how streams work it is a good time to discuss the differences between collections and streams.

  • Streams operate in a declarative rather than iterative way, in other words streams describe what to do, rather than how to do it as in a collections based approach.
  • There is no persistence of elements within a stream although the elements can be stored in an underlying collection or generated when needed.
  • There is no mutation of elements within a stream, so if the source is from a list the stream will preserve the ordering of the list. The filter() method above doesn't remove elements from the stream but creates a new stream with the filtered elements.
  • Stream operations are lazily constructed where possible (demand driven) and only executed when the result is required. Collections are eagerly constructed (supply driven) and executed before any result is required.
  • Streams are consumed after use, meaning they can only be traversed once, although a new stream can be created from the data source if still available. This obviously differs from collections which can be traversed multiple times.
  • Streams use internal iteration so this work is done for you whereas with collections you are responsible for iteration.

Another interesting aspect of streams is that they can be processed in parallel without any of the overheads and additional coding associated with concurrency.

We will go into much more detail about running streams in parallel in the Parallel Streams lesson later in the section, for now the following code snippets show how easy it is to turn a stream into a parallel stream.



// Stream
long count2 = Employee.listOfStaff().stream()
        .filter(e -> e.getAge() > 21)
        .count();


// Parallel Stream
long count2 = Employee.listOfStaff().parallelStream()
        .filter(e -> e.getAge() > 21)
        .count();


All we do is change the stream() method to the parallelStream() method and voila our stream can now run the filter() and count() methods in parallel!

The declarative code used for the streams above is typical when working with streams and can be thought of as a pipeline:

  1. Create a stream from a source.
  2. Specify zero or more intermediate operations that return a stream to be processed by the next operation.
  3. Specify zero or one terminal operation that will invoke the lazy operations preceding it (the intermediate operations) culminating in either a void or non-stream result.
  • Although you can specify a stream with intermediate operations and no terminal operation this is pointless as intermediate operations are lazy and so the stream will never be processed.
  • Another important point to remember when using streams is that if your streaming from a collection its important that the collection is not modified during streaming, regardless of thread safety. This is referred to as inteference in the Java documentation and applies to both streams and parallel streams. A point to remember here is that streams are lazily constructed and so collection modification up until the point when the terminal operation executes is fine.
  • Related Quiz

    Streams Quiz 1 - Introducing Streams Quiz

    Lesson 1 Complete

    In this lesson we introduced streams which are new in Java8.

    What's Next?

    In the next lesson we look at stream pipelines.