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:
Better code readability
Simpler support for multicore
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 decreasing 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
());
}
});
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.
The second big change in Java 8 was necessitated by multicore processors. In the past, your computer would have only one processing unit. To run an application faster usually meant increasing the performance of the processing unit. Unfortunately, the clock speeds of processing units are no longer getting any faster. Today, the vast majority of computers and mobile devices have multiple processing units (called cores) working in parallel.
Applications should utilize the different processing units for enhanced performance. Java applications typically achieve this by using threads. Unfortunately, working with threads tends to be difficult and error-prone and is often reserved for experts.
The Streams API in Java 8 lets you simply run a data processing query in parallel. For example, to run the preceding code in parallel you just need to use parallelStream()
instead of stream()
:
List
<
Integer
>
ids
=
invoices
.
parallelStream
()
.
filter
(
inv
->
inv
.
getAmount
()
>
1_000
)
.
map
(
Invoice:
:
getId
)
.
collect
(
Collectors
.
toList
());
In Chapter 3, I will discuss the details and best practices when using parallel streams.
This section provides an overview of Java 8’s primary new features—with code examples—to give you an idea of what’s available. The next two chapters will focus on Java 8’s two most important features: lambda expressions and streams.
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"
);
}
};
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 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
);
The code String::compareToIgnoreCase
is a method reference. It uses the special syntax ::
. (More detail on method references is in the next chapter.)
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 illustrates 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:
List
<
Invoice
>
trainingInvoices
=
new
ArrayList
<>();
for
(
Invoice
inv:
invoices
)
{
if
(
inv
.
getTitle
().
contains
(
"Training"
))
{
trainingInvoices
.
add
(
inv
);
}
}
Collections
.
sort
(
trainingInvoices
,
new
Comparator
()
{
public
int
compare
(
Invoice
inv1
,
Invoice
inv2
)
{
return
inv2
.
getAmount
().
compareTo
(
inv1
.
getAmount
());
}
});
List
<
Integer
>
invoiceIds
=
new
ArrayList
<>();
for
(
Invoice
inv:
trainingInvoices
)
{
invoiceIds
.
add
(
inv
.
getId
());
}
Java 8 introduces a new abstraction called Stream
that lets you process data in a declarative way. In Java 8, you can refactor the preceding 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.)
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
);
}
Default methods can also serve as a multiple inheritance mechanism for behavior. In fact, prior to Java 8, a class could already implement multiple interfaces. Now, you can inherit default methods from multiple different interfaces. Note that Java 8 has explicit rules to prevent inheritance issues common in C++ (such as the diamond problem).
Second, interfaces can now also have static methods. It’s a common pattern to define both an interface and a companion class defining static methods for working with instances of the interface. For example, Java has the Collection
interface and the Collections
class, which defines utility static methods. Such utility static methods can now live within the interface. For instance, the Stream
interface in Java 8 declares a static method like this:
public
static
<
T
>
Stream
<
T
>
of
(
T
...
values
)
{
return
Arrays
.
stream
(
values
);
}
Java 8 introduces a brand new Date and Time API that fixes many problems typical of the old Date
and Calendar
classes. The new Date and Time API was designed around two main principles:
The new Date and Time API precisely models various notions of date and time by introducing new classes to represent them. For example, you can use the class Period
to represent a value like “2 months and 3 days” and ZonedDateTime
to represent a date–time with a time zone. Each class provides domain-specific methods that adopt a fluent style. Consequently, you can chain methods to write more readable code. For example, the following code shows how to create a new LocalDateTime
object and add 2 hours and 30 minutes:
LocatedDateTime
coffeeBreak
=
LocalDateTime
.
now
()
.
plusHours
(
2
)
.
plusMinutes
(
30
);
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 Time API are all immutable. In other words, you can’t change an object’s state in the new Date and Time API. Instead, you use a method to return a new object with an updated value.
The following code exemplifies various methods available in the new Date and Time API:
ZoneId
london
=
ZoneId
.
of
(
"Europe/London"
);
LocalDate
july4
=
LocalDate
.
of
(
2014
,
Month
.
JULY
,
4
);
LocalTime
early
=
LocalTime
.
parse
(
"08:45"
);
ZonedDateTime
flightDeparture
=
ZonedDateTime
.
of
(
july4
,
early
,
london
);
System
.
out
.
println
(
flightDeparture
);
LocalTime
from
=
LocalTime
.
from
(
flightDeparture
);
System
.
out
.
println
(
from
);
ZonedDateTime
touchDown
=
ZonedDateTime
.
of
(
july4
,
LocalTime
.
of
(
11
,
35
),
ZoneId
.
of
(
"Europe/Stockholm"
));
Duration
flightLength
=
Duration
.
between
(
flightDeparture
,
touchDown
);
System
.
out
.
println
(
flightLength
);
// How long have I been in continental Europe?
ZonedDateTime
now
=
ZonedDateTime
.
now
();
Duration
timeHere
=
Duration
.
between
(
touchDown
,
now
);
System
.
out
.
println
(
timeHere
);
This code will produce an output similar to this:
2015
-
07
-
04
T08:
45
+
01
:
00
[
Europe
/
London
]
08
:
45
PT1H50M
PT269H46M55
.
736
S
Java 8 introduces a new way to think about asynchronous programming 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 declaratively process and compose multiple asynchronous tasks.
Here’s an example that concurrently queries two blocking tasks: a price finder service along with an exchange rate calculator. Once the results from the two services are available, you can combine their results to calculate and print the price in GBP:
findBestPrice
(
"iPhone6"
)
.
thenCombine
(
lookupExchangeRate
(
Currency
.
GBP
),
this
::
exchange
)
.
thenAccept
(
localAmount
->
System
.
out
.
printf
(
"It will cost you %f GBP "
,
localAmount
));
private
CompletableFuture
<
Price
>
findBestPrice
(
String
productName
)
{
return
CompletableFuture
.
supplyAsync
(()
->
priceFinder
.
findBestPrice
(
productName
));
}
private
CompletableFuture
<
Double
>
lookupExchangeRate
(
Currency
localCurrency
)
{
return
CompletableFuture
.
supplyAsync
(()
->
exchangeService
.
lookupExchangeRate
(
Currency
.
USD
,
localCurrency
));
}
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 frameworks (like Guava), but is now available as part of the Java API. The other benefit of Optional
is that it can protect you against Null
Pointer
Exceptions
. 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
();
If getEventWithId(10)
returns null
, then the code throws a NullPointerException
. If getLocation()
returns null
, then it also throws a NullPointerException
. In other words, if any of the methods return null
, a NullPointerException
could be thrown. You can avoid this by adopting defensive checks, like the following:
public
String
getCityForEvent
(
int
id
)
{
Event
event
=
getEventWithId
(
id
);
if
(
event
!=
null
)
{
Location
location
=
event
.
getLocation
();
if
(
location
!=
null
)
{
return
location
.
getCity
();
}
}
return
"TBC"
;
}
In this code, an event may have an associated location. However, a location always has an associated city. Unfortunately, it’s often easy to forget to check for a null
value. In addition, the code is now more verbose and harder to follow. Using Optional
, you can refactor the code to be more concise and explicit, like so:
public
String
getCityForEvent
(
int
id
)
{
Optional
.
ofNullable
(
getEventWithId
(
id
))
.
flatMap
(
this
::
getLocation
)
.
map
(
this
::
getCity
)
.
orElse
(
"TBC"
);
}
At any point, if a method returns an empty Optional
, you get the default value "TBC"
.