JAX London Blog

(R)Evolution Java 8 to 16

Jun 16, 2021

Many companies are still using Java 8, although various exciting innovations have been added in versions 9 to the current 16. Why is that? And what could you be missing out on?

The loyalty to Java 8 can probably be explained by the fact that, when many companies were considering switching to Java 9, Oracle announced a six-monthly release strategy. In addition, many were afraid of the complications in the new module system introduced with Java 9. The uncertainty grew when a new licensing policy was introduced with Java 11. Since this release, Java (Oracle’s JDK) is no longer free for commercial use.

In this article, I want to show you that despite these obstacles, there is a lot in Java versions 9 to 16 worth discovering. And by the end, you should have a pretty good overview of the many new features in them.

The new features start with useful simplifications to the syntax, go on to include some handy extensions to the APIs, and finish with the possibility of clean structuring and subdivision of Java applications into individual modules. But one thing at a time. Let’s start with a quick run-through of the main changes and then focus on some outstanding features.

Concurrency

Major changes have been happening in the area of concurrency. No one wants to program directly with low-level concepts with Threads and Runnables anymore, never mind trying to do this easily and correctly. To utilize multi-core processors better, concurrency should move away from the rather primitive but also complex approaches with threads. Instead, the new APIs make it easier to stay high-level. Here, Doug Lea has made an enormous contribution, inventing Executors, the Fork-Join framework, the great CompletableFuture class (though with a slightly overloaded API), and the Reactive Streams.

HTTP/2-API

Recently, HTTP/2 has become established and is slowly but surely replacing HTTP 1.1. For a long time, there was no support for it in the JDK. But with Java 11 HTTP/2-API was fully integrated into the JDK and offers a comprehensible programming model. This API uses concepts such as request and response of an HTTP communication instead of low-level constructs.

Syntax and API Enhancements

In JDK 9 to 16, there have been extensive enhancements to the APIs in process control, in the Stream API, in Optional, and many other areas. There is a lot to discover. The last versions in particular i.e. JDK 12 to 16, have included various innovations to the syntax. Some were initially provided as preview features to gather feedback and make the final version as practical as possible. I would particularly like to highlight the new, much clearer syntax for switch and the multi-line strings, named Text Blocks. One particular highlight is the possibility to concisely define data container classes using the “record” keyword.

Modularization

Last but not least, Java 9 introduced a new feature – “modularization”. Modularization helps better control the complexity and dependencies of the software, especially when programs become more complex. This can be achieved by subdividing a program into individual components in the form of modules, which should be as independent of each other as possible. Such clean structuring is also desirable for the JDK itself. However, what is sorely missed for common acceptance and later migrations is relevant support from IDEs and tools, which unfortunately only offer rudimentary help for creating new modules.

Example 1: Concurrency

The Future interface has been available in the JDK since Java 5 and it offers a get() method, which often leads to blocking code. As of JDK 8, a remedy is provided by the CompletableFuture class, which enables the definition of asynchronous processing on a higher semantic level compared to Runnable or Callable.

Let’s assume we wanted to parallelize the following method, which first reads data from a server, then processes it in three different ways and finally merges the pieces into one result:


public void execute()
{
List data = retrieveData();

Long value1 = processData1(data);
Long value2 = processData2(data);
Long value3 = processData3(data);

String result = calcResult(value1, value2, value3);
System.out.println("result: " + result);
}

As a rule of thumb one should first – as in this example – encapsulate the business functionality properly in methods that can be tested on their own before considering parallelization. If we tried to parallelize the whole thing at a low level, we wouldn’t want to go through all the hassle of thread synchronization, coordination of tasks, and so on. Better yet, we don’t have to do all this because CompletableFuture allows us to work at a much higher level of abstraction. It’s only necessary to suitably wrap the original method calls with invocations of CompletableFuture to achieve parallel execution. CompletableFuture then takes care of all the tedious details, which would normally need attention.

The version with added parallelization could look like this:


public void execute() throws InterruptedException, ExecutionException
{
CompletableFuture<List> cFData =
CompletableFuture.supplyAsync(() -> retrieveData());

// execute in parallel
CompletableFuture cFValue1 =
cFData.thenApplyAsync(data -> processData1(data));
CompletableFuture cFValue2 =
cFData.thenApplyAsync(data -> processData2(data));
CompletableFuture cFValue3 =
cFData.thenApplyAsync(data -> processData3(data));

// merge subresults synchronously
String result = calcResult(cFValue1.get(),
cFValue2.get(), cFValue3.get());
System.out.println("result: " + result);
}

Since Java 9, you can also add timeouts and exception handling and provide fallback values, as shown below:


CompletableFuture<List> cFData =
CompletableFuture.supplyAsync(() -> retrieveData()).
exceptionally((throwable) -> Collections.emptyList());

CompletableFuture cFValue3 =
cFData.thenApplyAsync(data -> processData3(data)).
completeOnTimeout(7L, 2, TimeUnit.SECONDS);

 

Example 2: HTTP-2 + Optional + Streams + Date and Time API + var

Conveniently, many APIs can be profitably combined, as in this example with the
HTTP/2 API, Stream API, and Optional.

Imagine you are supposed to calculate the exchange rates for a period of a few months. Via REST call at http://data.fixer.io we can retrieve the desired exchange rates free of charge (but we need a key—there is an example below). For this task, the HTTP-2 API is a good choice, and so we write a performGet() method for the REST call. To define the period of time, we use the method datesUntil(), which allows us to create a Stream. By utilizing a TemporalAdjuster we jump to the end of each month and then execute the GET call. Below we also can see the new feature of using var instead of a concrete type. Finally, Optional provides the ifPresentOrElse() method so that we can perform an action in both positive and negative cases:


public static void main(String[] args) throws Exception
{
// HTTP-2-API + Date and Time API
HttpClient httpClient =
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();

// Date and Time API => Stream with monthly steps
LocalDate startDay = LocalDate.of(2021, Month.JANUARY, 1);
LocalDate endDay = LocalDate.of(2021, Month.MAY, 1);
Period stepOneMonth = Period.ofMonths(1);

// datesUntil()
startDay.datesUntil(endDay, stepOneMonth).forEach(localDate ->
{
// TEMPORALADJUSTERS
LocalDate endOfMonth =
localDate.with(TemporalAdjusters.lastDayOfMonth());

// var and HTTP/2-API
var optResponseBody = performGet(httpClient, endOfMonth);

// OPTIONAL
optResponseBody.ifPresentOrElse(
(value) -> System.out.println(endOfMonth + " reported " + value),
() -> System.out.println("No data for " + endOfMonth));
});
}

private static Optional performGet(HttpClient httpClient,
LocalDate desiredDate)
{
try
{
var httpRequest =
HttpRequest.newBuilder().GET().uri(URI.create(
"http://data.fixer.io/" + desiredDate + "?symbols=CHF" +
"&access_key=51b26ac4e9ec53f00ad3aae88ea45be9")).build();

var response = httpClient.send(httpRequest, BodyHandlers.ofString());
String responseBody = response.body();

return Optional.of(responseBody);
}
catch (Exception ex)
{
// LOG
}
return Optional.empty();
}

Executing the above program will result in the Swiss franc to euro exchange rates from January 2021 to May 2021 being printed out, approximately as follows:


2021-01-31 reported {"success":true,"timestamp":1612137599,"historical":true,"base":"EUR","date":"2021-01-31","rates":{"CHF":1.080718}}
2021-02-28 reported {"success":true,"timestamp":1614556799,"historical":true,"base":"EUR","date":"2021-02-28","rates":{"CHF":1.09705}}
2021-03-31 reported {"success":true,"timestamp":1617235199,"historical":true,"base":"EUR","date":"2021-03-31","rates":{"CHF":1.107039}}
2021-04-30 reported {"success":true,"timestamp":1619827199,"historical":true,"base":"EUR","date":"2021-04-30","rates":{"CHF":1.09715}}

Example 3: Switch

In modern Java, there are switch expressions that greatly simplify the specification of case distinctions and provide an intuitive syntax:


DayOfWeek day = DayOfWeek.FRIDAY;

int numOfLetters = switch (day)
{
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
};

From this example, we can recognize some syntactic innovations: in addition to the obvious arrow instead of the colon, multiple values can now be specified for one case. Conveniently, a break is no longer needed. Each of the statements noted after the arrow are executed only for the case. So, with this syntax, there is no fall-through. In addition, switch can now return a value, which avoids the definition of auxiliary variables.

A terrific simplification – only one question remains: “Why only now?”

Example 4: Text Blocks

The Java community has long waited for the possibility to define multi-line strings easily. This feature, called “Text Blocks”, has finally been included and now allows multiple lines of strings without tedious concatenations. Even better, you can also avoid error-prone escaping. This makes it easier to handle for example SQL commands, and to define JavaScript or JSON in Java source code. After their definition, Text Blocks are regular strings so that you can call all usual string methods.
Let’s first look at the syntax for Text Blocks’ before looking at some possible examples of their use.

Basic syntax
Text blocks describe multi-line strings. These begin and end with three quotes each. The actual content starts on a new line:

System.out.println("""
-I am a-
-text block-
""");

Let’s look at the output and its indentation:


-I am a-
-text block-

How does the indentation of only two spaces result when there are some initial spaces before the two lines of text in the println() call? The answer is quite simple: the lower three quotes determine the start of the indentation. In addition, the last line will also have a new line added to it, resulting in an extra blank line.

Text Blocks in action
The following is also possible:


System.out.println("""
First 'line' simple quotes
Second "line" double quotes
Third line \""" three quotes
Fourth line no quotes, just \\ :-)""");

How about some JSON?


String jsonObj = """
{
"name" : "Mike",
"birthday" : "1971-02-07",
"comment" : "Text blocks are nice!"
}
""";
<pre>

Example 5: Records

Modern Java offers a feature called “records”. This allows users to create data container classes in a highly compact way. Records may be beneficial in various use cases, such as returning multiple values, defining compound keys for maps, and acting as DTOs, parameter value objects. Let’s consider a simple definition:

 record MyPoint(int x, int y) { }

This results in a class where the attributes are implicitly derived from the constructor parameters. This is also true for the necessary implementations of the read-only accessor methods. Further, the implementations of equals() and hashCode() are generated automatically and, above all, in conformity with their contracts. The same applies to a suitable constructor and also the toString() method.

Let’s take a look at how much source code would have to be written to achieve equivalent functionality with the existing Java language capabilities:


public final class MyPoint
{
private final int x;
private final int y;

public MyPoint(int x, int y)
{
this.x = x;
this.y = y;
}

@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;

MyPoint point = (MyPoint) o;
return x == point.x && y == point.y;
}

@Override
public int hashCode()
{
return Objects.hash(x, y);
}

@Override public String toString()
{
return "MyPoint[x=" + x + ", y=" + y + "]";
}
// ...
}

Other data containers can be created that are useful either to pass parameters or to return them, for example, the following:


record ColorAndRgbDTO(String name, int red, int green, int blue) { }
record IntStringReturnValue(int code, String info) { }

Conclusion

Java versions 9 to 16 contain a multitude of sensible and practical innovations and additions. Many everyday programming tasks can be realized a little more elegantly and more briefly than if you stick with Java 8. In this article, I have provided a useful overview of these changes but for those who want a more detailed introduction to then, feel free to delve into to my book «Java – die Neuerungen in Version 9 bis 14».

The innovations in Java releases 10, 12, 13, 15, 16, and even the LTS version of Java 11 are fairly manageable. However, Java 14 was a big, exciting step forward, especially with the syntax changes with switch and Text Blocks. But what’s even more exciting is the introduction of the “records” feature, which simplifies the definition of DTO classes, of parameter value objects and tuples for combined return types.

Let’s look briefly at the most substantial innovation, which Java experienced in version 9: modularization. Java finally has a module system, but unfortunately, it has not been well received by either the Java community or tool vendors. This has generated a negative feedback loop and so its distribution is still relatively low.

However, you don’t have to use the module system since a compatibility mode exists. Therefore, you can enjoy all the advantages of the syntax innovations and API extensions to the fullest.

Overall, the new Java versions are worthy successors to Java 8 and should make Java programming attractive for many developers and companies in the future. Personal users in particular have benefited from the fast release cycles, since there is at least one positive surprise to be found in almost every new release.

Behind the Tracks

Software Architecture & Design
Software innovation & more
Microservices
Architecture structure & more
Agile & Communication
Methodologies & more
DevOps & Continuous Delivery
Delivery Pipelines, Testing & more
Big Data & Machine Learning
Saving, processing & more