Overview
The addition of the Stream API was a significant enhancement in Java, introducing a powerful way to handle collections with functional-style operations. In Java 9, several improvements were made to this feature, adding more capabilities and refining its functionality. This tutorial will explore both the original Stream API and the enhancements introduced in Java 9, focusing on practical examples to illustrate their usage.
To understand this material, you need to have a basic, working knowledge of Java 8 (lambda expressions, Optional, method references).
Introduction
Java Streams, distinct from Java I/O streams (e.g., FileInputStream), are designed to facilitate efficient data processing operations. They act as wrappers around data sources, enabling functional-style operations without modifying the underlying data.

Streams are not data structures but tools for performing operations like map-reduce transformations on collections. This functionality—java.util.stream—supports functional-style operations on streams of elements.
This tutorial will guide you through the core concepts and new features, starting with basic stream operations and progressing to more advanced topics. Let’s dive into a few simple examples of stream creation and usage before getting into terminology and core concepts.
Java Stream Creation
Java Stream Creation
Let’s first obtain a stream from an existing array:
private static Employee[] arrayOfEmps = { new Employee(1, "Jeff Bezos", 100000.0), new Employee(2, "Bill Gates", 200000.0), new Employee(3, "Mark Zuckerberg", 300000.0) }; Stream.of(arrayOfEmps);
We can also obtain a stream from an existing list:
private static List<Employee> empList = Arrays.asList(arrayOfEmps); empList.stream();
And we can create a stream from individual objects using Stream.of():
Stream.of(arrayOfEmps[0], arrayOfEmps[1], arrayOfEmps[2]);
Or simply using Stream.builder():
Stream.Builder<Employee> empStreamBuilder = Stream.builder(); empStreamBuilder.accept(arrayOfEmps[0]); empStreamBuilder.accept(arrayOfEmps[1]); empStreamBuilder.accept(arrayOfEmps[2]); Stream<Employee> empStream = empStreamBuilder.build();
There are also other ways to obtain a stream, some of which we’ll see in the sections below.
Java Stream Operations
Let’s now see some common usages and operations we can perform on and with the help of the stream support in the language.
forEach
forEach() is the simplest and most common operation; it loops over the stream elements, calling the supplied function on each element.
The method is so common that it has been introduced directly in Iterable, Map etc.:
@Test public void whenIncrementSalaryForEachEmployee_thenApplyNewSalary() { empList.stream().forEach(e -> e.salaryIncrement(10.0)); assertThat(empList, contains( hasProperty("salary", equalTo(110000.0)), hasProperty("salary", equalTo(220000.0)), hasProperty("salary", equalTo(330000.0)) )); }
This will effectively call the salaryIncrement() on each element in the empList.
forEach() is a terminal operation, which means that, after the operation is performed, the stream pipeline is considered consumed, and can no longer be used. We’ll talk more about terminal operations in the next section.
map
map() produces a new stream after applying a function to each element of the original stream. The new stream could be of a different type.
The following example converts the stream of Integers into the stream of Employees:
@Test public void whenMapIdToEmployees_thenGetEmployeeStream() { Integer[] empIds = { 1, 2, 3 }; List<Employee> employees = Stream.of(empIds) .map(employeeRepository::findById) .collect(Collectors.toList()); assertEquals(employees.size(), empIds.length); }
Here, we obtain an Integer stream of employee IDs from an array. Each Integer is passed to the function employeeRepository::findById()—which returns the corresponding Employee object. This effectively forms an Employee stream.
collect
We saw how collect() works in the previous example; it’s one of the common ways to get stuff out of the stream once we are done with all the processing:
@Test public void whenCollectStreamToList_thenGetList() { List<Employee> employees = empList.stream().collect(Collectors.toList()); assertEquals(empList, employees); }
collect() performs mutable fold operations (repackaging elements to some data structures and applying some additional logic, concatenating them, etc.) on data elements held in the Stream instance.
The strategy for this operation is provided via the Collector interface implementation. In the example above, we used the toList collector to collect all Stream elements into a List instance.
filter
Next, let’s have a look at filter(). This produces a new stream that contains elements of the original stream that pass a given test (specified by a predicate).
Let’s have a look at how that works:
@Test public void whenFilterEmployees_thenGetFilteredStream() { Integer[] empIds = { 1, 2, 3, 4 }; List<Employee> employees = Stream.of(empIds) .map(employeeRepository::findById) .filter(e -> e != null) .filter(e -> e.getSalary() > 200000) .collect(Collectors.toList()); assertEquals(Arrays.asList(arrayOfEmps[2]), employees); }
In the example above, we first filter out null references for invalid employee ids and then again apply a filter to only keep employees with salaries over a certain threshold.
findFirst
findFirst() returns an Optional for the first entry in the stream. The Optional can, of course, be empty:
@Test public void whenFindFirst_thenGetFirstEmployeeInStream() { Integer[] empIds = { 1, 2, 3, 4 }; Employee employee = Stream.of(empIds) .map(employeeRepository::findById) .filter(e -> e != null) .filter(e -> e.getSalary() > 100000) .findFirst() .orElse(null); assertEquals(employee.getSalary(), new Double(200000)); }
Here, the first employee with a salary greater than 100000 is returned. If no such employee exists, then null is returned.
toArray
We saw how we used collect() to get data out of the stream. If we need to get an array out of the stream, we can simply use toArray():
@Test public void whenStreamToArray_thenGetArray() { Employee[] employees = empList.stream().toArray(Employee[]::new); assertThat(empList.toArray(), equalTo(employees)); }
The syntax Employee[]::new creates an empty array of Employee—which is then filled with elements from the stream.
flatMap
A stream can hold complex data structures like Stream<List<String>>. In cases like this, flatMap() helps us to flatten the data structure to simplify further operations:
@Test public void whenFlatMapEmployeeNames_thenGetNameStream() { List<List<String>> namesNested = Arrays.asList( Arrays.asList("Jeff", "Bezos"), Arrays.asList("Bill", "Gates"), Arrays.asList("Mark", "Zuckerberg")); List<String> namesFlatStream = namesNested.stream() .flatMap(Collection::stream) .collect(Collectors.toList()); assertEquals(namesFlatStream.size(), namesNested.size() * 2); }
Notice how we were able to convert the Stream<List<String>> to a simpler Stream<String>—using the flatMap() API.
peek
We saw forEach() earlier in this section, which is a terminal operation. However, sometimes we need to perform multiple operations on each element of the stream before any terminal operation is applied.
peek() can be useful in situations like this. Simply put, it performs the specified operation on each element of the stream and returns a new stream that can be used further. peek() is an intermediate operation:
@Test public void whenIncrementSalaryUsingPeek_thenApplyNewSalary() { Employee[] arrayOfEmps = { new Employee(1, "Jeff Bezos", 100000.0), new Employee(2, "Bill Gates", 200000.0), new Employee(3, "Mark Zuckerberg", 300000.0) }; List<Employee> empList = Arrays.asList(arrayOfEmps); empList.stream() .peek(e -> e.salaryIncrement(10.0)) .peek(System.out::println) .collect(Collectors.toList()); assertThat(empList, contains( hasProperty("salary", equalTo(110000.0)), hasProperty("salary", equalTo(220000.0)), hasProperty("salary", equalTo(330000.0)) )); }
Post a Comment