Why it's Time to Upgrade to Java 8 Now

The most recent version of Java, Java 8](http://www.oracle.com/technetwork/java/javase/overview/java8-2100321.html), came out over a year ago. While many companies and [developers continue to use older versions, which makes sense given the challenges of upgrading, some are even starting new projects with outdated versions. This is often unnecessary, as Java 8 introduced significant enhancements.

This article will delve into several new features in Java 8, highlighting some of the most practical and intriguing ones:

  • Lambda expressions
  • Stream API for collection manipulation
  • Asynchronous task chaining with CompletableFuture
  • A new Time API

Lambda Expressions

In essence, a lambda is a block of code that can be referenced and passed around for later execution, potentially multiple times. They are similar to anonymous functions found in other languages. Like functions, lambdas can accept arguments during execution, which can modify their output. Java 8 introduced lambda expressions, providing a streamlined syntax for creating and using lambdas.

To illustrate their benefits, let’s examine a straightforward comparator that compares two Integer values based on their modulo 2:

1
2
3
4
5
6
class BinaryComparator implements Comparator<Integer>{
   @Override
   public int compare(Integer i1, Integer i2) {
       return i1 % 2 - i2 % 2;
   }
}

An instance of this class could be used later in code where this comparison logic is needed, like so:

1
2
3
4
5
...
List<Integer> list = ...;
Comparator<Integer> comparator = new BinaryComparator();
Collections.sort(list, comparator);
...

The new lambda syntax simplifies this process. Here’s a concise lambda expression achieving the same outcome as the compare method in BinaryComparator:

1
(Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

This structure resembles a function. Arguments are defined within parentheses, the -> syntax denotes a lambda, and the right side defines the lambda’s behavior.

JAVA 8 LAMBDA EXPRESSION

Let’s refine our previous example:

1
2
3
4
...
List<Integer> list = ...;
Collections.sort(list, (Integer i1, Integer i2) -> i1 % 2 - i2 % 2);
...

We can store this object in a variable:

1
Comparator<Integer> comparator = (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

Now, we can reuse this functionality:

1
2
3
4
5
6
...
List<Integer> list1 = ...;
List<Integer> list2 = ...;
Collections.sort(list1, comparator);
Collections.sort(list2, comparator);
...

Observe how the lambda is passed to the sort() method, just like the BinaryComparator instance in the earlier example. How does the JVM interpret the lambda correctly?

Java 8 introduces the concept of a functional interface to enable functions to accept lambdas as arguments. A functional interface has only one abstract method. Notably, Java 8 treats lambda expressions as specialized implementations of functional interfaces. Therefore, to receive a lambda as an argument, the argument’s declared type only needs to be a functional interface.

When defining a functional interface, we can use the @FunctionalInterface annotation for clarity:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@FunctionalInterface
private interface DTOSender {
   void send(String accountId, DTO dto);
}

void sendDTO(BisnessModel object, DTOSender dtoSender) {
   //some logic for sending...
   ...
   dtoSender.send(id, dto);
   ...
}

Now, we can call the sendDTO method with different lambdas for different behaviors:

1
2
sendDTO(object, ((accountId, dto) -> sendToAndroid(accountId, dto)));
sendDTO(object, ((accountId, dto) -> sendToIos(accountId, dto)));

Method References

Lambda arguments empower us to modify a function’s or method’s behavior. As seen in the last example, sometimes a lambda simply calls another method (sendToAndroid or sendToIos). Java 8 introduces a convenient shorthand for this specific case: method references. This concise syntax, in the form of objectName::methodName, represents a lambda that calls a method. We can now make the previous example even more succinct:

1
2
sendDTO(object, this::sendToAndroid);
sendDTO(object, this::sendToIos);

Here, the sendToAndroid and sendToIos methods are implemented within the current class. We could also reference methods from other objects or classes.

Stream API

Java 8 introduces the Stream API, bringing new capabilities for working with Collections. This new functionality, provided by the java.util.stream package, aims to enable a more functional approach to collection manipulation. As we’ll see, this is largely possible due to the new lambda syntax.

The Stream API simplifies tasks like filtering, counting, and mapping collections, along with providing ways to extract slices and subsets of data. Thanks to its functional-style syntax, it allows for shorter and more elegant code when working with collections.

Let’s begin with a brief example. We’ll use this data model throughout:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Author {
   String name;
   int countOfBooks;
}

class Book {
   String name;
   int year;
   Author author;
}

Imagine we need to print all authors from a books collection who published a book after 2005. In Java 7, it might look like this:

1
2
3
4
5
for (Book book : books) {
   if (book.author != null && book.year > 2005){
       System.out.println(book.author.name);
   }
}

In Java 8, it becomes:

1
2
3
4
5
6
7

books.stream()
       .filter(book -> book.year > 2005)  // filter out books published in or before 2005
       .map(Book::getAuthor)              // get the list of authors for the remaining books
       .filter(Objects::nonNull)          // remove null authors from the list
       .map(Author::getName)              // get the list of names for the remaining authors
       .forEach(System.out::println);     // print the value of each remaining element

Just one expression! The stream() method, available on any Collection, returns a Stream object representing all elements. This stream can be manipulated using various Stream API modifiers like filter() and map(). Each modifier returns a new Stream with the modified results, enabling further manipulation. The .forEach() method lets us perform an action for each element in the resulting stream.

This example highlights the connection between functional programming and lambdas. Notice that the arguments passed to stream methods are either custom lambdas or method references. Technically, each modifier can accept any functional interface, as discussed earlier.

The Stream API offers a fresh perspective on Java collections. Suppose we need a Map of available languages in each country. In Java 7:

1
2
3
4
5
6
7
8
9
Map<String, Set<String>> countryToSetOfLanguages = new HashMap<>();

for (Locale locale : Locale.getAvailableLocales()){
   String country = locale.getDisplayCountry();
   if (!countryToSetOfLanguages.containsKey(country)){
       countryToSetOfLanguages.put(country, new HashSet<>());
   }
   countryToSetOfLanguages.get(country).add(locale.getDisplayLanguage());
}

In Java 8:

1
2
3
4
5
6
7
import java.util.stream.*;
import static java.util.stream.Collectors.*;

...
Map<String, Set<String>> countryToSetOfLanguages = Stream.of(Locale.getAvailableLocales())
      .collect(groupingBy(Locale::getDisplayCountry,
                          mapping(Locale::getDisplayLanguage, toSet())));

The collect() method allows us to collect stream results in various ways. Here, we group by country and then map each group by language. (groupingBy() and toSet() are static methods from the Collectors class.)

JAVA 8 STREAM API

The Stream API offers numerous other features. For a comprehensive understanding, refer to the documentation here. Exploring this package further will unveil its powerful tools.

Asynchronous Task Chaining with CompletableFuture

Java 7’s java.util.concurrent package includes the Future<T> interface, which lets us retrieve the status or result of an asynchronous task in the future. To utilize this, we need to:

  1. Create an ExecutorService to manage asynchronous task execution, generating Future objects for tracking.
  2. Create an asynchronous Runnable task.
  3. Execute the task in the ExecutorService, obtaining a Future for status/result access.

Using asynchronous task results involves external monitoring via the Future interface and explicit result retrieval for further actions once ready. This can be error-prone, especially in applications with many concurrent tasks.

Java 8 enhances the Future concept with the CompletableFuture<T> interface, enabling the creation and execution of asynchronous task chains. This powerful mechanism facilitates asynchronous Java 8 applications by automatically processing each task’s results upon completion.

Here’s an example:

1
2
3
4
5
import java.util.concurrent.CompletableFuture;
...
CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> blockingReadPage())
       .thenApply(this::getLinks)
       .thenAccept(System.out::println);

CompletableFuture.supplyAsync creates a new asynchronous task in the default Executor (usually ForkJoinPool). Upon completion, results are automatically passed to this::getLinks, also running asynchronously. Finally, the second stage’s results are printed to System.out. thenApply() and thenAccept() are just two of several useful methods for building concurrent tasks without manually managing Executors.

CompletableFuture simplifies complex asynchronous operation sequencing. Consider a multi-step mathematical operation with three tasks. Task 1 and task 2 use different algorithms for the first step, with only one guaranteed to work (depending on unknown input data). Their result must be summed with task 3’s result. We need either task 1 or task 2’s result, and task 3’s result. Here’s how we can achieve this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import static java.util.concurrent.CompletableFuture.*;

...

Supplier<Integer> task1 = (...) -> {
   ...                                   // some complex calculation
   return 1;                             // example result
};

Supplier<Integer> task2 = (...) -> {
   ...                                   // some complex calculation
   throw new RuntimeException();         // example exception
};

Supplier<Integer> task3 = (...) -> {
   ...                                   // some complex calculation 
   return 3;                             // example result
};

supplyAsync(task1)     // run task1
       .applyToEither(                   // use whichever result is ready first, result of task1 or
               supplyAsync(task2),       // result of task2
               (Integer i) -> i)         // return result as-is
       .thenCombine(                     // combine result
               supplyAsync(task3),       // with result of task3
               Integer::sum)             // using summation
       .thenAccept(System.out::println); // print final result after execution

Analyzing Java 8’s handling, we’ll see all three tasks run concurrently. Despite task 2 failing, the final result is computed and printed successfully.

JAVA 8 ASYNCHRONOUS PROGRAMMING WITH CompletableFuture

CompletableFuture streamlines multi-stage asynchronous task building and provides a clear interface for defining actions upon each stage’s completion.

Java Date and Time API

Java’s own admission states:

Before Java SE 8, date and time handling relied on java.util.Date, java.util.Calendar, and java.util.TimeZone classes, along with subclasses like java.util.GregorianCalendar. These had drawbacks:

  • Calendar lacked type safety.
  • Mutability made them unsuitable for multithreaded applications.
  • Unconventional month numbering and lack of type safety led to code errors.

Java 8 addresses these issues with the new java.time package, containing immutable date and time classes with APIs similar to the popular Joda-Time framework, often preferred over the native Date, Calendar, and TimeZone.

Useful classes in this package include:

  • Clock - Retrieves current time, instant, date, and time with time zone.
  • Duration and Period - Represent time durations. Duration uses time-based units (e.g., “76.8 seconds”), while Period uses date-based units (e.g., “4 years, 6 months, 12 days”).
  • Instant - Represents a specific point in time.
  • LocalDate, LocalDateTime, LocalTime, Year, YearMonth - Represent date, time, year, month, or combinations without time zones, following the ISO-8601 calendar system.
  • OffsetDateTime, OffsetTime - Represent date and time with UTC/Greenwich offsets (e.g., “2015-08-29T14:15:30+01:00”).
  • ZonedDateTime - Represents date and time with associated time zones (e.g., “1986-08-29T10:15:30+01:00 Europe/Paris”).
JAVA 8 TIME API

To find relative dates like “first Tuesday of the month,” java.time provides the TemporalAdjuster class. TemporalAdjuster offers static methods for:

  • Finding the first/last day of the month.
  • Finding the first/last day of the next/previous month.
  • Finding the first/last day of the year.
  • Finding the first/last day of the next/previous year.
  • Finding the first/last specific weekday within a month (e.g., “first Wednesday in June”).
  • Finding the next/previous specific weekday (e.g., “next Thursday”).

Here’s how to get the first Tuesday of the month:

1
2
3
4
LocalDate getFirstTuesday(int year, int month) {
   return LocalDate.of(year, month, 1)
                   .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY));
}
Still using Java 7? Get with the program! #Java8

Java 8 in Summary

Java 8 marks a significant milestone with numerous language changes, notably the introduction of lambdas, embracing functional programming concepts. The Stream API exemplifies how lambdas can transform our interaction with familiar Java tools.

Furthermore, Java 8 enhances asynchronous programming and revamps date and time handling.

Collectively, these changes signify a substantial leap forward, making Java development more engaging and efficient.

Licensed under CC BY-NC-SA 4.0