Introducing Java 8

A quick-start guide to lambdas and streams.

By Raoul-Gabriel Urma
November 8, 2015
Tiger in water Tiger in water (source: O'Reilly)

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.

Learn faster. Dig deeper. See farther.

Join the O'Reilly online learning platform. Get a free trial today and find answers on the fly, or master something new and useful.

Learn more

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” 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);
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 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 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();

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".

Post topics: Software Engineering
Share: