Introduction to Streams


Introduction to Streams
Streams are a sequence of elements that can be processed in a lazy way.
The Stream API is a feature introduced in version 8 of the Java platform that provides an efficient way to process collections. In Java 9, several improvements were made.
The Stream API is based on the concept of a pipeline of operations that can be applied to a stream of elements. This pipeline can be divided in three parts:
Stream creation
Intermediate operations
Terminal operations
A stream pipeline consists of a stream source, followed by zero or more intermediate operations, and finally a terminal operation. Notice that the terminal operation is the only one that returns a value.
Example of a stream pipeline without any intermediate operation:
streamOfEmployees
.foreach(e -> System.out.println(Employee::getName));
That was the formal definition of a stream. In practice, it can replace many loops, resulting in a much more readable and less error-prone code.
public List<Integer> getEvenNumbers(List<Integer> listOfNumbers) {
List<Integer> listOfEvenNumbers = new ArrayList<>();
for (Integer number : listOfNumbers) {
if (number % 2 == 0) {
listOfEvenNumbers.add(number);
}
}
return listOfEvenNumbers;
}
Using streams:
public List<Integer> getEvenNumbers(List<Integer> listOfNumbers) {
return listOfNumbers
.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
}
This is called a declarative way of writing code, because we are writing what result we want to achieve, without worrying about how to do it.
Stream creation
A stream can be created from an array or a collection.
private Employee[] employees = {
new Employee("Jason", 30),
new Employee("Maria", 32),
};
var streamOfEmployees = Stream.of(employees)
private List<Employee> employeesList = Arrays.asList(employees);
var streamOfEmployees = employeesList.stream()
We can use factory methods to create streams.
var streamOfIntegers = Stream.of(1, 2, 3, 4, 5)
var streamOfEmployees = Stream.of(
new Employee("Jason", 30),
new Employee("Maria", 32)
)
or using Stream.builder()
Stream.Builder<Employee> streamBuilder = Stream.builder();
streamBuilder.accept(new Employee("Jason", 30));
streamBuilder.accept(new Employee("Maria", 32));
var streamOfEmployees = streamBuilder.build();
Stream operations
forEach
It is used to perform an action by calling the supplied transformation (lambda expression) on each element of the stream.
streamOfEmployees.forEach(e -> e.getName());
forEach() is a terminal operation.
map
It produces a new stream whose elements are the results of applying the given function to the elements of an existing stream.
var streamOfNames = streamOfEmployees
.map(Employee::getName) // or .map(e -> e.getName())
.collect(Collectors.toList());
Notices map is an intermediate operation, therefore we have to use a Collector after the transformation.
filter
It is similar to map but it filters the elements of the stream.
var streamOfNames = streamOfEmployeesInTheirThirties
.filter(e -> e != null)
.filter(e -> e.getAge() >= 30)
.collect(Collectors.toList());
findFirst
It returns an Optional containing the first element of the stream.
var employee = streamOfEmployees
.map(employeeRepository::findById)
.findFirst()
.orElse(null);
findFirst() is a terminal operation.
flatMap
It is similar to map but it flattens the elements of the stream. An example is converting a list of lists to a list of elements.
var ListOfNames = listOfEmployees
.stream()
// .map(Employee::getName)
.flatMap(Collection::stream)
.collect(Collectors.toList());
Other stream operations
The following ones are only mentioned because they are very common. Their names are self explanatory:
distinct()
sorted()
limit()
skip()
peek()
anyMatch()
allMatch()
noneMatch()
min()
max()
Terminal operations
count
It returns the number of elements in the stream.
var numberOfEmployees = streamOfEmployees.count();
reduce
It reduces the stream to a single value.
var sumOfAges = streamOfEmployees
.map(Employee::getAge)
.reduce(0, Integer::sum);
Unlike other languages, reduce
must receive a starting value.
A reduction operation, also called a fold, takes a sequence of elements and combines them into a single value by repeated applications of a combining operation. Other reduction operations are findFirst(), min() and max().
var listOfNumber = List.of(1, 2, 3, 4, 5);
var sumOfNumbers = listOfNumber
.stream()
.reduce(0, Integer::sum);
or
var sumOfNumbers = listOfNumber
.mapToInt(Integer::intValue)
.sum();
collect
It is used to collect the elements of the stream into a container.
The strategy for this operation is provided via the Collector
interface. There are several implementations of this interface, but the most common ones are the ones related to the three collection types:
Collectors.toList()
Collectors.toSet()
Collectors.toMap()
toArray
It returns an array containing the elements of the stream.
Employee[] ArrayOfEmployees = streamOfEmployees
.toArray(Employee[]::new);
Lazy evaluation
In the beginning of the article we mentioned that the Stream API is lazy. This means that the stream pipeline is not executed until the terminal operation is called.
Why should we care? Let's see an example.
Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 1);
List<Integer> listOfNaturalNumbers = infiniteStream
.skip(3)
.limit(10)
.collect(Collectors.toList());
System.out.println(listOfNaturalNumbers);
The output is [4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
.
The stream pipeline above will be executed only when the terminal operation is called. So all intermediate operations are lazy. This technique allows computations on finite streams to be completed in finite time. In other words, the evaluation of the functions is separated from the description of the transformation. This is the definition of lazy evaluation.
One of the most important characteristics of the streams is that they allow for significant optimizations through lazy evaluations.
Parallel Streams
Parallel streams are streams that are executed in parallel.
Stream<Integer> parallelStream = Stream.of(1, 2, 3, 4, 5)
.parallel()
.map(n -> n * n);
As is the case when writing multi-threaded code, we need to be aware of a few things while using parallel streams:
We need to ensure that the code is thread-safe. Take special care if the operations performed in parallel modify shared data.
We should not use parallel streams if the order in which operations are performed or the order returned in the output stream matters. For example, operations like findFirst() may generate different results in the case of parallel streams.
Also, we should ensure that it’s worth making the code execute in parallel. Understanding the performance characteristics of the operation in particular, but also of the system as a whole, is naturally very important.
Infinite Streams
Also called unbounded streams, infinite streams are used to perform operations while the elements are still being generated. However, when using map, all the elements are already populated.
There are two ways to generate infinite streams:
generate
// Print ten random numbers
Stream.generate(Math::random)
.limit(10)
.forEach(System.out::println);
iterate
It takes an initial value, called the seed and a function that generates the next element using the previous value. iterate() is statefull, therefore it may not be useful in parallel streams:
Stream<Integer> evenNumbersStream = Stream.iterate(2, i -> i * 2)
public getFirstEvenNumbers(int size) {
return Stream.iterate(2, i -> i * 2) // evenNumbersStream
.limit(size)
.collect(Collectors.toList());
}
limit() is the terminating condition.
Summary
The motivations behind Java streams were explained and several examples were presented. Several of the most common transformation were explained or mentioned.
The lazy evaluation technique was explained, which is used by many methods of the Stream API and allows to perform operations on infinite streams in a finite time. Parallel streams were created, which are streams that are executed in parallel.
The stream API is a powerful tool that can be used to perform operations on collections in a declarative way. It is remarkable change for any Java programmer.
Subscribe to my newsletter
Read articles from José Ramón (JR) directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

José Ramón (JR)
José Ramón (JR)
Software Engineer for quite a few years. From C programmer to Java web programmer. Very interested in automated testing and functional programming.