Introducing Java 8
A quick-start guide to lambdas and streams.
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:
-
Better code readability
-
Simpler support for multicore
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 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 Adopting Streams. For now, think of a Stream
as a new abstraction for expressing data processing queries in a readable way.
Multicore
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 Adopting Streams, I will discuss the details and best practices when using parallel streams.
A Quick Tour of Java 8 Features
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
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 Adopting Lambda Expressions.
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);
The code String::compareToIgnoreCase
is a method reference. It uses the special syntax ::
. (More detail on method references is in the next chapter.)
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 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 Adopting Streams.)
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); }
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); }
New Date and Time API
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:
- Domain-driven design
-
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” andZonedDateTime
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 newLocalDateTime
object and add 2 hours and 30 minutes:
LocatedDateTime coffeeBreak = LocalDateTime.now() .plusHours(2) .plusMinutes(30);
- Immutability
-
One of the problems with
Date
andCalendar
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-04T08:45+01:00[Europe/London] 08:45 PT1H50M PT269H46M55.736S
CompletableFuture
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\n", 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)); }
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 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"
.