Introducing Java 8: A Quick-Start Guide To Lambdas and Streams
Introducing Java 8: A Quick-Start Guide To Lambdas and Streams
Introducing Java 8: A Quick-Start Guide To Lambdas and Streams
Java 8
A Quick-Start Guide to Lambdas
and Streams
Raoul-Gabriel Urma
Additional
Resources
4 Easy Ways to Learn More and Stay Current
Programming Newsletter
Get programming related news and content delivered weekly to your inbox.
oreilly.com/programming/newsletter
O’Reilly Radar
Read more insight and analysis about emerging technologies.
radar.oreilly.com
Conferences
Immerse yourself in learning at an upcoming O’Reilly conference.
conferences.oreilly.com
©2015 O’Reilly Media, Inc. The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. #15305
Introducing Java 8
Raoul-Gabriel Urma
Introducing Java 8
by Raoul-Gabriel Urma
Copyright © 2015 O’Reilly Media, Inc. All rights reserved.
Printed in the United States of America.
Published by O’Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA
95472.
O’Reilly books may be purchased for educational, business, or sales promotional use.
Online editions are also available for most titles (http://safaribooksonline.com). For
more information, contact our corporate/institutional sales department:
800-998-9938 or corporate@oreilly.com.
Editors: Nan Barber and Brian Foster Interior Designer: David Futato
Production Editor: Colleen Lobner Cover Designer: Ellie Volckhausen
Copyeditor: Lindsy Gamble Illustrator: Rebecca Demarest
The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. Introducing Java 8
and related trade dress are trademarks of O’Reilly Media, Inc.
While the publisher and the author have used good faith efforts to ensure that the
information and instructions contained in this work are accurate, the publisher and
the author disclaim all responsibility for errors or omissions, including without limi‐
tation responsibility for damages resulting from the use of or reliance on this work.
Use of the information and instructions contained in this work is at your own risk. If
any code samples or other technology this work contains or describes is subject to
open source licenses or the intellectual property rights of others, it is your responsi‐
bility to ensure that your use thereof complies with such licenses and/or rights.
978-1-491-93434-0
[LSI]
Table of Contents
3. Adopting Streams. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
The Need for Streams 19
What Is a Stream? 20
Stream Operations 21
Filtering 21
Matching 22
Finding 22
Mapping 23
Reducing 23
Collectors 24
Putting It All Together 24
Parallel Streams 26
Summary 27
v
CHAPTER 1
Java 8: Why Should You Care?
Java has changed! The new version of Java, released in March 2014,
called Java 8, introduced features that will change how you program
on a day-to-day basis. But don’t worry—this brief guide will walk
you through the essentials so you can get started.
This first chapter gives an overview of Java 8’s main additions. The
next two chapters focus on Java 8’s main features: lambda expressions
and streams.
There were two motivations that drove the changes in Java 8:
Code Readability
Java can be quite verbose, which results in reduced readability.
In other words, it requires a lot of code to express a simple concept.
Here’s an example: say you need to sort a list of invoices in decreas‐
ing order by amount. Prior to Java 8, you’d write code that looks
like this:
Collections.sort(invoices, new Comparator<Invoice>() {
public int compare(Invoice inv1, Invoice inv2) {
return Double.compare(inv2.getAmount(), inv1.getAmount());
}
});
1
In this kind of coding, you need to worry about a lot of small details
in how to do the sorting. In other words, it’s difficult to express a
simple solution to the problem statement. You need to create a
Comparator object to define how to compare two invoices. To do
that, you need to provide an implementation for the compare
method. To read this code, you have to spend more time figuring
out the implementation details instead of focusing on the actual
problem statement.
In Java 8, you can refactor this code as follows:
invoices.sort(comparingDouble(Invoice::getAmount).reversed());
Now, the problem statement is clearly readable. (Don’t worry about
the new syntax; I’ll cover that shortly.) That’s exactly why you should
care about Java 8—it brings new language features and API updates
that let you write more concise and readable code.
Moreover, Java 8 introduces a new API called Streams API that lets
you write readable code to process data. The Streams API supports
several built-in operations to process data in a simpler way. For
example, in the context of a business operation, you may wish to
produce an end-of-day report that filters and aggregates invoices
from various departments. The good news is that with the Streams
API you do not need to worry about how to implement the
query itself.
This approach is similar to what you’re used to with SQL. In fact, in
SQL you can specify a query without worrying about its internal
implementation. For example, suppose you want to find all the IDs
of invoices that have an amount greater than 1,000:
SELECT id FROM invoices WHERE amount > 1000
This style of writing what a query does is often referred to as
declarative-style programming. Here’s how you would solve the
problem in parallel using the Streams API:
List<Integer> ids = invoices.stream()
.filter(inv -> inv.getAmount() > 1_000)
.map(Invoice::getId)
.collect(Collectors.toList());
Don’t worry about the details of this code for now; you’ll see
the Streams API in depth in Chapter 3. For now, think of a Stream
as a new abstraction for expressing data processing queries in a
readable way.
Lambda Expressions
Lambda expressions let you pass around a piece of code in a concise
way. For example, say you need to get a Thread to perform a task.
You could do so by creating a Runnable object, which you then pass
as an argument to the Thread:
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hi");
}
Multicore | 3
};
new Thread(runnable).start();
Using lambda expressions, on the other hand, you can rewrite the
previous code in a much more readable way:
new Thread(() -> System.out.println("Hi")).start();
You’ll learn about lambda expressions in much greater detail in
Chapter 2.
Method References
Method references make up a new feature that goes hand in hand
with lambda expressions. They let you select an existing method
defined in a class and pass it around. For example, say you need to
compare a list of strings by ignoring case. Currently, you would
write code that looks like this:
List<String> strs = Arrays.asList("C", "a", "A", "b");
Collections.sort(strs, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareToIgnoreCase(s2);
}
});
The code just shown is extremely verbose. After all, all you need is
the method compareToIgnoreCase. Using method references, you
can explicitly say that the comparison should be performed using
the method compareToIgnoreCase defined in the String class:
Collections.sort(strs, String::compareToIgnoreCase);
Streams
Nearly every Java application creates and processes collections.
They’re fundamental to many programming tasks since they let you
group and process data. However, working with collections can be
quite verbose and difficult to parallelize. The following code illus‐
trates how verbose processing collections can be. It processes a list
of invoices to find the IDs of training-related invoices sorted by the
invoice’s amount:
Java 8 introduces a new abstraction called Stream that lets you pro‐
cess data in a declarative way. In Java 8, you can refactor the preced‐
ing code using streams, like so:
List<Integer> invoiceIds =
invoices.stream()
.filter(inv -> inv.getTitle().contains("Training"))
.sorted(comparingDouble(Invoice::getAmount)
.reversed())
.map(Invoice::getId)
.collect(Collectors.toList());
In addition, you can explicitly execute a stream in parallel by using
the method parallelStream instead of stream from a collection
source. (Don’t worry about the details of this code for now. You’ll
learn much more about the Streams API in Chapter 3.)
Enhanced Interfaces
Interfaces in Java 8 can now declare methods with implementation
code thanks to two improvements. First, Java 8 introduces default
methods, which let you declare methods with implementation code
inside an interface. They were introduced as a mechanism to evolve
the Java API in a backward-compatible way. For example, you’ll see
that in Java 8 the List interface now supports a sort method that is
defined as follows:
default void sort(Comparator<? super E> c) {
Collections.sort(this, c);
}
Immutability
One of the problems with Date and Calendar is that they
weren’t thread-safe. In addition, developers using dates as part
of their API can accidentally update values unexpectedly. To
prevent these potential bugs, the classes in the new Date and
ZonedDateTime touchDown
= ZonedDateTime.of(july4,
LocalTime.of (11, 35),
ZoneId.of("Europe/Stockholm"));
Duration flightLength = Duration.between(flightDeparture, touch
Down);
System.out.println(flightLength);
CompletableFuture
Java 8 introduces a new way to think about asynchronous program‐
ming with a new class, CompletableFuture. It’s an improvement on
the old Future class, with operations inspired by similar design
choices made in the new Streams API (i.e., declarative flavor and
ability to chain methods fluently). In other words, you can declara‐
tively process and compose multiple asynchronous tasks.
Optional
Java 8 introduces a new class called Optional. Inspired by functional
programming languages, it was introduced to allow better modeling
in your codebase when a value may be present or absent. Think of it
as a single-value container, in that it either contains a value or is
empty. Optional has been available in alternative collections frame‐
works (like Guava), but is now available as part of the Java API. The
other benefit of Optional is that it can protect you against
NullPointerExceptions. In fact, Optional defines methods to force
you to explicitly check the absence or presence of a value. Take the
following code as an example:
getEventWithId(10).getLocation().getCity();
11
result.add(inv);
}
}
return result;
}
Using this method is simple enough. However, what if you need to
also find invoices smaller than a certain amount? Or worse, what if
you need to find invoices from a given customer and also of a
certain amount? Or, what if you need to query many different prop‐
erties on the invoice? You need a way to parameterize the behavior
of filter with some form of condition. Let’s represent this condi‐
tion by defining InvoicePredicate interface and refactoring the
method to make use of it:
interface InvoicePredicate {
boolean test(invoice inv);
}
There are two forms of lambda expressions. You use the first form
when the body of the lambda expression is a single expression:
(parameters) -> expression
You use the second form when the body of the lambda expression
contains one or multiple statements. Note that you have to use curly
braces surrounding the body of the lambda expression:
(parameters) -> { statements;}
Generally, one can omit the type declarations from the lambda
parameters if they can be inferred. In addition, one can omit the
parentheses if there is a single parameter.
@FunctionalInterface
public interface FileFilter {
boolean accept(File pathname);
}
The important point here is that lambda expressions let you create
an instance of a functional interface. The body of the lambda
expression provides the implementation for the single abstract
method of the functional interface. As a result, the following uses of
Runnable via anonymous classes and lambda expressions will pro‐
duce the same output:
Method References
Method references let you reuse existing method definitions and
pass them around just like lambda expressions. They are useful in
certain cases to write code that can feel more natural and readable
compared to lambda expressions. For example, you can find hidden
files using a lambda expression as follows:
File[] hiddenFiles = mainDirectory.listFiles(f -> f.isHid
den());
Using a method reference, you can directly refer to the method isH
idden using the double colon syntax (::).
File[] hiddenFiles = mainDirectory.listFiles(File::isHidden);
The most simple way to think of a method reference is as a short‐
hand notation for lambda expressions calling for a specific method.
There are four main kinds of method references:
Method References | 15
• A method reference to an instance method. Specifically, you’re
referring to a method of an object that will be supplied as the
first parameter of the lambda:
Function<Invoice, Integer> invoiceToId = Invoice::getId;
• A method reference to an instance method of an existing object:
Consumer<Object> print = System.out::println;
Specifically, this kind of method reference is very useful when
you want to refer to a private helper method and inject it into
another method:
File[] hidden = mainDirectory.listFiles(this::isXML);
In Java 8, the List interface supports the sort method, so you can
use that instead of Collections.sort:
invoices.sort((Invoice inv1, Invoice inv2)
-> Double.compare(inv2.getAmount(),
inv1.getAmount()));
invoices.sort(byAmount);
You may notice that the more concise method reference
Invoice::getAmount can simply replace the lambda (Invoice inv)
-> inv.getAmount():
Comparator<Invoice> byAmount
= Comparator.comparing(Invoice::getAmount);
invoices.sort(byAmount);
Summary
Here are the key concepts from this chapter:
In this chapter, you’ll learn how to adopt the Streams API. First,
you’ll gain an understanding behind the motivation for the Streams
API, and then you’ll learn exactly what a stream is and what it’s used
for. Next, you’ll learn about various operations and data processing
patterns using the Streams API, and about Collectors, which let you
write more sophisticated queries. You’ll then look at a practical
refactoring example. Finally, you’ll learn about parallel streams.
19
architectures. However, writing parallel code is hard and error-
prone.
The Streams API addresses both these issues. It introduces a new
abstraction called Stream that lets you process data in a declarative
way. Furthermore, streams can leverage multicore architectures
without you having to deal with low-level constructs such as
threads, locks, conditional variables, and volatiles, etc.
For example, say you need to filter a list of invoices to find those
related to a specific customer, sort them by amount of the invoice,
and then extract their IDs. Using the Streams API, you can express
this simply with the following query:
List<Integer> ids
= invoices.stream()
.filter(inv ->
inv.getCustomer() == Customer.ORACLE)
.sorted(comparingDouble(Invoice::getAmount))
.map(Invoice::getId)
.collect(Collectors.toList());
You’ll see how this code works in more detail later in this chapter.
What Is a Stream?
So what is a stream? Informally, you can think of it as a “fancy itera‐
tor” that supports database-like operations. Technically, it’s a
sequence of elements from a source that supports aggregate opera‐
tions. Here’s a breakdown of the more formal definition:
Sequence of elements
A stream provides an interface to a sequenced set of values of a
specific element type. However, streams don’t actually store ele‐
ments; they’re computed on demand.
Source
Streams consume from a data-providing source such as collec‐
tions, arrays, or I/O resources.
Aggregate operations
Streams support database-like operations and common opera‐
tions from functional programming languages, such as filter,
map, reduce, findFirst, allMatch, sorted, and so on.
20 | Adopting Streams
Furthermore, stream operations have two additional fundamental
characteristics that differentiate them from collections:
Pipelining
Many stream operations return a stream themselves. This allows
operations to be chained to form a larger pipeline. This style
enables certain optimizations such as laziness, short-circuiting,
and loop fusion.
Internal iteration
In contrast to collections, which are iterated explicitly (external
iteration), stream operations do the iteration behind the scenes
for you.
Stream Operations
The Stream interface in java.util.stream.Stream defines many
operations, which can be grouped into two categories:
Filtering
There are several operations that can be used to filter elements from
a stream:
filter
Takes a Predicate object as an argument and returns a stream
including all elements that match the predicate
Stream Operations | 21
distinct
Returns a stream with unique elements (according to the imple‐
mentation of equals for a stream element)
limit
Returns a stream that is no longer than a certain size
skip
Returns a stream with the first n number of elements discarded
List<Invoice> expensiveInvoices
= invoices.stream()
.filter(inv -> inv.getAmount() > 10_000)
.limit(5)
.collect(Collectors.toList());
Matching
A common data processing pattern is determining whether some
elements match a given property. You can use the anyMatch,
allMatch, and noneMatch operations to help you do this. They all
take a predicate as an argument and return a boolean as the result.
For example, you can use allMatch to check that all elements in a
stream of invoices have a value higher than 1,000:
boolean expensive =
invoices.stream()
.allMatch(inv -> inv.getAmount() > 1_000);
Finding
In addition, the Stream interface provides the operations findFirst
and findAny for retrieving arbitrary elements from a stream. They
can be used in conjunction with other stream operations such as
filter. Both findFirst and findAny return an Optional object
(which we discussed in Chapter 1):
Optional<Invoice> =
invoices.stream()
.filter(inv ->
inv.getCustomer() == Customer.ORACLE)
.findAny();
22 | Adopting Streams
Mapping
Streams support the method map, which takes a Function object as
an argument to turn the elements of a stream into another type.
The function is applied to each element, “mapping” it into a new ele‐
ment.
For example, you might want to use it to extract information from
each element of a stream. This code returns a list of the IDs from a
list of invoices:
List<Integer> ids
= invoices.stream()
.map(Invoice::getId)
.collect(Collectors.toList());
Reducing
Another common pattern is that of combining elements from a
source to provide a single value. For example, “calculate the invoice
with the highest amount” or “calculate the sum of all invoices’
amounts.” This is possible using the reduce operation on streams,
which repeatedly applies an operation to each element until a result
is produced.
As an example of a reduce pattern, it helps to first look at how you
could calculate the sum of a list using a for loop:
int sum = 0;
for (int x : numbers) {
sum += x;
}
Each element of the list of numbers is combined iteratively using the
addition operator to produce a result, essentially reducing the list of
numbers into one number. There are two parameters in this code:
the initial value of the sum variable—in this case 0—and the opera‐
tion for combining all the elements of the list, in this case the
addition operation.
Using the reduce method on streams, you can sum all the elements
of a stream as shown here:
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
Mapping | 23
The reduce method takes two arguments:
Collectors
The operations you have seen so far were either returning another
stream (i.e., intermediate operations) or returning a value, such as a
boolean, an int, or an Optional object (i.e., terminal operations).
By contrast, the collect method is a terminal operation. It lets you
accumulate the elements of a stream into a summary result.
The argument passed to collect is an object of type
java.util.stream.Collector. A Collector object essentially
describes a recipe for accumulating the elements of a stream into a
final result. The factory method Collectors.toList() used earlier
returns a Collector object describing how to accumulate a stream
into a List. However, there are many similar built-in collectors
available, which you can see in the class Collectors. For example,
you can group invoices by customers using Collectors.groupingBy
as shown here:
Map<Customer, List<Invoice>> customerToInvoices
= invoices.stream().collect(Collectors.group
ingBy(Invoice::getCustomer));
24 | Adopting Streams
List<Invoice> oracleAndTrainingInvoices = new ArrayList<>();
List<Integer> ids = new ArrayList<>();
List<Integer> firstFiveIds = new ArrayList<>();
Collections.sort(oracleAndTrainingInvoices,
new Comparator<Invoice>() {
@Override
public int compare(Invoice inv1, Invoice inv2) {
return Double.compare(inv1.getAmount(), inv2.getA
mount());
}
});
Parallel Streams
The Streams API supports easy data parallelism. In other words, you
can explicitly ask for a stream pipeline to be performed in parallel
without thinking about low-level implementation details. Behind
the scenes, the Streams API will use the Fork/Join framework, which
will leverage the multiple cores of your machine.
All you need to do is exchange stream() with parallelStream().
For example, here’s how to filter expensive invoices in parallel:
List<Invoice> expensiveInvoices
= invoices.parallelStream()
.filter(inv -> inv.getAmount() > 10_000)
.collect(Collectors.toList());
26 | Adopting Streams
= expensiveInvoices.parallel()
.collect(Collectors.toList());
Nonetheless, it’s not always a good idea to use parallel streams.
There are several factors you need to take into consideration to
manage performance benefits:
Splittability
The internal implementation of parallel streams relies on how
simple it is to split the source data structure so different threads
can work on different parts. Data structures such as arrays are
easily splittable, but other data structures such as LinkedList or
files offer poor splittability.
Cost per element
The more expensive it is to calculate an element of the stream,
the more benefit from parallelism you can get.
Boxing
It is preferable to use primitives instead of objects if possible, as
they have lower memory footprint and better cache locality.
Size
A larger number of data elements can produce better results
because the parallel setup cost will be amortized over the pro‐
cessing of many elements, and the parallel speedup will out‐
weigh the setup cost. This also depends on the processing cost
per element, just mentioned.
Number of cores
Typically, the more cores available, the more parallelism you
can get.
In practice, I advise that you benchmark and profile your code if
you want a performance improvement. Java Microbenchmark
Harness (JMH) is a popular framework maintained by Oracle that
can help you with that. Without care, you could get poorer perfor‐
mance by simply switching to parallel streams.
Summary
Here are the most important takeaways from this chapter:
Summary | 27
• There are two types of stream operations: intermediate and ter‐
minal operations.
• Intermediate operations can be connected together to form a
pipeline.
• Intermediate operations include filter, map, distinct, and
sorted.
• Terminal operations process a stream pipeline to return a result.
• Terminal operations include allMatch, collect, and forEach.
• Collectors are recipes to accumulate the element of a stream
into a summary result, including containers such as List and
Map.
• A stream pipeline can be executed in parallel.
• There are various factors to consider when using parallel
streams for enhanced performance, including splittability, cost
per element, packing, data size, and number of cores available.
28 | Adopting Streams
Acknowledgments
I would like to thank my parents for their continuous support. In
addition, I would like to thank Alan Mycroft and Mario Fusco, with
whom I wrote the book Java 8 in Action. Finally, I would also like to
thank Richard Warburton, Stuart Marks, Trisha Gee, and the
O’Reilly staff, who provided valuable reviews and suggestions.