Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
67 views

Java8 Lambda Expressions Streams

This book is a guide to using Java 8 lambda expressions and streams. It contains 13 chapters that cover topics like lambda expressions, functional interfaces, streams, and stream operations. The book is published through Leanpub, which allows authors to publish works in progress and get reader feedback to help shape the book.

Uploaded by

asmxforce
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
67 views

Java8 Lambda Expressions Streams

This book is a guide to using Java 8 lambda expressions and streams. It contains 13 chapters that cover topics like lambda expressions, functional interfaces, streams, and stream operations. The book is published through Leanpub, which allows authors to publish works in progress and get reader feedback to help shape the book.

Uploaded by

asmxforce
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 71

A Practical Guide for Java 8 Lambdas and

Streams
Mastering Java 8 Lambdas and Streams

Fu Cheng
This book is for sale at http://leanpub.com/java8-lambda-expressions-streams

This version was published on 2019-12-23

This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing
process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and
many iterations to get reader feedback, pivot until you have the right book and build traction once
you do.

© 2015 - 2019 Fu Cheng


Also By Fu Cheng
JUnit 5 Cookbook
Lodash 4 Cookbook
ES6 Generators
To my wife Andrea and my daughters Olivia and Erica
Contents

1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1

2. Lambda expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2.1 Start a thread - A simple example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2.2 Functional interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2.3 Target typing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.4 Lambda expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.5 Lexical scoping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.6 Effectively final local variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.7 Method references . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.7.1 Types of method references . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.8 Default interface methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.8.1 Static interface methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10

3. Functional interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.1 Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.1.1 Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.1.2 BiFunction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
3.1.3 Function and BiFunction with predefined types . . . . . . . . . . . . . . 13
3.2 Consumers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3.2.1 Consumer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3.2.2 BiConsumer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.2.3 Consumer with predefined types . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.2.4 Example of using Consumer . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.3 Suppliers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
3.3.1 Supplier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
3.3.2 Supplier with predefined types . . . . . . . . . . . . . . . . . . . . . . . . . 19
3.4 Predicates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.4.1 Predicate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.4.2 BiPredicate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.4.3 Predicate and BiPredicate with predefined types . . . . . . . . . . . . . 20
3.5 Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.5.1 UnaryOperator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.5.2 BinaryOperator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
CONTENTS

3.5.3 Operators with predefined types . . . . . . . . . . . . . . . . . . . . . . . . 22

4. Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.1 User-specified behavior . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.2 Basic stream concepts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.2.1 Sequential or parallel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.2.2 Encounter order . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.3 Stream operation characteristics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.3.1 Stateful or stateless . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.4 Stream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.5 Spliterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.5.1 Spliterator characteristics . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
4.5.2 Spliterators for primitive types . . . . . . . . . . . . . . . . . . . . . . . . 26
4.5.3 Spliterators to create Spliterators . . . . . . . . . . . . . . . . . . . . . 26
4.5.4 Late-binding and fail-fast . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
4.6 BaseStream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
4.7 StreamSource . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
4.8 Stream static methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
4.9 Stream sources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
4.9.1 Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
4.9.2 Collections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
4.9.3 Stream builder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.9.4 I/O channels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.9.4.1 lines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.9.4.2 list . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
4.9.4.3 walk . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
4.9.4.4 find . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
4.10 Examples for stream processing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
4.11 Stream intermediate operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
4.11.1 map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
4.11.2 flatMap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
4.11.3 filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4.11.4 distinct . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4.11.5 limit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4.11.6 skip . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
4.11.7 sorted . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
4.11.8 peek . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
4.11.9 Operations chaining . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
4.12 Stream terminal operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
4.12.1 forEach and forEachOrdered . . . . . . . . . . . . . . . . . . . . . . . . . . 38
4.12.2 reduce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
4.12.3 max and min . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.12.4 match . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
CONTENTS

4.12.5 find . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4.12.6 toArray . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.12.7 collect . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.12.8 Collectors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
4.12.8.1 Collect into collections . . . . . . . . . . . . . . . . . . . . . . . 42
4.12.8.2 groupingBy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.12.8.3 joining . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
4.12.8.4 mapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
4.12.8.5 partitioningBy . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
4.12.8.6 counting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
4.12.8.7 averaging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
4.12.8.8 summing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
4.12.8.9 maxBy and minBy . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4.12.8.10 summarizing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4.12.8.11 collectingAndThen . . . . . . . . . . . . . . . . . . . . . . . . . 48
4.12.8.12 reducing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
4.12.9 Parallel stream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
4.12.9.1 Parallel reduction . . . . . . . . . . . . . . . . . . . . . . . . . . 49
4.13 How-tos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
4.13.1 How to convert a Collection to a Map? . . . . . . . . . . . . . . . . . . . . 49
4.13.2 How to filter map entries by keys? . . . . . . . . . . . . . . . . . . . . . . . 50
4.13.3 How to filter map entries by values? . . . . . . . . . . . . . . . . . . . . . . 50
4.13.4 How to group elements by multiple conditions? . . . . . . . . . . . . . . . 50
4.13.5 How to combine multiple collections into a stream? . . . . . . . . . . . . 51
4.13.6 How to convert a stream to another stream with different type? . . . . . 51
4.13.7 How to find an element in the stream? . . . . . . . . . . . . . . . . . . . . 52
4.13.8 How to sort map entries? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
4.13.9 Exceptions handling in streams . . . . . . . . . . . . . . . . . . . . . . . . . 53

5. Optional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
5.1 What’s Optional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
5.2 Usage of Optional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
5.2.1 Simple usage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
5.2.2 Chained usage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
5.2.3 Functional usage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
5.3 How-tos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
5.3.1 How to interact with legacy library code before Optional? . . . . . . . . 59
5.3.2 How to get value from chained Optional reference path? . . . . . . . . . 59
5.3.3 How to get first value of a list of Optionals? . . . . . . . . . . . . . . . . . 60
5.3.4 How to chain method invocations with return value of Optional
objects in sequence? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
5.3.5 How to convert an Optional object to a stream? . . . . . . . . . . . . . . 63
5.3.6 How to use Optional to check for null and assign default values? . . . 64
1. Introduction
This book is not the first book about Java 8 lambda expressions and streams, and it’s definitely not
the last one. Java 8 is a Java platform upgrade which the community has been looking forward to
for a long time. Lambda expressions and streams quickly gain popularity in Java developers. There
are already a lot of books and online tutorials about lambda expressions and streams. This book is
trying to explain lambda expressions and streams from a different perspective.

• For lambda expressions, this book explains in details based on JSR 335.
• For streams, this book covers fundamental concepts of Java core library.
• This book provides how-to examples for lambda expressions and streams.
• This book also covers the important utility class Optional.

Lambda expressions and streams are easy to understand and use. This book tries to provide some
insights about how to use them efficiently.
You can purchase and down PDF/EPUB/MOBI version of this book on Leanpub¹.
¹https://leanpub.com/java8-lambda-expressions-streams
2. Lambda expressions
When people are talking about Java 8, lambda expression is always the first feature to mention.
Lambda expression in Java 8 brings functional programming style into Java platform, which has
been demanded by Java developers in the community for a long time. Lambda expression is also
widely used as a marketing term to promote Java 8 upgrade.
Lambda expressions can increase developers’ productivity in many ways. There are a lot of books
and online tutorials about Java 8 lambda expressions. This chapter is trying to explain lambda
expressions from a different perspective based on the official JSR 335: Lambda Expressions for the
JavaTM Programming Language¹. This chapter also provides how-tos for real programming tasks.

2.1 Start a thread - A simple example


Let’s start from a simple example which starts a thread and outputs some text to console, see Listing
2.1.
Listing 2.1 Start a thread and output text to console

1 public class OldThread {


2 public static void main(String[] args) {
3 new Thread(new Runnable() {
4 public void run() {
5 System.out.println("Hello World!");
6 }
7 }).start();
8 }
9 }

The small Java program in Listing 2.1 has 9 lines of code, but only one line of code (line 5) does the
real work. All the rest are just boilerplate code. To increase productivity, boilerplate code should be
removed as much as possible.
In Listing 2.1, line 1, 2, 8 and 9 are required for Java program to run, so these lines cannot be removed.
Line 3 to 7 create a new java.lang.Thread object with an implementation of java.lang.Runnable
interface, then invoke Thread object’s start() method to start the thread. Runnable interface is
implemented using an anonymous inner class.
¹https://jcp.org/en/jsr/detail?id=335
Lambda expressions 3

To simplify the code, new Runnable() in line 3 and 7 can be removed because the interface type
Runnable can be deduced from allowed constructors of Thread class. Line 4 and 6 can also be
removed, because run() is the only method in Runnable interface.
After removing boilerplate code in line 3, 4, 6 and 7, we get the new code using lambda expressions
in Listing 2.2.
Listing 2.2 Lambda expressions style to start a thread

1 public class LambdaThread {


2 public static void main(String[] args) {
3 new Thread(() -> System.out.println("Hello World!")).start();
4 }
5 }

The new Java program in Listing 2.2 only has 5 lines of code and the core logic is implemented with
only one line of code. Lambda expression () -> System.out.println("Hello World!") does the
same thing as anonymous class in Listing 2.1, but using lambda expression is concise and elegant
and easier to understand with less code. Less code means increasing productivity.
Simply put, lambda expression is the syntax sugar to create anonymous inner classes. But there are
more to discuss about lambda expressions.

2.2 Functional interfaces


As shown in Listing 2.2, we made two assumptions to remove the boilerplate code. The first
assumption is that the interface type, e.g. Runnable can be deduced from current context. The second
assumption is that there is only one abstract method in the interface. Let’s start from the second
assumption.
If an interface only has one abstract method (aside from methods of Object), it’s called functional
interface. Functional interfaces are special because instances of functional interfaces can be created
with lambda expressions or method references.
In Java SE 7, there are already several functional interfaces, e.g.

• java.lang.Runnable
• java.util.concurrent.Callable
• java.util.Comparator
• java.io.FileFilter

Java SE 8 adds a new package java.util.function which includes new functional interfaces.
Java developers don’t need to care too much about the concept of functional interfaces. If we are
trying to use lambda expressions with non-functional interfaces, compiler will throw errors, then
Lambda expressions 4

we’ll know about that. Modern IDEs can also do checks for us. For API and library authors, if an
interface is designed to be functional, it should be marked with @FunctionalInterface annotation.
Compiler will generate an error if the interface doesn’t meet the requirements of being a functional
interface.

Functional interfaces and single responsibility principle


Single responsibility principle states that every class should only have one responsibility and only
one reason to change. Functional interfaces only provide one abstract method to implement, which
makes functional interfaces the nature choice to apply single responsibility principle. With the
power of lambda expressions, implementing function interfaces is also very elegant. Using functional
interfaces is a good combination of design principle and coding practice.
When writing your own application, try to always use functional interfaces. This can also help to
create better design.
http://en.wikipedia.org/wiki/Single_responsibility_principle

2.3 Target typing


For the first assumption about lambda expressions, lambda expressions don’t have type information.
The actual type of a lambda expression is deduced by compiler from its surrounding context at
compile time. For example, lambda expression () -> System.out.println("Hello World!") can
appear in any context where requires an instance of functional interface with the only abstract
method takes no arguments and returns void. This could be java.lang.Runnable interface or any
other functional interfaces created by third-party libraries or application code. So the same lambda
expression can have different types in different contexts. Expressions like lambda expressions which
have deduced type influenced by target type are called poly expressions.

Standalone expressions and Poly expressions


Standalone expressions are expressions which have type that can be determined from the contents
of expressions themselves. For example, a + b and str.substring(1) are standalone expressions.
Poly expressions are expressions which have type that can be influenced by target type. Lambda
expressions and method references are always poly expressions. Some other expressions may be
poly expressions in certain cases.
Lambda expressions 5

2.4 Lambda expressions


Lambda expressions have a very simple syntax to write. The basic syntax looks like (a1, a2) -> {}.
It’s similar with a method, which has a list of formal parameters and a body. Lambda expression’s
syntax is also very flexible.
Listing 2.3 Example of lambda expressions
1 list -> list.size()
2
3 (x, y) -> x * y
4
5 (double x, double y) -> x + y
6
7 () -> "Hello World"
8
9 (String operator, double v1, double v2) -> {
10 switch (operator) {
11 case "+":
12 return v1 + v2;
13 case "-":
14 return v1 - v2;
15 default:
16 return 0;
17 }
18 }

In Listing 2.3, lambda expression in line 1 only has one formal parameter, so parentheses are omitted.
Expression’s body is a single expression, so braces are also omitted. Formal parameter list has
no type declaration, so the type of list is implicitly inferred by compiler. Lambda expression
in line 3 has two formal parameters, so parentheses around parameters list are required. Lambda
expression in line 5 has explicit type for formal parameters x and y. Lambda expression in line 7 has
no formal parameters. In this case, parentheses are required. Lambda expression starts from line 9
is a complicated example with three formal parameters and multiple lines of code in body. In this
case, braces are required to wrap the body.
Lambda expression’s body can return a value or nothing. If a value is returned, the type of return
value must be compatible with target type. If an exception is thrown by the body, the exception must
be allowed by target type’s throws declaration.

2.5 Lexical scoping


In the body of lambda expressions, it’s a common requirement to access variables in the enclosing
context. Lambda expression uses a very simple approach to handle resolution of names in the body.
Lambda expressions 6

Lambda expressions don’t introduce a new level of scoping. They are lexically scoped, which means
names in lambda expression’s body are interpreted as they are in the expression’s enclosing context.
So lambda expression body is executed as it’s in the same context of code enclosing it, except that
the body can also access expression’s formal parameters. this used in the expression body has the
same meaning as in the enclosing code.
Listing 2.4 Lexical scoping of lambda expression
public void run() {
String name = "Alex";
new Thread(() -> System.out.println("Hello, " + name)).start();
}

In Listing 2.4, lambda expression uses variable name from enclosing context. Listing 2.5 shows the
usage of this in lambda expression body. this is used to access sayHello method in the enclosing
context.
Listing 2.5 this in lambda expression
public class LambdaThis {
private String name = "Alex";

public void sayHello() {


System.out.println("Hello, " + name);
}

public void run() {


new Thread(() -> this.sayHello()).start();
}

public static void main(String[] args) {


new LambdaThis().run();
}
}

From code examples in Listing 2.4 and Listing 2.5, we can see how lambda expressions make
developers’ life much easier by simplifying the scope and name resolution.

2.6 Effectively final local variables


To capture variables from enclosing context in lambda expression body, the variables must be
final or effectively final. final variables are declared with final modifiers. effectively final
variables are never assigned after their initialization. Code in Listing 2.6 has compilation error,
because variable name is assigned again after its initialization, so name cannot be referenced in lambda
expression body.
Lambda expressions 7

Listing 2.6 Compilation error of using non-final variables in lambda expression body

public void run() {


String name = "Alex";
new Thread(() -> System.out.println("Hello, " + name)).start();
name = "Bob";
}

2.7 Method references


Method references are references to existing methods by name. For example, Object::toString
references toString method of java.lang.Object class. :: is used to separated class or instance
name and method name. Method references are similar with lambda expressions, except that method
references don’t have a body, just refer existing methods by name. Method references can be used
to remove more boilerplate code. If the body of a lambda expression only contains one line of code
to invoke a method, it can be replaced with a method reference.
StringProducer in Listing 2.7 is a functional interface with method produce.

Listing 2.7 Functional interface to produce a string

@FunctionalInterface
public interface StringProducer {
String produce();
}

Listing 2.8 is an example of using StringProducer. displayString method takes an instance of


StringProducer and outputs the string to console. In run method, method reference this::toString
is used to create an instance of StringProducer. Invoking run method will output text like
StringProducerMain@65ab7765 to the console. It’s the same result as invoking toString() method of
current StringProducerMain object. Using this::toString is the same as using lambda expression
() -> this.toString(), but in a more concise way.
Lambda expressions 8

Listing 2.8 Usage of StringProducer with method reference


public class StringProducerMain {
public static void main(String[] args) {
new StringProducerMain().run();
}

public void run() {


displayString(this::toString);
}

public void displayString(StringProducer producer) {


System.out.println(producer.produce());
}
}

2.7.1 Types of method references


There are different types of method references depends on the type of methods they refer to.

Static method
Refer to a static method using ClassName::methodName, e.g. String::format, Integer::max.
Instance method of a particular object
Refer to an instance method of a particular object using instanceRef::methodName, e.g.
obj::toString, str::toUpperCase.
Method from superclass
Refer to an instance method of a particular object’s superclass using super::methodName,
e.g. super::toString. In Listing 2.8, if replacing this::toString with super::toString, then
toString of Object class will be invoked instead of toString of StringProducerMain class.
Instance method of an arbitrary object of a particular type
Refer to an instance method of any object of a particular type using ClassName::methodName,
e.g. String::toUpperCase. The syntax is the same as referring a static method. The difference
is that the first parameter of functional interface’s method is the invocation’s receiver.
Class constructor
Refer to a class constructor using ClassName::new, e.g. Date::new.
Array constructor
Refer to an array constructor using TypeName[]::new, e.g. int[]::new, String[]::new.

2.8 Default interface methods


The introduction of default methods is trying to solve a longstanding problem in Java interface
design: how to evolve a published interface. Java’s interface is a very tight contract between an
Lambda expressions 9

API and its client. Before Java 8, once an interface is published and implemented by other clients,
it’s very hard to add a new method to the interface without breaking existing implementations. All
implementations must be updated to implement the new method. This means interface design has
to be done correctly at the first time and cannot happen iteratively without breaking existing code.
Before Java 8, an interface can only have abstract methods. Abstract methods must be implemented
by implementation classes. Default methods of interfaces in Java 8 can have both declarations and
implementations. Implementation of a default method will be inherited by classes which don’t
override it. If we want to add a new method to an existing interface, this new method can have
default implementation. Then existing code won’t break because this new method will use the
default implementation. New code can override this default implementation to provide a better
solution. In the default method’s implementation, only existing interface methods can be used.
Listing 2.9 is a simple example about how to use default interface methods. The scenario is to insert
records into a database.
Listing 2.9 First version of interface RecordInserter to insert records

public interface RecordInserter {


void insert(Record record);
}

In the first version, interface RecordInserter only has one abstract method insert. SimpleRecordInserter
class in Listing 2.10 is the first implementation.
Listing 2.10 First implementation of RecordInserter interface

public class SimpleRecordInserter implements RecordInserter {


@Override
public void insert(Record record) {
System.out.println("Inserting " + record);
}
}

Then we find out that the performance of inserting a lot of records is not very good, so we want
to add batch inserting to improve performance. So a new method insertBatch is added to the
RecordInserter interface with default implementation. insertBatch takes a list of Record as input,
so the default implementation just iterates the list and uses existing insert method to insert Record
one by one. This default implementation can make sure SimpleRecordInserter class still works.
Lambda expressions 10

Listing 2.11 RecordInserter interface with batch insert

public interface RecordInserter {


void insert(Record record);

default void insertBatch(List<Record> recordList) {


recordList.forEach(this::insert);
}
}

To improve the performance, a new implementation FastRecordInserter class overrides the default
implementation of insertBatch method and create a better solution.
Listing 2.12 New implementation of insertBatch method

public class FastRecordInserter extends SimpleRecordInserter {


@Override
public void insertBatch(List<Record> recordList) {
System.out.println("Inserting " + recordList);
//Use SQL batch operations to insert
}
}

2.8.1 Static interface methods


In Java 8, interfaces can also have static methods. Helper methods related to an interface can be
added as static methods, see Listing 2.13.
Listing 2.13 Interface with a static method

public interface WithStaticMethod {


static void simpleMethod() {
System.out.println("Hello World!");
}
}
3. Functional interfaces
With the introduction of lambda expressions and method references, developers want to have
more functional interfaces to use. Before Java 8, there is only a small number of functional
interfaces. To facilitate developers’ daily coding with lambda expressions, Java 8 provides a rich
set of functional interfaces to satisfy common requirements. These new functional interfaces are in
java.util.function package. Other Java library code has been updated to leverage these functional
interfaces. We are going to take a deep look at these new functional interfaces.

3.1 Functions

3.1.1 Function
Function<T, R> represents a function which takes one argument and produces one result. T is the
type of the input to this function and R is the type of the output of this function. For example,
Function<String, Integer> represents a function takes a String object as the input and produces
an Integer object as the output. The only abstract method in Function is R apply(T t), which
applies this function with given input argument and returns the result.
convert method in Listing 3.1 takes a Function<String, Integer> object and a String object
as the input arguments and returns the result of applying the function to input String object.
When using convert method, we use method references String::length and String::hashCode
as implementation of this functional interface. The first argument when invoking these two method
references is the invocation’s receiver, i.e. input object, so these two method references match the
target type of Function<String, Integer>.
Listing 3.1 Example of using Function interface
public class FunctionUsage {
public static void main(String[] args) {
String input = "Hello World!";
FunctionUsage usage = new FunctionUsage();
System.out.println(usage.convert(String::length, input));
System.out.println(usage.convert(String::hashCode, input));
}

public Integer convert(Function<String, Integer> converter, String string) {


return converter.apply(string);
}
}
Functional interfaces 12

Besides the abstract apply() method, Function interface has two default methods and one static
method. Default methods <V> Function<T,V> andThen(Function<? super R,? extends V> after)
and <V> Function<V,R> compose(Function<? super V,? extends T> before) are designed to chain
Function interfaces together to create a new Function interface. addThen creates a composed method
which applies current function to the input first, then applies given after function to the result of
previous invocation. compose is the opposite of andThen, which applies given before function to the
input first, then applies current function to the result of previous invocation.
Listing 3.2 Example of using addThen and before

public class FunctionUsage {


public static void main(String[] args) {
String input = "Hello World!";
FunctionUsage usage = new FunctionUsage();
System.out.println(usage.convert(usage.find("world"), input));
System.out.println(usage.convert(usage.findUppercase("WORLD"), input));
}

public Integer convert(Function<String, Integer> converter, String string) {


return converter.apply(string);
}

public Function<String, Integer> find(String pattern) {


Function<String, String> function = String::toLowerCase;
return function.andThen((str) -> str.indexOf(pattern));
}

public Function<String, Integer> findUppercase(String pattern) {


Function<String, Integer> function = (str) -> str.indexOf(pattern);
return function.compose(String::toUpperCase);
}
}

// -> Output "6" and "6" to console

In Listing 3.2, find method uses method reference String::toLowerCase to create an instance of
Function<String, String>, then uses andThen to chain it with another lambda expression to search
in the lowercased string. findUppercase method uses a lambda expression to create an instance of
Function<String, Integer> to search in the string, then uses compose to apply method reference
String::toUpperCase before the first function.

The static method <T> Function<T,T> identity() creates a function which always returns its input
argument when invoked. identity is useful to turn an arbitrary object into a function. The type of
created function is Function<T, T> where T is the type of input argument.
Functional interfaces 13

Listing 3.3 Example of using identity

String input = "Hello World!";


Function<String, String> function = Function.identity();
System.out.println(function.apply(input));

//-> Output "Hello World!" to the console

3.1.2 BiFunction
BiFunction<T,U,R> represents a function with takes two arguments and produces one result. T is the
type of the first input argument, U is the type of the second argument and R is the type of output of
the function. The only abstract method in BiFunction is R apply(T t, U u), with the same name as
in Function. In Listing 3.4, substring with type BiFunction<String, Integer, String> is created
using a method reference to String class’s substring(int beginIndex) method.
Listing 3.4 Example of BiFunction

BiFunction<String, Integer, String> substring = String::substring;


System.out.println(substring.apply("Hello World!", 6));

//-> Output "World!" to console

BiFunction also has default method <V> BiFunction<T,U,V> andThen(Function<? super R,?
extends V> after) to chain current function with another function. After applying current
BiFunction instance to input arguments, the result is provided to the next Function instance to
produce the actual result. In Listing 3.5, toUpperCase is created by chaining substring in Listing 3.4
with method reference to String class’s toUpperCase() method.
Listing 3.5 Example of andThen

BiFunction<String, Integer, String> toUpperCase = substring.andThen(String::toUpperC\


ase);
System.out.println(toUpperCase.apply("Hello World!", 6));

//-> Output "WORLD!" to console

3.1.3 Function and BiFunction with predefined types


Although Function and BiFunction interfaces are very useful when representing common types of
functions, the generic syntax to declare functions is cumbersome with generic type declarations.
Java 8 provides other functional interfaces to simplify the syntax for common scenarios.
Functional interfaces 14

DoubleFunction<R>
DoubleFunction<R> is the simplified syntax to create a function which takes a double type input
argument. The abstract method in this interface is R apply(double value). DoubleFunction<R>
is the simplified version of Function<Double, R>.
IntFunction<R>
IntFunction<R> represents a function which takes a int value as input argument. The abstract
method in this interface is R apply(int value). IntFunction<R> is the simplified version of
Function<Integer, R>.
LongFunction<R>
LongFunction<R> represents a function which takes a long value as input argument. The
abstract method in this interface is R apply(long value). LongFunction<R> is the simplified
version of Function<Long, R>.
ToDoubleFunction<T>
ToDoubleFunction<T> represents a function which produces a double value as result. The
abstract method in this interface is double applyAsDouble(T value). ToDoubleFunction<T>
is the simplified version of Function<T, Double>.
ToIntFunction<T>
ToIntFunction represents a function which produces a int value as result. The abstract method
in this interface is int applyAsInt(T value). ToIntFunction<T> is the simplified version of
Function<T, Integer>.
ToIntFunction<T>
ToLongFunction represents a function which produces a long value as result. The abstract
method in this interface is long applyAsLong(T value). ToLongFunction<T> is the simplified
version of Function<T, Long>.
DoubleToIntFunction
DoubleToIntFunction represents a function which takes a double value as input argument and
produces a int value as result. The abstract method in this interface is int applyAsInt(double
value). DoubleToIntFunction is the simplified version of Function<Double, Integer>.
DoubleToLongFunction
DoubleToLongFunction represents a function which takes a double value as input argu-
ment and produces a long value as result. The abstract method in this interface is long
applyAsLong(double value). DoubleToLongFunction is the simplified version of Function<Double,
Long>.
IntToDoubleFunction
IntToDoubleFunction represents a function which takes a int value as input argument
and produces a double value as result. The abstract method in this interface is double
applyAsDouble(int value). IntToDoubleFunction is the simplified version of Function<Integer,
Double>.
IntToLongFunction
IntToLongFunction represents a function which takes a int value as input argument and
produces a long value as result. The abstract method in this interface is long applyAsLong(int
value). IntToLongFunction is the simplified version of Function<Integer, Long>.
Functional interfaces 15

LongToDoubleFunction
LongToDoubleFunction represents a function which takes a long value as input argument
and produces a double value as result. The abstract method in this interface is double
applyAsDouble(long value). LongToDoubleFunction is the simplified version of Function<Long,
Double>.
LongToIntFunction
LongToIntFunction represents a function which takes a long value as input argument and
produces a int value as result. The abstract method in this interface is int applyAsInt(long
value). LongToIntFunction is the simplified version of Function<Long, Integer>.
ToDoubleBiFunction<T,U>
ToDoubleBiFunction<T,U> represents a function which takes two arguments and produces a
double value as result. The abstract method in this interface is double applyAsDouble(T t, U
u). ToDoubleBiFunction<T,U> is the simplified version of BiFunction<T, U, Double>.
ToIntBiFunction<T,U>
ToIntBiFunction<T,U> represents a function which takes two arguments and produces a
int value as result. The abstract method in this interface is int applyAsInt(T t, U u).
ToIntBiFunction<T,U> is the simplified version of BiFunction<T, U, Integer>.
ToLongBiFunction<T,U>
ToLongBiFunction<T,U> represents a function which takes two arguments and produces a
long value as result. The abstract method in this interface is long applyAsLong(T t, U u).
ToLongBiFunction<T,U> is the simplified version of BiFunction<T, U, Long>.

See Listing 3.6 for some examples of using predefined function types.
Listing 3.6 Examples of using simplified function syntax

IntFunction<Integer> function1 = Integer::bitCount;


ToIntFunction<Integer> function2 = Integer::bitCount;
ToIntBiFunction<String, String> function3 = String::indexOf;

3.2 Consumers

3.2.1 Consumer
Consumer<T> represents a function which takes an input argument of type T and returns no result. The
input argument is consumed and certain side-effects are expected by design since there is no result.
For example, Consumer<String> represents a function which consumes a String object. The abstract
method in the interface is void accept(T t). Consumer interface has one default method Consumer<T>
andThen(Consumer<? super T> after) to chain current function with another Consumer function to
process in sequence, which is similar with andThen method in Function interface.
Functional interfaces 16

3.2.2 BiConsumer
BiConsumer<T,U> represents a function which takes two input arguments with type T and U and
returns no result. For example, Consumer<String, Integer> represents a function which consumes
a String and an Integer object. The abstract method in the interface is void accept(T t, U u).
BiConsumer interface also has the default method BiConsumer<T,U> andThen(BiConsumer<? super
T,? super U> after) to chain BiConsumer functions together.

3.2.3 Consumer with predefined types


DoubleConsumer
DoubleConsumer represents a function which takes a double value as input argument and
produces no result. The abstract method in this interface is void accept(double value). It also
has a default method DoubleConsumer andThen(DoubleConsumer after) to chain functions.
DoubleConsumer is the simplified version of Consumer<Double>.
IntConsumer
IntConsumer represents a function which takes a int value as input argument and produces no
result. The abstract method in this interface is void accept(int value). It also has a default
method IntConsumer andThen(IntConsumer after) to chain functions. IntConsumer is the
simplified version of Consumer<Integer>.
LongConsumer
LongConsumer represents a function which takes a long value as input argument and produces
no result. The abstract method in this interface is void accept(long value). It also has a default
method LongConsumer andThen(LongConsumer after) to chain functions. LongConsumer is the
simplified version of Consumer<Long>.
ObjDoubleConsumer
ObjDoubleConsumer<T> represents a function which takes a object of type T and a double value
as input arguments and returns no result. The abstract method in this interface is void accept(T
t, double value). ObjDoubleConsumer<T> is the simplified version of BiConsumer<T, Double>.
ObjIntConsumer
ObjIntConsumer<T> represents a function which takes a object of type T and a int value as
input arguments and returns no result. The abstract method in this interface is void accept(T
t, int value). ObjIntConsumer<T> is the simplified version of BiConsumer<T, Integer>.
ObjLongConsumer
ObjLongConsumer<T> represents a function which takes a object of type T and a long value as
input arguments and returns no result. The abstract method in this interface is void accept(T
t, long value). ObjLongConsumer<T> is the simplified version of BiConsumer<T, Long>.

3.2.4 Example of using Consumer


Listing 3.7 shows a simple Record class.
Functional interfaces 17

Listing 3.7 A simple Record class

public class Record {


private String name = "";
private int value;

public Record(String name, int value) {


this.name = name;
this.value = value;
}

public int getValue() {


return value;
}

public void setValue(int value) {


this.value = value;
}

public String getName() {


return name;
}

public void setName(String name) {


this.name = name;
}

@Override
public String toString() {
return "Record{" +
"name='" + name + '\'' +
", value=" + value +
'}';
}
}

Listing 3.8 shows RecordProcessor class to process Records. addProcessor method adds a Consumer<Record>
instance to the processor chain. New processor is added to the end of the chain using addThen.
process method uses the processor chain to process the record.
Functional interfaces 18

Listing 3.8 RecordProcessor class to process Records

public class RecordProcessor {

private Consumer<Record> processor;

public RecordProcessor addProcessor(Consumer<Record> consumer) {


if (processor == null) {
processor = consumer;
}
else {
processor = processor.andThen(consumer);
}
return this;
}

public void process(Record record) {


if (processor == null) {
throw new IllegalArgumentException("Add at least one processor.");
}
else {
processor.accept(record);
}
}
}

Listing 3.9 shows an example of using RecordProcessor to process records. The initial record object’s
name is Test and value is 1. Then three processors are added to the processing chain. After applying
processors to the record object, the record object’s name is Demo and value is 30.
Listing 3.9 Use RecordProcessor to process records

RecordProcessor recordProcessor = new RecordProcessor();


Record record = new Record("Test", 1);
recordProcessor
.addProcessor(r -> r.setName("Demo"))
.addProcessor(r -> r.setValue(10))
.addProcessor(r -> r.setValue(30));
recordProcessor.process(record);
System.out.println(record);

//-> Record{name='Demo', value=30}


Functional interfaces 19

3.3 Suppliers

3.3.1 Supplier
Supplier<T> is the opposite of Consumer, which takes no input and returns a object of type T as the
result. The abstract method in Supplier is T get().

3.3.2 Supplier with predefined types


BooleanSupplier
BooleanSupplier represents a function which takes no input and returns a boolean value. The
abstract method in this interface is boolean getAsBoolean(). BooleanSupplier is the simplified
version of Supplier<Boolean>.
DoubleSupplier
DoubleSupplier represents a function which takes no input and returns a double value. The
abstract method in this interface is double getAsDouble(). DoubleSupplier is the simplified
version of Supplier<Double>.
IntSupplier
IntSupplier represents a function which takes no input and returns a int value. The
abstract method in this interface is int getAsInt(). IntSupplier is the simplified version
of Supplier<Integer>.
LongSupplier
LongSupplier represents a function which takes no input and returns a long value. The
abstract method in this interface is long getAsLong(). LongSupplier is the simplified version
of Supplier<Long>.

Listing 3.10 shows a Supplier for random numbers.


Listing 3.10 Supplier for random numbers

public class RandomNumberSupplier implements LongSupplier {


private Random random = new Random();

@Override
public long getAsLong() {
return random.nextLong();
}
}
Functional interfaces 20

3.4 Predicates

3.4.1 Predicate
Predicate<T> represents a function which takes one input argument of type T and returns a boolean
value. Predicates are very useful in many scenarios, e.g. filtering elements in a collection. The
abstract method boolean test(T t) in Predicate interface is used to test if a predicate is satisfied.
Predicate interface has three default methods which can be used to compose Predicate instances
together to create a composite predicate. Predicate<T> and(Predicate<? super T> other) creates
a new predicate which represents a short-circuiting logical AND composition of current predicate and
another predicate. Predicate<T> or(Predicate<? super T> other) is similar with and, but it creates
a new predicate which represents a short-circuiting logical OR composition. Complicated predicates
can be created by combining and and or. and and or methods are short-circuiting, so they may not
be evaluated if the result of composed predicate is known after evaluating the first predicate. For
example, if current predicate of and method is evaluated with result false, then the other predicate
is not evaluated as the result is always false. This is also the case when current predicate of or
method is evaluated with result true. Predicate<T> negate() creates a predicate which is the logic
negation of current predicate.
Static method <T> Predicate<T> isEqual(Object targetRef) creates a predicate to test if two
objects are equal.

3.4.2 BiPredicate
BiPredicate<T,U> is similar with Predicate except that BiPredicate takes two input arguments
of type T and U and returns a boolean value. The abstract and default methods in BiPredicate
have the same name and meaning as in Predicate. The abstract method is boolean test(T t,
U u). The default methods are BiPredicate<T,U> and(BiPredicate<? super T,? super U>
other), BiPredicate<T,U> or(BiPredicate<? super T,? super U> other) and BiPredicate<T,U>
negate().

3.4.3 Predicate and BiPredicate with predefined types


DoublePredicate
DoublePredicate represents a function which takes a double value as input argument and
returns a boolean value. The abstract method in this interface is boolean test(double value).
It also has default methods DoublePredicate and(DoublePredicate other), DoublePredicate
or(DoublePredicate other) and DoublePredicate negate(). DoublePredicate is the simpli-
fied version of Predicate<Double>.
IntPredicate
IntPredicate represents a function which takes a int value as input argument and returns a
Functional interfaces 21

boolean value. The abstract method in this interface is boolean test(int value). It also has
default methods IntPredicate and(IntPredicate other), IntPredicate or(IntPredicate
other) and IntPredicate negate(). IntPredicate is the simplified version of Predicate<Integer>.
LongPredicate
LongPredicate represents a function which takes a long value as input argument and returns
a boolean value. The abstract method in this interface is boolean test(long value).
It also has default methods LongPredicate and(LongPredicate other), LongPredicate
or(LongPredicate other) and LongPredicate negate(). LongPredicate is the simplified
version of Predicate<Long>.

Listing 3.11 shows a Predicate which filters strings by length.


Listing 3.11 Predicate filtering strings by length
public class StringFilter implements Predicate<String> {
@Override
public boolean test(String str) {
return str != null && str.length() > 5;
}
}

3.5 Operators
Operators are special kinds of functions which extend from Function or BiFunction with predefined
types.

3.5.1 UnaryOperator
UnaryOperator<T> extends from Function<T, T>, so both the input argument and return result have
the same type T. For example, UnaryOperator<String> represents a function which takes a String
objects as the input argument and returns a String object result. UnaryOperator has a static method
<T> UnaryOperator<T> identity() with the same meaning as identity() method in Function.

3.5.2 BinaryOperator
BinaryOperator<T> extends from BiFunction<T,T,T>, so all the input arguments and return result
have the same type T. For example, BinaryOperator<Integer> represents a function which takes two
Integer objects as the input arguments and returns a Integer object as the result. BinaryOperator
has two static methods BinaryOperator<T> maxBy(Comparator<? super T> comparator) and <T>
BinaryOperator<T> minBy(Comparator<? super T> comparator). maxBy() creates a BinaryOperator
which returns the input argument with greater value by comparing using given Comparator. minBy()
is similar with maxBy(), except that it returns the input argument with lesser value.
Functional interfaces 22

3.5.3 Operators with predefined types


DoubleUnaryOperator
DoubleUnaryOperator represents a function which takes a double value as input argument
and return a double value as result. The abstract method is double applyAsDouble(double
operand). It also has default methods DoubleUnaryOperator andThen(DoubleUnaryOperator
after) and DoubleUnaryOperator compose(DoubleUnaryOperator before) and static method
DoubleUnaryOperator identity(). andThen(), compose() and identity() have the same
meaning as in Function. DoubleUnaryOperator is the simplified version of UnaryOperator<Double>.
IntUnaryOperator
IntUnaryOperator represents a function which takes a int value as input argument and return
a int value as result. The abstract method is int applyAsInt(int operand). It also has de-
fault methods IntUnaryOperator andThen(IntUnaryOperator after) and IntUnaryOperator
compose(IntUnaryOperator before) and static method IntUnaryOperator identity(). andThen(),
compose() and identity() have the same meaning as in Function. IntUnaryOperator is the
simplified version of UnaryOperator<Integer>.
LongUnaryOperator
LongUnaryOperator represents a function which takes a long value as input argument and
return a long value as result. The abstract method is long applyAsLong(long operand).
It also has default methods LongUnaryOperator andThen(LongUnaryOperator after) and
LongUnaryOperator compose(LongUnaryOperator before) and static method LongUnaryOperator
identity(). andThen(), compose() and identity() have the same meaning as in Function.
LongUnaryOperator is the simplified version of UnaryOperator<Long>.
DoubleBinaryOperator
DoubleBinaryOperator represents a function with takes two double values as input argu-
ments and return a double value as result. The abstract method in this interface is double
applyAsDouble(double left, double right). DoubleBinaryOperator is the simplified version
of BinaryOperator<Double>.
IntBinaryOperator
IntBinaryOperator represents a function with takes two int values as input arguments and
return a int value as result. The abstract method in this interface is int applyAsInt(int left,
int right). IntBinaryOperator is the simplified version of BinaryOperator<Integer>.
LongBinaryOperator
LongBinaryOperator represents a function with takes two long values as input arguments and
return a long value as result. The abstract method in this interface is long applyAsLong(long
left, long right). LongBinaryOperator is the simplified version of BinaryOperator<Long>.
4. Streams
Stream is a new concept introduced in Java 8. A stream represents a sequence of elements. Elements
in a stream can be objects, integers, long values or double values. Stream is a high-level abstraction
which doesn’t care about the source of elements or how those elements are managed. It only
cares about the operations performed on elements of the stream. When combined with functional
interfaces and lambda expressions, it’s easy to write simple yet powerful code to manipulate data
with streams.
To perform computations on streams, different stream operations can be chained together to create
stream pipelines. A stream pipeline consists of a source, zero or more intermediate operations and a
terminal operation.

Stream source
Stream source is the source of elements in the stream. There are different kinds of stream
sources in Java 8, including arrays, collections, generator functions and I/O channels.
Intermediate operation
An intermediate operation perform computation on a stream and returns a new stream.
Intermediate operations are lazily executed.
Terminal operation
A terminal operation traverses the stream to produce a result or a side-effect. After a terminal
operation is performed on a stream, the stream is consumed and cannot be consumed again.

A stream pipeline starts its execution when the terminal operation is executed.

4.1 User-specified behavior


Stream operations usually require user-specified behavior to complete certain tasks. For example,
when using filter operations, the filtering conditions should be provided. These behaviors are
specified as parameters of stream operations using lambda expressions or method references. This
is a powerful combination which provides a much better development experience for processing
collections than Java 7.
Generally speaking, user-specified behavior should not interfere with the underlying data source
of a stream during the execution of current stream pipeline. Failing to meet this non-interference
requirement may cause exceptions or wrong results. So the stream source should not be modified
during pipeline’s execution. However, if the stream is created from a concurrent collection, then it
can be modified concurrently.
Streams 24

4.2 Basic stream concepts

4.2.1 Sequential or parallel


Stream operations can be executed in serial or in parallel. Parallel execution of stream operations
may yield better performance than sequential execution. However, poorly implemented stream
behavior may have worse performance due to data racing conditions and unnecessary thread
synchronization.
Whether a stream will be executed in serial or in parallel can be determined by isParallel()
method. Depends on how a stream is created, it has a default execution mode. This default mode
can be changed by sequential() or parallel() method, which changes stream’s execution mode
to sequential or parallel, respectively.

4.2.2 Encounter order


A stream’s encounter order is the order of its elements when elements are processed. A stream may
or may not have a defined encounter order according to its characteristics. For example, a stream
created from an ArrayList has a defined encounter order, but a stream created from a HashSet or
entries of a HashMap doesn’t have a defined encounter order. Most stream operations respect streams’
encounter order and they process elements in their encounter order. If a stream is not ordered,
repeated executions of the same stream pipeline may produce different results.
For a unordered stream, we can use operations like sorted to sort the stream to make it ordered. For
a ordered stream, we can use unordered() method to make it unordered.

4.3 Stream operation characteristics

4.3.1 Stateful or stateless


Stream operations can be stateful or stateless. When a stateful operation is processing an element, it
may require information retained from processing of previous elements. On the other side, a stateless
operation can process each element independently.
distinct and sorted are examples of stateful operations. distinct operation removes duplicate
elements from a stream, so it needs to keep record of previously seen elements when determining
if currently processing element should be removed. sorted operation sorts the stream, so it needs
to know all seen elements to determine the correct order of currently processing element. filter
and map are examples of stateless operations. filter operation can determine if currently processing
element should be filtered solely based on the element itself. map operation can transform currently
processing element without knowing other elements in the stream.
Streams 25

Stateful operations may need to process the entire input stream before they can produce results. So
stateful operations may affect performance in parallel computation. sorted operation may require
multiple passes on the stream to finish sorting. distinct operation needs additional memory to store
seen elements. If a stream pipeline only contains stateless operations, then it can be processed in one
pass with minimal extra memory.

4.4 Stream
Stream<T> is the interface to represent a stream. T is the type of elements in the stream.

Except from generic streams of objects, there are three other interfaces to represent streams of
primitive values.

• IntStream represents streams of int values.


• LongStream represents streams of long values.
• DoubleStream represents streams of double values.

IntStream, LongStream and DoubleStream should be used for int, long and double values in-
stead of using Stream<Integer>, Stream<Long> and Stream<Double>. IntStream, LongStream and
DoubleStream remove unnecessary boxing and unboxing, so they have better performance than
streams of wrapper types.

4.5 Spliterator
Spliterator is the most important concept to understand how streams work in Java 8. All streams
are created using some sort of Spliterators. The name Spliterator itself comes from split and
iterator. We can think Spliterator as a parallel version of Iterator which can partition elements
by splitting the data source. Spliterators can work like Iterators to traverse elements sequentially
by using tryAdvance() method. Spliterators can also be used in parallel operations by using
trySplit() method to create a new instance of Spliterator which works on a different set of
elements.
Spliterator also supports bulk traversal using default method forEachRemaining(Consumer<?
super T> action), which performs specified action for each remaining element in the source
sequentially. forEachRemaining is useful to process a relatively small number of elements when there
is no need for further splitting. When there are a large number of elements, we can use trySplit
to split current Spliterator into Spliterators with less elements. When the number of elements is
small enough, we can use forEachRemaining to process all remaining elements sequentially instead
of using trySplit to split again. We can get an estimate of the number of elements that would be
encountered by a forEachRemaining traversal using estimateSize() method, which can give us a
rough idea about the size of elements to be processed.
Streams 26

4.5.1 Spliterator characteristics


A Spliterator can have a set of characteristics regarding its data source and elements. We can use
characteristics() method to query the unified characteristics supported by the Spliterator. Table
4.1 shows all characteristics.

Table 4.1 Spliterator characteristics

Name Description
IMMUTABLE Signifies that the element source cannot be modified, so there cannot be any change during
traversal.
CONCURRENT Signifies that the element source may be concurrently modified without using external
synchronization.
DISTINCT Signifies that the element source contains no duplicate elements.
NONNULL Signifies that the element source contains no null elements.
ORDERED Signifies that elements in the source have a defined encounter order. Elements will be
traversed in the encounter order.
SORTED Signifies that elements in the source have a defined sort order. If a Spliterator is SORTED,
then it must be ORDERED.
SIZED Signifies that the element source has an exact number of elements.
SUBSIZED Signifies that all Spliterators created from trySplit() will be both SIZED and SUBSIZED.

Spliterator also has some default methods which may be useful depends on its characteristics. If a
Spliterator is SORTED, we can use getComparator() method to get the Comparator instance used to
sort the elements. If elements are sorted in natural order, then getComparator method returns null.
getExactSizeIfKnown() method returns result of estimateSize() method when the Spliterator is
SIZED, otherwise returns -1.

4.5.2 Spliterators for primitive types


If data source of a Spliterator contains elements of primitive types, i.e. int, long and double, it’s
better to use specialized Spliterator sub-interfaces, Spliterator.OfInt, Spliterator.OfLong and
Spliterator.OfDouble. This can avoid unnecessary performance penalty of boxing and unboxing
when using generic Spliterator interface to process primitive types.

4.5.3 Spliterators to create Spliterators


java.util.Spliterators is a utility class to create instances of Spliterator, Spliterator.OfInt,
Spliterator.OfLong and Spliterator.OfDouble for common use cases. Spliterators only has a
set of static methods for different use cases.

• emptySpliterator - Creates an empty Spliterator.


• spliterator - Creates a Spliterator from an int[], double[], long[], Object[] array or a
Collection.
Streams 27

• spliteratorUnknownSize - Creates a Spliterator from an Iterator.

We can also convert a Spliterator to an Iterator using iterator method.

4.5.4 Late-binding and fail-fast


Spliterators need to bind to the stream source first before they can traverse the elements. Different
Spliterator implementations may have different timing to bind to the stream source. If a
Spliterator is late-binding, then it binds to the stream source at the point of first traversal, first
split, or first query for estimated size. If a Spliterator is not late-binding, then it binds to the stream
source at the point of construction or first invocation of any method.
The timing of binding is important because modifications to the stream source before binding will
be visible during spliterator’s traversal. So if a Spliterator is late-binding, then we are still free to
modify the stream source as long as traversal is not started, i.e. before stream pipeline’s terminal
operation is executed. Late-binding is more flexible and should be used when possible.
In Listing 4.1, list contains two elements Hello and World, then a stream is created from list. Even
though new element Goodbye is added to list after the stream is created, it’s still visible for stream
operation forEach because of late-binding.
Listing 4.1 Example of late-binding

List<String> list = Lists.newArrayList("Hello", "World");


Stream<String> stream = list.stream();
list.add("Goodbye");
stream.forEach(System.out::println);

// -> Output "Hello", "World" and "Goodbye"

Some Spliterators are also fail-fast, which means they throw ConcurrentModificationException
when detecting any interference during traversal after binding to the stream source.
In Listing 4.2. we use two threads to simulate concurent modification to the stream’s source during
traversal. When the code is running, ConcurrentModificationException will be thrown.
Streams 28

Listing 4.2 Example of fail-fast

List<String> list = Lists.newArrayList("Hello", "World");


Stream<String> stream = list.stream();
new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add("Bad");
}).start();

stream.forEach((str) -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(str);
});

4.6 BaseStream
BaseStream<T,S extends BaseStream<T,S>> is the superinterface of Stream<T>, IntStream, LongStream
and DoubleStream. Some aforementioned methods come from BaseStream, including isParallel(),
parallel(), sequential() and unordered(). BaseStream also has spliterator() to get a Spliterator
and iterator() to get an Iterator.
BaseStream extends from AutoCloseable, so we can use close() method to close the stream when re-
quired. Streams can also be used in try-with-resources statement. We can also use onClose(Runnable
closeHandler) method to add close handlers which are invoked when close() method is called on
the stream. Close handlers are invoked in the order they were added.

4.7 StreamSource
StreamSource is the low level API to create streams from Spliterators. StreamSource provides
a set of methods to create four kinds of streams, i.e. Stream<T>, IntStream, LongStream and
DoubleStream, from Spliterators or Suppliers of Spliterators. For example, <T> Stream<T>
stream(Spliterator<T> spliterator, boolean parallel) and <T> Stream<T> stream(Supplier<?
Streams 29

extends Spliterator<T>> supplier, int characteristics, boolean parallel) methods are used
to create generic Streams. Let’s take a closer look at these two methods.
Both methods accept parameter parallel to specify whether the stream is parallel or sequential,
which is the stream’s default execution mode. The difference of these two methods is whether
to create the stream directly from a Spliterator, or indirectly from a Supplier function of
Spliterator. It depends on the characteristics of underlying Spliterator to choose the correct way
to create the stream. If the Spliterator is IMMUTABLE or CONCURRENT or late-binding, then it should
be used directly to create the stream, otherwise Supplier function should be used.
When a Supplier function is used, it’s only invoked after the stream pipeline’s terminal operation
starts, so any changes to the stream source before the start of terminal operation are still visible
in the stream result. This can minimize potential interference with the data source. However, we
should always favor IMMUTABLE or CONCURRENT spliterators, or at least support late-binding.

4.8 Stream static methods


Stream interface also provides some static methods to create streams.

empty()
empty method creates an empty sequential stream.
of(T t)
of method creates a sequential stream with a single element t of type T.
of(T... values)
of method creates a sequential stream with given elements values of type T.
generate(Supplier<T> s)
generate method creates an infinite unordered sequential stream with elements generated by
invoking Supplier function.
iterate(T seed, UnaryOperator<T> f)
iterate method creates an infinite ordered sequential stream with elements generated by
invoking function f with input from last invocation. seed is the initial input for first invocation.
The stream contains elements of seed, f(seed), f(f(seed)) and so on.
concat(Stream<? extends T> a, Stream<? extends T> b)
concat method creates a new stream by concatenating stream a and stream b. Elements in first
stream a come before stream b in the encounter order.

4.9 Stream sources

4.9.1 Arrays
To create a stream from an array, we can use new static methods added in java.util.Arrays. There
are eight methods with the same name stream to create streams from arrays. These eight methods
Streams 30

can be divided into four groups based on the types of streams they create, i.e. Stream<T>, IntStream,
LongStream and DoubleStream.

Take Stream<T> as the example, <T> Stream<T> stream(T[] array) method creates a stream from
the whole input array. <T> Stream<T> stream(T[] array, int startInclusive, int endExclusive)
method creates a stream from the range specified by startInclusive and endExclusived index in
the input array.
Listing 4.3 Use Arrays.stream to create new streams

Arrays.stream(new String[] {"Hello", "World"})


.forEach(System.out::println);

// -> Output "Hello\nWorld" to console

int sum = Arrays.stream(new int[] {1, 2, 3})


.reduce((a, b) -> a + b)
.getAsInt();

// -> Value of "sum" is "6"

Spliterators of streams created using Arrays.stream method have characteristics IMMUTABLE and
ORDERED.

4.9.2 Collections
New default method stream() and parallelStream() are added to Collection interface to create
a sequential or parallel stream from a collection. Streams are created from spliterators returned
by new default method spliterator(). Collections’ spliterators have characteristics SIZED and
SUBSIZED by default. Sub-interfaces’ implementation of spliterator method may report additional
characteristics. For example, spliterators created from Sets have characteristic DISTINCT, while those
created from Lists have characteristic ORDERED.
Listing 4.4 Create streams from Collections

List<String> list = new ArrayList<>();


list.add("Hello");
list.add("World");
list.stream()
.forEach(System.out::println);
Streams 31

4.9.3 Stream builder


Stream builders (Stream.Builder<T>) are used to create a new stream from individual elements. An
instance of Stream.Builder can be created using static method <T> Stream.Builder<T> builder()
of Stream<T> interface. Stream.Builder<T> extends from Consumer<T> as it consumes an input
element and add it to the stream.
After a new instance of Stream.Builder is created, accept or add method can be invoked to add an
element to the stream. The difference between accept and add is that add method returns the current
Stream.Builder instance, so invocation of add method can be chained. accept method comes from
Consumer interface. After all elements have been added, invoke build() method to create the stream.
build() method returns an instance of Stream<T>.

Listing 4.5 Use Stream.Builder to create streams

Stream.builder()
.add("Hello")
.add("World")
.build()
.forEach(System.out::println);

We can also use specialized stream builders for primitive types, including IntStream.Builder,
LongStream.Builder and DoubleStream.Builder.

4.9.4 I/O channels


In Java 8, some classes related to I/O operations have been updated to include methods to create
streams from underlying I/O channels.
java.io.BufferedReader adds a method lines() to return a Stream<String> object with lines read
from this BufferedReader.
java.ni.file.Files adds several static methods related to streams.

4.9.4.1 lines

lines(Path path) and lines(Path path, Charset cs) are both used to read all lines from a file
specified by path as a Stream<String> object. Specified charset cs is used for string decoding, or
default charset UTF-8 is used.
Streams 32

Listing 4.6 Read lines as stream from file


public void readLines() throws IOException {
try (Stream<String> lines = Files.lines(Paths.get("sample.txt"))) {
lines.forEach(System.out::println);
}
}

4.9.4.2 list

list(Path dir) returns all entries in specified directory dir as a Stream<Path> object. This directory
listing is not recursive. If recursive listing is required, use walk method instead. Returned stream is
weakly consistent with underlying file system. It may not reflect updates to the underlying directory
which occur after this method is returned.
Listing 4.7 List files as stream
public void listFiles() throws IOException {
try (Stream<Path> paths = Files.list(Paths.get("."))) {
paths.forEach((path) -> System.out.println(path.getFileName()));
}
}

4.9.4.3 walk

walk(Path start, FileVisitOption... options) and walk(Path start, int maxDepth, FileVisitOption...
options) are both used to traverse the file system tree rooted at given path start as a Stream<Path>
object. If maxDepth parameter is provided, then it limits the maximum number of levels of directories
to visit, otherwise the whole directory tree is visited.
Listing 4.8 Walk file system directory tree
public void walkDirectory() throws IOException {
Path root = Paths.get("/tmp/test");
try (Stream<Path> paths = Files.walk(root)) {
paths.forEach((path) -> {
int level = path.getNameCount() - root.getNameCount();
String displayPath = String.format("|-%s %s",
StringUtils.repeat('-', level), path.getFileName());
System.out.println(displayPath);
});
}
}

The output of code example looks like below:


Streams 33

Listing 4.9 Output of walking file system directory tree in Listing 4.8

|- test
|-- 1.txt
|-- a
|--- a1.txt
|--- a2.txt
|-- b
|--- b1.txt
|-- c
|--- c1.txt

4.9.4.4 find

find(Path start, int maxDepth, BiPredicate<Path,BasicFileAttributes> matcher, FileVisitOption...


options) is used to search for files in the file system tree rooted at given path start. Parameter
matcher of type BiPredicate<Path,BasicFileAttributes> is used to check if a path should be
included in the returned Stream<Path> object. Parameters start, maxDepth and options have the
same meanings as in walk method. Actually it’s possible to use walk method to create a stream first,
then filter on the stream using predicates. But using find method directly is more efficient.
Listing 4.10 Find files

public void findFiles() throws IOException {


try (Stream<Path> paths = Files.find(Paths.get("."),
Integer.MAX_VALUE,
(path, attr) -> path.toString().endsWith(".java")
&& attr.isRegularFile())) {
paths.forEach(System.out::println);
}
}

java.util.zip.ZipFile adds a new method stream to return a Stream<? extends ZipEntry> object
with entries in the zip file.
java.util.jar.JarFile adds a new method stream to return a Stream<JarEntry> object with entries
in the jar file.
java.util.regex.Pattern adds a new method splitAsStream(CharSequence input) to split a
CharSequence object by current regular expression pattern and return a Stream<String> object with
matching substrings.
Streams 34

4.10 Examples for stream processing


To better illustrate how stream operations are used, here we use a common real-world example. The
data model is very common in e-commerce applications. It contains three domain models, Customer,
Order and LineItem.

Customer
Customer class has the basic information about a customer.

Listing 4.11 Customer class


public class Customer {
private long id;
private String firstName;
private String lastName;
private String email;
}

Order
Order class has different properties about the order, including created time, status, amount and
the customer who placed this order.

Listing 4.12 Order class


public class Order {
private long id;
private ZonedDateTime createdAt;
private Status status = Status.PENDING;
private BigDecimal shippingAmount = BigDecimal.ZERO;
private BigDecimal subTotalAmount;
private BigDecimal grandTotalAmount;
private Customer customer;
private List<LineItem> items = new ArrayList<>();

public static enum Status {


PENDING, PROCESSING, COMPLETED, CANCELLED
}
}

LineItem
LineItem class has the related product id, price and quantity.
Streams 35

Listing 4.13 LineItem class

public class LineItem {


private long productId;
private BigDecimal price;
private int quantity;
}

In the code listings below, variable orders is an object of type List<Order>.

4.11 Stream intermediate operations


Stream intermediate operations always return streams after applied to existing streams.

4.11.1 map
<R> Stream<R> map(Function<? super T,? extends R> mapper) transforms a stream with elements
of type T into a stream with elements of type R. The transformation is done by applying function
mapper to each element of the source stream.

If target type of transformation is primitive type double, int and long, methods DoubleStream
mapToDouble(ToDoubleFunction<? super T> mapper), IntStream mapToInt(ToIntFunction<?
super T> mapper) and LongStream mapToLong(ToLongFunction<? super T> mapper) can be used
to return DoubleStream, IntStream and LongStream, respectively.
In Listing 4.14, the first result is a stream of Integers as the number of line items in each order. The
second result is a stream with the same elements as the first result, but the type is IntStream.
Listing 4.14 Stream map operation

orders.stream()
.map(order -> order.getItems().size())

orders.stream()
.mapToInt(order -> order.getItems().size())

4.11.2 flatMap
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
method looks like map since flatMap also transforms a stream with elements of type T into a stream
with elements of type R, but the difference is that flatMap uses a transformation function which
transforms one element in source stream into another stream. All these transformed streams are
Streams 36

flattened to generate the resulting elements of the new stream. That’s why the return type of mapper
function is ? extends Stream<? extends R>.
Similar with map method, there are also primitive type versions of flatMap method: DoubleStream
flatMapToDouble(Function<? super T,? extends DoubleStream> mapper), IntStream flatMapToInt(Function<?
super T,? extends IntStream> mapper) and LongStream flatMapToLong(Function<? super T,?
extends LongStream> mapper).

In Listing 4.15, the result is a stream of all orders’ LineItems.


Listing 4.15 Stream flatMap operation
orders.stream()
.flatMap(order -> order.getItems().stream());

4.11.3 filter
Stream<T> filter(Predicate<? super T> predicate) filters a stream by only keeping elements
match given predicate. Predicate is specified using Predicate interface which applies to an element
and returns a boolean value.
In Listing 4.16, the result is a stream of completed orders, i.e. status set to Order.Status.COMPLETED.
Listing 4.16 Stream filter operation
orders.stream()
.filter(order -> order.getStatus() == Order.Status.COMPLETED);

4.11.4 distinct
Stream<T> distinct() removes duplicate elements from a stream by using Object’s equals method
for comparison.
In Listing 4.17, the result is a stream of distinct orders.
Listing 4.17 Stream distinct operation
orders.stream().distinct();

4.11.5 limit
Stream<T> limit(long maxSize) truncates a stream to contain a maximum number of maxSize
elements.
In Listing 4.18, the result is a stream of at most 5 orders.
Streams 37

Listing 4.18 Stream limit operation

orders.stream().limit(5);

4.11.6 skip
Stream<T> skip(long n) returns a stream with elements from current stream except the first n
elements.
In Listing 4.19, the result is a stream of orders without first 2 orders.
Listing 4.19 Stream skip operation

orders.stream().skip(2);

4.11.7 sorted
Stream<T> sorted() and Stream<T> sorted(Comparator<? super T> comparator) sort a stream. If
no comparator is provided, elements are sorted using natural order, otherwise, elements are sorted
using given comparator.
In Listing 4.20, the result is a stream of orders sorted in chronological order. When invoking sorted
method, an instance of Comparator interface is passed in to compare orders using ZonedDateTime’s
compareTo method. Comparator.comparing is a new static method in Comparator interface to create
Comparator instances using value extraction methods.

Listing 4.20 Stream sorted operation

orders.stream()
.sorted(Comparator.comparing(Order::getCreatedAt));

4.11.8 peek
We can use peek method to inspect elements in a stream without modifying the stream. Stream<T>
peek(Consumer<? super T> action) returns a stream with the same elements as in this stream.
When elements in the result stream are consumed, each element will be consumed by action first.
peek method is useful when debugging.

In Listing 4.21, each order will be outputted twice to console in sequence. The first output comes
from peek method invocation, the second output comes from forEach method invocation.
Streams 38

Listing 4.21 Stream peek operation


orders.stream()
.peek(System.out::println)
.forEach(System.out::println);

4.11.9 Operations chaining


Stream intermediate operations can be chained together to express complicated data processing logic
in a concise way. Method chaining is very common when writing code with streams.

4.12 Stream terminal operations


Stream terminal operations produce the actual results or side-effects.

4.12.1 forEach and forEachOrdered


void forEach(Consumer<? super T> action) takes an instance of Consumer<? super T> as the
action to perform on each element of the stream. There is no deterministic order of when each
element is consumed. If guaranteed order of element processing is required, use forEachOrdered
method instead. void forEachOrdered(Consumer<? super T> action) processes elements in the
encounter order of the stream if the stream has a defined encounter order.
Listing 4.22 Stream forEach operation
orders.stream()
.forEach(System.out::println);

4.12.2 reduce
Optional<T> reduce(BinaryOperator<T> accumulator) performs a reduction on elements of the
stream using given accumulation function and returns an optional value as the result. BinaryOperator
instance accumulator takes current element and previous accumulated value as the input and returns
a new value. New accumulated value is used in next reduction operation. The accumulator function
needs to be associative to generate correct reduction result. The result type of this reduce method
is Optional<T>, because it cannot return a reduction value for an empty stream.
T reduce(T identity, BinaryOperator<T> accumulator) performs a reduction on elements of the
stream using given initial value and accumulation function. For empty streams, the initial value is
returned, otherwise the reduction value is returned.
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U>
combiner) is a more complicated and flexible reduction method. It has following arguments in order:
Streams 39

1. identity - The initial value of reduction. This value is returned for empty streams.
2. accumulator - The function to accumulate new reduction result. This function also does the
map operation from element of type T to the result type U. The input parameters of this function
are current reduction value and current element.
3. combiner - This function is only used for parallel streams. For sequential streams, this function
is not used. When reduction operation is executed on parallel streams, combiner function is
used to combine partial results from different executions into the final result.

The code in Listing 4.23 calculates the subtotal amount of each order using reduce. For reduction of
each order’s line items, the initial value is BigDecimal.ZERO. The accumulator function adds current
accumulated value with multiplication of each item’s unit price and quantity. The combiner function
is BigDecimal’s add method.
Listing 4.23 Stream reduce operation

BigDecimal subtotal = order.getItems().stream()


.reduce(
BigDecimal.ZERO,
(value, item) -> value.add(item.getPrice()
.multiply(BigDecimal.valueOf(item.getQuantity()))),
BigDecimal::add);

4.12.3 max and min


Optional<T> max(Comparator<? super T> comparator) and Optional<T> min(Comparator<? super
T> comparator) are special cases of reduce operation which return the maximum and minimum
value of current stream with elements compared using given Comparator instance.
In Listing 4.24, max and min methods are used to get orders with maximum and minimum
grandTotalAmount.

Listing 4.24 Stream max and min operation

Optional<Order> maxOrder = orders.stream()


.max(Comparator.comparing(Order::getGrandTotalAmount));

Optional<Order> minOrder = orders.stream()


.min(Comparator.comparing(Order::getGrandTotalAmount));
Streams 40

4.12.4 match
Given a stream, we may want to check its elements to see if all, any or none of these ele-
ments match certain conditions. boolean allMatch(Predicate<? super T> predicate), boolean
anyMatch(Predicate<? super T> predicate) and boolean noneMatch(Predicate<? super T>
predicate) take a predicate as argument to check if all, any or none of stream’s elements match
the predicate.
In Listing 4.25, line 1 uses allMatch to check if all orders have at least one line item. Line 4 uses
anyMatch to check if any order’s grandTotalAmount is larger than 100. Line 8 uses noneMatch to
check if there is no order with status not equals to Order.Status.COMPLETED. Line 8 is the same as
orders.stream().allMatch(order -> order.getStatus() == Order.Status.COMPLETED).

Listing 4.25 Stream allMatch, anyMatch and noneMatch operation

boolean allWithItems = orders.stream()


.allMatch(order -> order.getItems().size() > 0);

boolean hasBigOrder = orders.stream()


.anyMatch(order -> order.getGrandTotalAmount()
.compareTo(BigDecimal.valueOf(100)) > 0);

boolean allOrderCompleted = orders.stream()


.noneMatch(order -> order.getStatus() != Order.Status.COMPLETED);

4.12.5 find
Optional<T> findFirst() and Optional<T> findAny() find an element from the stream, or an
empty Optional object if the stream is empty. findFirst returns the first element in the stream if
the stream has defined encounter order, otherwise any element may be returned. findAny returns
any element in the stream. So if a stream has no encounter order, findFirst and findAny actually
do the same thing.
In Listing 4.26, firstOrder and anyOrder represent first and any order in the stream of orders.
Listing 4.26 Stream findFirst and findAny operation

Optional<Order> firstOrder = orders.stream().findFirst();

Optional<Order> anyOrder = orders.stream().findAny();


Streams 41

4.12.6 toArray
Object[] toArray() method converts the stream into an array.

<A> A[] toArray(IntFunction<A[]> generator) method uses a generator function to create the
return array. The generator function takes a int value as the input, which is the required size of the
array, then it should return an array of type A[] with the required size.

4.12.7 collect
Terminal operation collect represents a different type of mutable reduction operation. Instead of
returning a value, mutable reduction operation accumulates elements into a mutable result container,
e.g. a List or a Set.
The first form of collect method is <R,A> R collect(Collector<? super T,A,R> collector).
This method takes an input argument of interface java.util.stream.Collector. Collector is a
simple interface which only contains abstract methods to return required components of a collector.
Following are methods of Collector interface:

• Supplier<A> supplier() - Returns a new container of type A to contain result elements.


• BiConsumer<A,T> accumulator() - Return a function to accumulate new elements into the
result container. The function is a BiConsumer with result container and current element as the
input.
• BinaryOperator<A> combiner() - Return a function to combine two result containers with
partial results into a single container. The function is a BinaryOperator.
• Function<A,R> finisher() - Return a function to transform accumulated result to final result
of type R.
• Set<Collector.Characteristics> characteristics() - Return a Set of Collector.Characteristics
enums which describe collector’s behavior. These characteristics can be used to optimize
performance of reduction operations.
– CONCURRENT - The accumulator function can be called concurrently with the same result
container from multiple threads.
– UNORDERED - This operation doesn’t preserve the encounter order.
– IDENTITY_FINISH - The finisher function is the identity function and can be elided.

As Collector interface only contains abstract methods to return different functions, it provides two
static methods of to create an instance of Collector interface easily by providing these functions
directly.

• <T,A,R> Collector<T,A,R> of(Supplier<A> supplier, BiConsumer<A,T> accumulator,


BinaryOperator<A> combiner, Function<A,R> finisher, Collector.Characteristics...
characteristics) is the standard way to create a Collector instance with given supplier,
accumulator, combiner, finisher and characteristics.
Streams 42

• <T,R> Collector<T,R,R> of(Supplier<R> supplier, BiConsumer<R,T> accumulator, BinaryOperator<R>


combiner, Collector.Characteristics... characteristics) is a simpler format which
doesn’t have the finisher part.

The second form of collect method is <R> R collect(Supplier<R> supplier, BiConsumer<R,?


super T> accumulator, BiConsumer<R,R> combiner). This method’s parameters are similar
with those of Collector’s static of method. The parameters supplier, accumulator and combiner
are actually components to create a Collector instance. So collect(supplier, accumulator,
combiner) is the same as collect(Collector.of(supplier, accumulator, combiner)).

4.12.8 Collectors
Usually we don’t need to implement Collector interface directly, nor use static of method,
java.util.stream.Collectors class provides a lot of helper methods to create different kinds of
Collector instances.

4.12.8.1 Collect into collections

A common usage of collectors is to collect elements into collections. Collectors class has different
methods to create Collector instances for different types of collections.

<T,C extends Collection<T>> Collector<T,?,C> toCollection(Supplier<C> collectionFactory)


Collector to accumulate elements into a generic Collection.
<T> Collector<T,?,List<T>> toList()
Collector to accumulate elements into a List.
<T> Collector<T,?,Set<T>> toSet()
Collector to accumulate elements into a Set.
<T,K,U> Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper,
Function<? super T,? extends U> valueMapper)
Collector to accumulate elements into a Map. For each element, its corresponding map
entry’s key is generated by function keyMapper and the entry’s value is generated by function
valueMapper. If mapped keys from keyMapper have duplicates, an IllegalStateException is
thrown.
<T,K,U> Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper,
Function<? super T,? extends U> valueMapper, BinaryOperator<U> mergeFunction)
This method is also used to create Collectors to accumulate elements into a Map, but it has
another parameter mergeFunction to provide a function to resolve conflicts when duplicate
mapped keys are found. For those values of duplicate keys, the values are provided to the
mergeFunction to merge into a single value.
<T,K,U,M extends Map<K,U>> Collector<T,?,M> toMap(Function<? super T,? extends K>
keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier)
This is the most complicated form of toMap method. It has another parameter mapSupplier to
create the result Map instance.
Streams 43

toConcurrentMap
There are three methods with the same name toConcurrentMap which have the same signature
as toMap methods. The difference is that toConcurrentMap methods return ConcurrentMap
instances instead of Map instances.

In Listing 4.27, line 1 collects the stream of orders into a Set using Collectors.toSet. Line 3 collects
the stream into a Map<Long, Order>. Each entry in the map has the key of the order’ id and has
the value of the order object itself. Function.identity is used here to return the order object itself
without any transformation.
Listing 4.27 Stream collect operation

Set<Order> orderSet = orders.stream().collect(Collectors.toSet());

Map<Long, Order> orderMap = orders.stream()


.collect(Collectors.toMap(Order::getId, Function.identity()));

4.12.8.2 groupingBy

A set of groupingBy methods can be used to create a Collector to group input elements by a
classification function. Elements with the same result after applying the classification function will
be put into the same group.

<T,K> Collector<T,?,Map<K,List<T>>> groupingBy(Function<? super T,? extends K> classifier)


Result of the grouping operation is a Map. Keys of this Map are unique values after applying
classifier function to input elements. Value of each key is a List of elements which generate
this key.

Listing 4.28 uses groupingBy to group orders by status.


Listing 4.28 Collector groupingBy operation

Map<Order.Status, List<Order>> ordersByStatus = orders.stream()


.collect(Collectors.groupingBy(Order::getStatus));

<T,K,A,D> Collector<T,?,Map<K,D>> groupingBy(Function<? super T,? extends K> classifier,


Collector<? super T,A,D> downstream)
After grouping input elements using classifier function, further reduction operation is
performed on the values associated with a given key. The return value of this reduction
operation is the value in the result Map.
Streams 44

Listing 4.29 Collector groupingBy with downstream processing

Map<Order.Status, BigDecimal> ordersAmountByStatus = orders.stream()


.collect(Collectors.groupingBy(Order::getStatus,
Collectors.reducing(BigDecimal.ZERO,
Order::getGrandTotalAmount, BigDecimal::add)));

<T,K,D,A,M extends Map<K,D>> Collector<T,?,M> groupingBy(Function<? super T,? extends


K> classifier, Supplier<M> mapFactory, Collector<? super T,A,D> downstream)
This form of groupingBy method takes an extra mapFactory argument to generate the result
Map instance.
groupingByConcurrent
There are three methods with the same name groupingByConcurrent which have the same
signature as groupingBy methods. The different is that groupingByConcurrent methods return
ConcurrentMap instances instead of Map instances.

4.12.8.3 joining

The set of joining methods is used to concatenate input elements of CharSequence together to create
a String. Input elements are concatenated in encounter order.

Collector<CharSequence,?,String> joining()
This is the simplest form of joining method which returns a Collector instance to concatenate
CharSequence elements to create a result String.
Collector<CharSequence,?,String> joining(CharSequence delimiter)
This form of joining method takes an extra argument delimiter as the delimiter to separate
different elements.
Collector<CharSequence,?,String> joining(CharSequence delimiter, CharSequence prefix,
CharSequence suffix)
This is the most complicated form of joining method which also allows to specify the prefix
and suffix of the concatenated string.
Streams 45

Listing 4.30 Collector joining

Stream.of("Hello", "World").collect(Collectors.joining());
// -> HelloWorld

Stream.of("Hello", "World").collect(Collectors.joining(", "));


// -> Hello, World

Stream.of("Hello", "World").collect(Collectors.joining(", ", "[", "]"));


// -> [Hello, World]

4.12.8.4 mapping

Given a Collector accepting elements of type U, <T,U,A,R> Collector<T,?,R> mapping(Function<?


super T,? extends U> mapper, Collector<? super U,A,R> downstream) method can create
a Collector to accept elements of type T by using mapping function mapper to transform input
elements first.
Listing 4.31 Collector mapping

orders.stream()
.collect(Collectors.mapping(
order -> order.getCustomer().getEmail(), Collectors.joining(", ")
));

4.12.8.5 partitioningBy

A set of partitioningBy methods can be used to create a Collector which partitions input elements
according to a Predicate.

Collector<T,?,Map<Boolean,List<T>>> partitioningBy(Predicate<? super T> predicate)


The result of partitioningBy is of type Map<Boolean,List<T>>. There are at most two entries
in the result Map based on the result value of applying predicate function to input elements. This
method is a simpler form of groupingBy(Function<? super T,? extends K> classifier) as
the keys in the result map can only be Boolean.TRUE and Boolean.FALSE.
<T,D,A> Collector<T,?,Map<Boolean,D>> partitioningBy(Predicate<? super T> predicate,
Collector<? super T,A,D> downstream)
This form of partitioningBy method returns a Collector which further reduces values in each
partition using another Collector. This method is a simpler form of groupingBy(Function<?
super T,? extends K> classifier, Collector<? super T,A,D> downstream).
Streams 46

Listing 4.32 Collector partitioningBy

orders.stream()
.collect(Collectors.partitioningBy(
order -> order.getStatus() == Order.Status.COMPLETED
));

4.12.8.6 counting

<T> Collector<T,?,Long> counting() method creates a Collector which counts the number of
input elements.
Listing 4.33 Collector counting

orders.stream().collect(Collectors.counting());

4.12.8.7 averaging

<T> Collector<T,?,Double> averagingDouble(ToDoubleFunction<? super T> mapper), <T>


Collector<T,?,Double> averagingInt(ToIntFunction<? super T> mapper) and <T> Collector<T,?,Double>
averagingLong(ToLongFunction<? super T> mapper) methods are all used to create Collectors that
produce arithmetic mean values. The difference is the type of mapped values. A mapper function
is used to transform input elements to primitive types double, int or long, then average value is
calculated.
Listing 4.34 Collector averagingDouble

orders.stream()
.collect(Collectors.averagingDouble(
order -> order.getGrandTotalAmount().doubleValue()));

4.12.8.8 summing

<T> Collector<T,?,Double> summingDouble(ToDoubleFunction<? super T> mapper), <T> Collector<T,?,Integer>


summingInt(ToIntFunction<? super T> mapper) and <T> Collector<T,?,Long> summingLong(ToLongFunction<?
super T> mapper) methods are all used to create Collectors that produce arithmetic sum values.
The difference is the type of mapped value which can be double, int or long.
Streams 47

Listing 4.35 Collector summingDouble

orders.stream()
.collect(Collectors.summingDouble(
order -> order.getGrandTotalAmount().doubleValue()));

4.12.8.9 maxBy and minBy

<T> Collector<T,?,Optional<T>> maxBy(Comparator<? super T> comparator) and <T> Collector<T,?,Optional<T


minBy(Comparator<? super T> comparator) methods are used to create Collectors which produce
the maximum or minimum value of the input elements. Value comparison is done using given
Comparator instance. The return result is of type Optional<T> as there is no result for empty streams.

Listing 4.36 Collector maxBy and minBy

orders.stream()
.collect(Collectors.maxBy(Comparator.comparing(Order::getGrandTotalAmount)));

orders.stream()
.collect(Collectors.minBy(Comparator.comparing(Order::getGrandTotalAmount)));

4.12.8.10 summarizing

If we want to know more statistics about a stream’s elements, use <T> Collector<T,?,DoubleSummaryStatistics>
summarizingDouble(ToDoubleFunction<? super T> mapper), <T> Collector<T,?,IntSummaryStatistics>
summarizingInt(ToIntFunction<? super T> mapper) and <T> Collector<T,?,LongSummaryStatistics>
summarizingLong(ToLongFunction<? super T> mapper). These three methods create Collectors
which transform input elements to double, int or long values, then return objects about statistics
of those primitive values.
DoubleSummaryStatistics, IntSummaryStatistics and LongSummaryStatistics are the classes to
represent statistics for different types. These classes have methods to get different statistics.

• getAverage - Get arithmetic mean of values.


• getCount - Get the total number of values.
• getSum - Get the sum of values.
• getMin - Get the minimum value.
• getMax - Get the maximum value.
Streams 48

Listing 4.37 Collector summarizingDouble

orders.stream()
.collect(Collectors.summarizingDouble(
order -> order.getGrandTotalAmount().doubleValue()))

Listing 4.38 DoubleSummaryStatistics of Listing 4.37

DoubleSummaryStatistics{count=3, sum=220.000000,
min=50.000000, average=73.333333, max=100.000000}

4.12.8.11 collectingAndThen

<T,A,R,RR> Collector<T,A,RR> collectingAndThen(Collector<T,A,R> downstream, Function<R,RR>


finisher) method creates a Collector which uses another Collector to collect input elements first,
then transform the collected value to the final result using function finisher.

4.12.8.12 reducing

There are also Collectors to perform reduction operations which can be created using reducing
methods. These reducing methods are similar with Stream’s reduce method. Since these reducing
methods create Collectors, they are especially useful when used as downstream collectors of other
methods, e.g. groupingBy or partitioningBy.
These three reducing methods <T> Collector<T,?,Optional<T>> reducing(BinaryOperator<T>
op), <T> Collector<T,?,T> reducing(T identity, BinaryOperator<T> op) and <T,U> Collector<T,?,U>
reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator<U> op) take
the same input arguments as their corresponding reduce methods in Stream.

4.12.9 Parallel stream


When dealing with large amount of data, parallel processing can generally increase the overall
performance. But parallel processing with multithreading is not an easy task in Java. Badly
implemented multithreading code may yield wrong result or have worse performance than single-
threaded code. Streams make parallel processing much easier than before.
In Listing, values is an array of 10,000 integers. Line 1 and line 3 use the same stream operation max
to get the maximum value in the array. The difference is that line 1 uses sequential stream, while
line 3 uses parallel stream. When running both lines in the same machine, line 1 takes about 100ms
to complete, while line 3 only takes about 5ms. So parallel streams have remarkable performance
improvements over sequential streams. All we need to do is using parallel() method.
Streams 49

Listing 4.39 max for sequential and parallel streams

Arrays.stream(values).max();

Arrays.stream(values).parallel().max();

4.12.9.1 Parallel reduction

Stream’s reduce method can perform reduction on elements. reduce method has a parameter
combiner as the function to combine results from parallel executions. reduce method turns a stream
into a single value as the result. reduce method works by applying accumulator and combiner
functions iteratively to elements and partial results. accumulator and combiner functions should
be stateless, then each method invocation doesn’t modify any shared state. So reduce method can
work correctly for parallel streams. However, reduce method may have performance issues for
certain cases. For example, we can use reduce method to collect elements in a stream into a list.
The accumulator function should take current accumulated List object and current element as
the input parameters and return a new List object which contains all accumulated elements and
current element. The combiner function takes two List objects as the input parameters and returns
a new List object which contains all elements in two input List objects. This may create too many
intermediate List objects which requires more memory to process.
On the other hand, Stream’s collect performs a mutable reduction operation by using shared
objects. Comparing to reduce method, collect requires less memory, but needs to be used carefully
with parallel streams. Collector implementation requires synchronization when accessing shared
objects for parallel streams.

4.13 How-tos

4.13.1 How to convert a Collection to a Map?


We can use Collectors.toMap() method to convert a Collection into a Map. In Listing 4.40, orders
is a list of Order objects, the converted map consists of entries with Order’s id and Order object as
key and value, respectively.
Listing 4.40 Convert a Collection to a Map

List<Order> orders = importData();


orders.stream()
.collect(Collectors.toMap(Order::getId, Function.identity()));
Streams 50

4.13.2 How to filter map entries by keys?


To filter map entries by keys, we also need to use Collectors.toMap() method. In Listing 4.41, orders
is a map of orders. We first get map’s entries, then filter entries with entry -> entry.getKey() > 1
to keep only entries with key greater than 1, finally we use Collectors.toMap() to rebuild the map.
Listing 4.41 Filter map entries by keys

Map<Long, Order> filteredOrders = orders


.entrySet()
.stream()
.filter(entry -> entry.getKey() > 1)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

4.13.3 How to filter map entries by values?


Filtering map entries by values is similar with filtering by keys. The only difference in Listing 4.42
and Listing 4.41 is the filtering condition.
Listing 4.42 Filter map entries by values

Map<Long, Order> filteredOrders = orders


.entrySet()
.stream()
.filter(entry -> entry.getValue().getStatus() == Order.Status.COMPLETED)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

4.13.4 How to group elements by multiple conditions?


We can use chained Collectors.groupingBy methods to group elements by multiple conditions to
create nested maps. In Listing 4.43, orders are first grouped by customer, then grouped again by
status.
Listing 4.43 Group elements by multiple conditions

Map<Customer, Map<Order.Status, List<Order>>> customerOrdersByStatus =


orders
.stream()
.collect(Collectors.groupingBy(Order::getCustomer,
Collectors.groupingBy(Order::getStatus)));
Streams 51

4.13.5 How to combine multiple collections into a stream?


If we have multiple collections of the same type, we can combine them into a single stream using
Stream.concat or Stream.flatMap. In Listing 4.44, list1, list2 and list3 are three lists of String.
The first approach we use Stream.concat twice to concatenate three streams. The second approach
we use Stream.of to create a stream of List<String> first, then use flatMap to get a stream of
String.

Listing 4.44 Combine multiple collections into a stream

List<String> list1 = Lists.newArrayList("Hello", "World");


List<String> list2 = Lists.newArrayList("Thanks", "World");
List<String> list3 = Lists.newArrayList("Goodbye", "World");

Stream.concat(
Stream.concat(list1.stream(),list2.stream()),
list3.stream()
).forEach(System.out::println);

Stream.of(list1, list2, list3)


.flatMap(Collection::stream)
.forEach(System.out::println);

4.13.6 How to convert a stream to another stream with different


type?
If we have a stream of instances from both parent class types and sub-class types, we can use filter
and map to convert it to a stream of sub-class type. In Listing 4.45, class Child extends from Parent.
In convert method, result of Stream.of(new Parent(), new Child(), new Parent()) is a stream
of type Stream<Parent>. We filter the stream using instanceof first, the use Child.class::cast in
map operation to get a stream of type Stream<Child>.

Listing 4.45 Convert a stream to another stream with different type

public class Parent {

public class Child extends Parent {

public void convert() {


Streams 52

Stream.of(new Parent(), new Child(), new Parent())


.filter(elem -> elem instanceof Child)
.map(Child.class::cast)
.forEach(System.out::println);
}

4.13.7 How to find an element in the stream?


Stream has no method to find an element in the stream. We can create a helper method to find an
element using filter and findFirst, see Listing 4.46.
Listing 4.46 Find an element in the stream

public <T> Optional<T> find(final Stream<T> stream,


final Predicate<T> predicate) {
return stream.filter(predicate).findFirst();
}

4.13.8 How to sort map entries?


Stream has sorted method to sort elements in the stream. Listing 4.47 shows how to sort map entries.
data is a Map<String, String> object. Comparator.comparingInt is used to create the Comparator
instance to compare entries by String value’s length. The output of Listing 4.47 is key3=this is
another test.

Listing 4.47 Sort map entries

Map.Entry<String, String> result = data


.entrySet()
.stream()
.sorted(
Comparator.<Map.Entry<String, String>>comparingInt(
entry -> entry.getValue().length()
).reversed())
.findFirst()
.orElse(null);
System.out.println(result);
Streams 53

4.13.9 Exceptions handling in streams


If you look at methods in Stream, you’ll find out that those methods don’t throw any checked
exceptions. There has been a long debate regarding checked and unchecked exceptions. Since these
Stream methods don’t throw any checked exceptions, we need to consider what to do when the
handling methods of a stream may throw checked exceptions. We’ll use a concrete example to
explain exceptions handling in streams.
Suppose that we have an interface UserService, which has a load() method to load User objects
by ID. The load() method throws UserLoadException if something went wrong when loading the
user information.
Listing 4.48 User service

public interface UserService {


User load(String id) throws UserLoadException;
}

Now we have a list of user IDs and we need to return a list of User objects corresponding to these
IDs. So we need to implement the method List<User> loadAllUsers(List<String> userIds).
We cannot simply use userIds.stream().map(userService::load). It’s a compilation error because
we didn’t handle the check exception UserLoadException. Now we need to decide how to handle
the exception. It’s a simple question and the answer depends on the business logic. For example, if
the list of user IDs contain 10 elements and two of them went wrong when loading, what we should
do?
The first choice is to simply ignore these two user IDs and continue with the rest of successfully
loaded User objects. Then we can have following exception handling logic. Here we map the stream
of user IDs to a stream of Optional<User>, then do a filter operation.
Listing 4.49 Best effort exception handling

public List<User> loadAllUsersBestEffort(final List<String> userIds) {


return userIds.stream().<Optional<User>>map(id -> {
try {
return Optional.of(this.userService.load(id));
} catch (UserLoadException e) {
return Optional.empty();
}
}).filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
}
Streams 54

The second choice is to fail the whole loading operation and return no results. The logic behind
this is to treat the whole loading operation as an atomic unit. Here we only to wrap the checked
exception as unchecked and throw the unchecked exception.
Listing 4.50 Fail fast exception handling
public List<User> loadAllUsersFailFast(final List<String> userIds) {
return userIds.stream().map(id -> {
try {
return this.userService.load(id);
} catch (UserLoadException e) {
throw new RuntimeException(e);
}
}).collect(Collectors.toList());
}

The last choice is similar with the second one, except that it can also return the exception. The return
type becomes Tuple2<List<User>, Optional<UserLoadException>>. If the load succeeded, the
Tuple2 contains the List<User> object with Optional object is empty; if any exception occurred dur-
ing the loading, multiple exceptions are reduced to a single one using Throwable.addSuppressed().
Listing 4.51 More exception details
public Tuple2<List<User>, Optional<UserLoadException>> loadAllUsersWithException(
final List<String> userIds) {
return userIds.stream().map(id -> {
try {
return Tuple
.of((List<User>) Lists.newArrayList(this.userService.load(id)),
Optional.<UserLoadException>empty());
} catch (UserLoadException e) {
return Tuple.of((List<User>) Lists.<User>newArrayList(), Optional.of(e));
}
}).reduce(Tuple.of(new ArrayList<>(), Optional.empty()), (r1, r2) -> {
r1._1.addAll(r2._1);
if (r2._2.isPresent()) {
if (r1._2.isPresent()) {
r1._2.get().addSuppressed(r2._2.get());
} else {
return Tuple.of(r1._1, r2._2);
}
}
return r1;
});
}
5. Optional
NullPointerExceptions may be the most seen exceptions during Java developers’ daily develop-
ment. Null reference is considered as The billion dollar mistake by its inventor Tony Hoare¹. Maybe
null should not be introduced into Java language in the first place. But it’s already there, so we have
to deal with it.
Suppose we have a method with argument val of a non-primitive type, we should check first to
make sure the value of val is not null.
Listing 5.1 Check null value

public void myMethod(Object val) {


if (val == null) {
throw new IllegalArgumentException("val cannot be null!");
}
// Use of val
}

Listing 5.1 shows a common pattern of writing code that takes arguments with possible null values.
If a method accepts multiple arguments, all these arguments should be checked. Utility methods like
Objects.requireNonNull() can help, but the code is still long and tedious. Another case is for long
object references, e.g. a.b.c, then all objects in the reference path need to be checked. Groovy has
safe navigation operator² ?., but Java doesn’t the same thing.

5.1 What’s Optional


Java 8 introduced a new class Optional<T>³ to solve the issues with nulls. The idea behind Optional
is quite simple. An Optional object is a holder of the actual object which may be null. Client code
interacts with the Optional object instead of the actual object. The Optional object can be queried
about the existence of the actual object. Although the actual object may be null, there is always an
Optional object.

The major benefit of using Optional is to force client code to deal with the situation that the actual
object that it wants to use may be null. Instead of using the object reference directly, the Optional
object needs to be queried first.
¹http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare
²http://docs.groovy-lang.org/latest/html/documentation/#_safe_navigation_operator
³https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html
Optional 56

Optional objects are very easy to create. If we are sure the actual object is not null, we can use
<T> Optional<T> of(T value) method to create an Optional object, otherwise we should use <T>
Optional<T> ofNullable(T value) method. If we just want to create an Optional object which
holds nothing, we can use <T> Optional<T> empty().
Listing 5.2 Create Optional objects

Optional<String> opt1 = Optional.of("Hello");

Optional<String> opt2 = Optional.ofNullable(str);

Optional<String> opt3 = Optional.empty();

If you pass a null value to Optional.of() method, it throws a NullPointerException.

5.2 Usage of Optional

5.2.1 Simple usage


The simple usage of Optional is to query it first, then retrieve the actual object hold by it. We can
use boolean isPresent() method to check if an Optional object holds a non-null object, then use
T get() method to get the actual value.

Listing 5.3 Simple usage of Optional

public void myMethod(Optional<Object> holder) {


if (!holder.isPresent()) {
throw new IllegalArgumentException("val cannot be null!");
}
Object val = holder.get();
}

Listing 5.3 is similar with Listing 5.1, except it uses Optional. But the code in Listing is still long and
tedious.

5.2.2 Chained usage


Since Optionals are commonly used with if..else, Optional class has built-in support for this kind
of scenarios.
void ifPresent(Consumer<? super T> consumer) method invokes the specified consumer function
when value is present, otherwise it does nothing.
Optional 57

Listing 5.4 Example of Optional.ifPresent

public void output(Optional<String> opt) {


opt.ifPresent(System.out::println);
}

T orElse(T other) returns the value if it’s present, otherwise return the specified other object. This
other object acts as the default or fallback value.

Listing 5.4. Example of Optional.orElse

public String getHost(Optional<String> opt) {


return opt.orElse("localhost");
}

T orElseGet(Supplier<? extends T> other) is similar with orElse, except a supplier function is in-
voked to get the value if not present. getPort() method in Listing invokes getNextAvailablePort()
method to get the port if no value is present.
Listing 5.5 Example of Optional.orElseGet

public int getNextAvailablePort() {


int min = 49152;
int max = 65535;
return new Random().nextInt((max - min) + 1) + min;
}

public int getPort(Optional<Integer> opt) {


return opt.orElseGet(this::getNextAvailablePort);
}

<X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) returns the


value if present, otherwise invokes the supplier function to get the exception to throw.
Listing 5.6 Example of Optional.orElseThrow

public void anotherMethod(Optional<Object> opt) {


Object val = opt.orElseThrow(IllegalArgumentException::new);
}
Optional 58

5.2.3 Functional usage


As Optional objects are the holders of actual objects, it’s not convenient to use when manipulating
the actual objects. Optional provides some methods to use it in a functional way.
Optional<T> filter(Predicate<? super T> predicate) filters a Optional object’s value using
specified predicate. If the Optional object holds a value and the value matches specified predicate,
an Optional object with the value is returned, otherwise an empty Optional object is returned.
Listing 5.7 only outputs a string when it is not empty.
Listing 5.7 Example of Optional.filter

Optional<String> opt = Optional.of("Hello");


opt.filter((str) -> str.length() > 0).ifPresent(System.out::println);

<U> Optional<U> map(Function<? super T,? extends U> mapper) applies specified mapping
function to the Optional’s value if present. If the mapping result is not null, it returns an Optional
object with this mapping result, otherwise returns an empty Optional object.
Listing 5.8 outputs the length of input string.
Listing 5.8 Example of Optional.map

Optional<String> opt = Optional.of("Hello");


opt.map(String::length).ifPresent(System.out::println);

<U> Optional<U> flatMap(Function<? super T,Optional<U>> mapper) is similar with map(), but
the mapping function returns an Optional object. The returned Optional object only contains value
when value if present in both current Optional object and mapping result Optional object.
flatMap() is very useful when handling long reference chain. For example, considering a Customer
class has a method getAddress() to return an Optional<Address> object. The Address class has a
method getZipCode() to return an Optional<ZipCode> object. Listing 5.9 use two chained flatMap()
method invocations to get an Optional<ZipCode> object from an Optional<Customer> object. With
the use of flatMap(), it’s very easy to create safe chained object references.
Listing 5.9 Example of Optional.flatMap

Optional<Customer> opt = getCustomer();


Optional<ZipCode> zipCodeOptional = opt.flatMap(Customer::getAddress)
.flatMap(Address::getZipCode);
zipCodeOptional.ifPresent(System.out::println);
Optional 59

5.3 How-tos

5.3.1 How to interact with legacy library code before Optional?


Not all library code has been updated to use Java 8 Optional, so when we use third-party libraries,
we need to wrap an object using Optional and unwrap an Optional object.
Listing 5.10 Old legacy library code

public Date getUpdated() {


//Get date from somewhere
}

public void setUpdated(Date date) {

Listing 5.11 Wrap legacy library code with Optional

public Optional<Date> getUpdated() {


return Optional.ofNullable(obj.getUpdate());
}

public void setUpdated(Optional<Date> date) {


obj.setUpdated(date.get());

obj.setUpdated(date.orElse(new Date()));
}

5.3.2 How to get value from chained Optional reference path?


Given an object reference path a.getB().getC() with getB() and getC() methods both return
Optional objects, we can use flatMap() method to get the actual value. In Listing 5.12, we use
flatMap() to get value of reference path a.b.c.value.
Optional 60

Listing 5.12 Get value from chained Optional reference path

public class OptionalReferencePath {


public static void main(String[] args) {
new OptionalReferencePath().test();
}

public void test() {


A a = new A();
String value = a.getB()
.flatMap(B::getC)
.flatMap(C::getValue)
.orElse("Default");
System.out.println(value);
}

class C {
private String value = "Hello";

public Optional<String> getValue() {


return Optional.ofNullable(value);
}
}

class B {
private C c = new C();
public Optional<C> getC() {
return Optional.ofNullable(c);
}
}

class A {
private B b = new B();
public Optional<B> getB() {
return Optional.ofNullable(b);
}
}
}

5.3.3 How to get first value of a list of Optionals?


Given a list of Optional objects, we can use code in Listing 5.13 to get the first value.
Optional 61

Listing 5.13 Get first value of a list of Optionals

public class ListOfOptionals {


public static void main(String[] args) {
List<Optional<String>> optionalList = Arrays.asList(
Optional.empty(),
Optional.of("hello"),
Optional.ofNullable(null),
Optional.of("world")
);
String value = optionalList.stream()
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.orElse(null);
System.out.println(value);
}
}

5.3.4 How to chain method invocations with return value of


Optional objects in sequence?

Suppose we want to retrieve a value to be used in the client code and we have different types of
retrievers to use, the code logic is that we try the first retriever first, then the second retriever and
so on, until a value is retrieved. In Listing 5.14, ValueRetriever is the interface of retrievers with
only one method retrieve() which returns an Optional<Value> object. Classes ValueRetriever1,
ValueRetriever2 and ValueRetriever3 are different implementations of interface ValueRetriever.

Listing 5.14 shows two different ways to chain method invocations. First method retrieveResultOrElseGet()
uses Optional’s orElseGet() method to invoke a Supplier function to retrieve the value. Second
method retrieveResultStream() uses Stream.of() to create a stream of Supplier<Optional<Value>>
objects, then filters the stream to only include Optionals with values, then gets the first value.
Listing 5.14 Chain method invocations with return value of Optional objects in sequence

public class ChainedOptionals {


private ValueRetriever retriever1 = new ValueRetriever1();
private ValueRetriever retriever2 = new ValueRetriever2();
private ValueRetriever retriever3 = new ValueRetriever3();

public static void main(String[] args) {


ChainedOptionals chainedOptionals = new ChainedOptionals();
Value value = chainedOptionals.retrieveResultOrElseGet();
Optional 62

System.out.println(value);
value = chainedOptionals.retrieveResultStream();
System.out.println(value);
}

public Value retrieveResultOrElseGet() {


return retriever1.retrieve()
.orElseGet(() -> retriever2.retrieve()
.orElseGet(() -> retriever3.retrieve().orElse(null)));
}

public Value retrieveResultStream() {


return Stream.<Supplier<Optional<Value>>>of(
() -> retriever1.retrieve(),
() -> retriever2.retrieve(),
() -> retriever3.retrieve())
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.orElse(null);
}

class Value {
private String value;
public Value(String value) {
this.value = value;
}

@Override
public String toString() {
return String.format("Value -> [%s]", this.value);
}
}

interface ValueRetriever {
Optional<Value> retrieve();
}

class ValueRetriever1 implements ValueRetriever {

@Override
public Optional<Value> retrieve() {
Optional 63

return Optional.empty();
}
}

class ValueRetriever2 implements ValueRetriever {

@Override
public Optional<Value> retrieve() {
return Optional.of(new Value("hello"));
}
}

class ValueRetriever3 implements ValueRetriever {

@Override
public Optional<Value> retrieve() {
return Optional.of(new Value("world"));
}
}
}

As streams are lazily evaluated, Supplier functions in Listing 5.14 won’t be invoked unless
last Supplier function returns an empty Optional object. This can make sure there won’t
be any unnecessary method invocations.

5.3.5 How to convert an Optional object to a stream?


Sometimes we may want to convert an Optional object to a stream to work with flatMap.
optionalToStream method in Listing 5.15 converts an Optional object to a stream with a single
element or an empty stream.
Listing 5.15 Convert an Optional object to a stream

public <T> Stream<T> optionalToStream(Optional<T> opt) {


return opt.isPresent() ? Stream.of(opt.get()) : Stream.empty();
}
Optional 64

5.3.6 How to use Optional to check for null and assign default
values?
It’s a common case to check if a value is null and assign default value to it if it’s null. Optional can
be used to write elegant code in this case. Listing 5.16 shows the traditional way to check for null
which uses four lines of code.
Listing 5.16 Traditional way to check for null

public void test() {


MyObject myObject = calculate();
if (myObject == null) {
myObject = new MyObject();
}
}

Listing 5.17 shows how to use Optional to do the same thing as Listing 5.16 with only one line of
code.
Listing 5.17 Use Optional to simplify code

public void test() {


MyObject myObject = Optional.ofNullable(calculate()).orElse(new MyObject());
}

You might also like