Should you upgrade to a newer version of Java?

A quick tour of important Java features added in Java 9 through Java 16

All technology goes through innovation and evolution; such is the way with any good programming language as well. A programming language should evolve and grow to stay relevant. The iPhone 6 and Java 8 were both released in the year 2014 - which is seven, almost eight, years ago at this point. Most of us have upgraded away from the iPhone 6 by now, but many large organizations are still stuck using older versions of Java. So why aren’t more organizations using Java 16? Upgrading to a new version of your favorite programming language should be as exciting as upgrading your favorite gadget.

If you’re using an older version of Java, you’re missing out on a lot of excitement. You can see from the image from Oracle that 100+ new features have been added into Java since Java 8 was released. This article is a condensed version of Java’s official release notes, covering Java 9 through Java 16, with examples of some of the interesting language additions that can impact decisions on whether to upgrade from older Java versions.

But before we dive into the details of each release and their features, let’s take a look at the below support roadmap for the Oracle Java SE.

The support roadmap will be important to you if you are using the Oracle Java SE for your production deployments; you can be less worried about the support roadmap if you are using the OpenJDK for production deployments. This article is focused only on the language updates and interesting features developers should be aware of, and covers both the long-term support and short-term support releases.

  • Short-term support (STS or non-LTS) - once in every 6 months.
  • Long-term support (LTS) - once in every 3 years.

Java 9 Release and Features

Java 9 has been generally available since Sep 2017. There were many production ready and incubated features included in this release. In this section we will cover some of the more important features including:

  • Java Platform Module System (JPMS)
  • Jlink the Java linker
  • Jshell (Read-Eval-Print loop) a CLI for Java
  • Keystore format changes.
  • Integrated JVM logging
  • Important language changes

1.  Java Platform Module System (JPMS)

JDK is a monolith. We need to install the entire JDK just to run a simple hello world program. The JDK comes with packages like applet, awt, corba, swing, etc. which usually aren’t used for building backend applications. See the problem? Additional classes are loaded which you may not need, which means more time for the application to start and a fat java runtime. This is why modularization was introduced in Java 9.

Let's look at how you can create custom modules.

Run this command in Java 9 or later:

    java --list-modules

What you see below is the list of modules bundled into the JDK16. Note that the java.base module is the parent module.


Get more details about the module by running this command.

    java --describe-module java.base

Note: The exports in the base module contain several other modules and that it exports them explicitly so that they can be accessed by the required packages in other modules.

exports java.lang
exports java.lang.annotation
uses java.text.spi.DateFormatProvider
uses java.nio.charset.spi.CharsetProvider
uses sun.util.spi.CalendarProvider
provides java.nio.file.spi.FileSystemProvider with jdk.internal.jrtfs.JrtFileSystemProvider
qualified exports jdk.internal.access.foreign to jdk.incubator.foreign
qualified exports to jdk.jartool
qualified exports to jdk.crypto.cryptoki
qualified exports to jdk.jfr
contains com.sun.crypto.provider

Notice the module has many directives like exports, uses, provides, qualified and contains. These directives are declared in the module descriptor file called and shows the DNA of the module.

How to create a custom module?

Creating a custom module is simple. Wait... did I say simple? Yes, that's true. But only after spending several hours understanding how everything works. 😁

From the below snapshot consider the JAVA9_MODULES as your base project directory. The SRC directory contains the source code for the modules we are generating: in this case:

  2. com.module.two

Each module should have its own where you define the dependency and export the accessible packages.

Screenshot of project structure

Screenshot of project structure

Screenshot of of module one

Screenshot of of module one

Notice that the file declares the “requires” module and the exported package. I am just showing the basic module here, but it has a lot more to it.

    package com.moduleone.demo;
import com.moduletwo.demo.ModuleTwo;
public class ModuleOne {
    public static void main(String... args){
        System.out.println(new ModuleTwo().sayHello("Module 2"));

    package com.moduletwo.demo;

public class ModuleTwo {
    public String sayHello(String name){
        return "Hello "+ name;

Run the javac command to compile the source code and module info file and move it to the mods directory.

    javac -d mods --module-source-path src/ src/ src/ src/com.module.two/ src/com.module.two/com/moduletwo/demo/

Next, run this, which will output Hello Module 2:

    java --module-path mods --module

Module is a collection of related packages and data. We typically write package → classes. With modules this will change to module→ package→ classes. JPMS introduces a stricter encapsulation. i.e. not all the public classes are available for a dependent system unless the package was explicitly allowed by the owner of the package via export.

2. Jlink: The Java linker

You can use the jlink tool to assemble and optimize a set of modules and their dependencies into a custom runtime image.

Run the below command:

    jlink --module-path $JAVA_HOME/jmods:mods --add-modules,com.module.two --output customapp

This will create the customapp directory with the custom Java runtime. Notice the release only has the modules we defined in the modules-info files. 

customapp directory with the custom Java runtime

3. Jshell (Java Read-Eval-Print Loop)

This interactive tool is a great addition to the jdk. Using this, developers can run prototyping or testing types of expressions and statements. Once you get used to this tool, you may not even need to open your favorite IDE to run any quick experiments.

4. Change in keystore formats

Do you pay attention to the warning messages displayed by the software applications you are using? If so, have you noticed this warning message while attempting to create a keystore file with lower versions of Java?

    keytool -keystore jdk8 -genkey -alias client -keyalg rsa
The JKS keystore uses a proprietary format. It is recommended to migrate to PKCS12 which is an industry standard format using "keytool -importkeystore -srckeystore jdk8 -destkeystore jdk8 -deststoretype pkcs12"

Java made pkcs12 the default format since the release of Java 9. JKS is proprietary to Oracle and it has been the way Java has maintained the private keys and certificates. However, pkcs is the language-neutral way of maintaining these. Note: you can still use the JKS file format in the newer versions of Java, but you may not create a new one.

5. JVM logging

We all know the power of good logs. Log files are the first place engineers will dive in to analyze application issues. Log files are like the black box of an aircraft. And JVM is like the engine for the Java base application. Java 9 introduced unified JVM logging which can be enabled by passing the below:

    option         :=  [][:[][:[][:]]]

The default configuration is:


Example with log rotation:

To get the list

To get the list of available selectors, tag list, output options, decorators, etc, run:

    java -Xlog:help

6. Language changes

Convenient Factory methods in Set, List and Map

New factory methods have been introduced in the Collections framework which simplifies the creation of Sets, Lists and Map data types with predefined values.

Java 8 standard:

    Set colors = new HashSet<>();

In Java 9 this can be done in one line:

    Set colors = Set.of("red","blue","green");

Compact Strings

From the JEP 254 problem statement - until Java 8, string class stored characters in a char array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char arrays of such String objects is going unused. This was changed in Java 9.

From this release, Java 9 will internally use the byte array with an encoding option. This is an internal implementation change only; the String API methods didn't change. Applications that create too many Strings will surely see a reduction in memory consumption as by default, compact strings are enabled. If for some reason you want to disable compact strings, use the -XX:-CompactStrings Java command line option.

Java 10 Release and Features:

This was a non-LTS release which was made generally available in March 2018. This was released with a dozen new features. In this section we will go through the four most important of those features:

  • Local variable type inference
  • Inclusion of Optional orElseThrow() method
  • Improvements for running Java in docker containers
  • Collections api updates to create unmodifiable collections

1. Local variable type inference

Java 10 introduced the var keyword for the local variable type inference. The compiler now does the type inference, no need to declare that.

    //Java 9 or below:
Map keyValue = new HashMap();
//Java 10 or above:
var keyValue = new HashMap()

Note: this can be only done in the local variable.

2. Optional orElseThrow

Java 10 introduced Optional.orElseThrow() as a recommended replacement for the Optional.get() method as the latter method is error prone.

Some other features that were introduced included new APIs to create unmodifiable collections, garbage collection improvements, and the addition of new root CA certificates.

Improvements for docker containers

Starting with Java 10, Java can be instructed to optimize its performance and settings when it runs inside a container. Java runtime parameters have been introduced to make the JVM aware that it is running in a Docker container and will extract container specific configuration information instead of querying the operating system.

Some helpful settings include:

  • Container support is enabled by default, it can be disable if you have to by setting: -XX:-UseContainerSupport
  • The number of CPUs allocated to the JVM by the container can be configured by setting: -XX:ActiveProcessorCount=count
  • The amount of memory to be allocated to JVM by the container can be configured by the settings: -XX:InitialRAMPercentage, -XX:MaxRAMPercentage, -XX:MinRAMPercentage

3. Immutable Collections:

We have seen how to use the Set.Of, List.Of, and Map.Of Java 9 factory methods. In Java 10 this has been further enhanced. The Collections API has been updated with new methods to create unmodifiable collections.

    //immutable List
List colors = List.of("Red","Blue","Green");
List immutable_colors = -> color.toUpperCase()).collect(Collectors.toUnmodifiableList());

//immutable Map
Map  immutable_javaReleases = Collections.unmodifiableMap( Map.of(2014,"Java8",2017,"Java 9",2018,"Java10",2021,"Java17"));
immutable_javaReleases.forEach((k,v) -> System.out.println("key:"+ k+ ",value:"+ v));

Java 11 Release and Features:

Java 11 was a LTS release made available in Sep 2018 and included more features than Java 10. In this section we will cover the following Java 10 features:

  • Execute programs in a single file in a single step
  • String utility methods
  • Standard Http client
  • TLS v1.3 support
  • Simplified file writing and reading functions

1. Execute programs in a single file in a single step

Exploratory code contained in a single file can be directly run without running the javac compile.

    class SingleFile {
  public static void main(String... args){
    SingleFile obj = new SingleFile();
  private String sayHello(String name){
    return "Hello "+name;

Before Java 11 we had to first run the javac, then run java SingleFile. This can be done in Java 11 by simply running java

2. String utility methods

In Java 11 a few notable utility methods have been added in the String class.This includes

    String s ="*";
System.out.println(s.repeat(10)); //**********
s = "   *   ";
System.out.println(s.strip()); //*
s = "   *   ";
System.out.println(s.stripLeading()); //*  
s = "   *   ";
System.out.println(s.stripTrailing());//   *
s= "";
System.out.println(s.isBlank()); //true

3. Java 11 and HTTP client

This was originally incubated in Java 9 under the jdk.incubator.http package, it was then standardized in Java 11 under the package. This was one of the features I was personally looking for in Java. Now we can build http client applications without the need for a library dependency (ex: Apache http client, Jersey, etc). Requests can be sent either synchronously or asynchronously. Here are some examples.

4. Transport Layer Security (TLS) 1.3 support

TLS 1.3 has security and performance improvements, Starting in Java 11, Java supports TLS 1.3. Note: it was backported to specific versions of Oracle and OpenJDK 8.

5. Simplified file writing and reading functions

You no longer need to explicitly use the buffered reader, buffered writer or input and output streams to write strings to a file. Java 11 added convenient methods to the Files API to perform that.

    //Writing string to a file
Files.writeString(Path.of("./", "sample.txt"), "This goes into a file", Charset.forName("UTF-8"), StandardOpenOption.CREATE_NEW);

//Reading from a file. 
String data= Files.readString(Path.of("./", "sample.txt"), Charset.forName("UTF-8"));
System.out.println(data);  //This goes into a file

Java 12 Release and Features

We are just half way through at this point! It's quite a journey, isn’t it? Java 12 is a non-LTS release that was made GA in Mar 2019 with a few new features. In this section we will cover the following:

  • Compact number formatting
  • -XX+ExtensiveErrorReports
  • New Switch case expression

1. Compact number formatting

With Java 12, it is now easy to return compact numbers. For example: 1000 as 1 thousand or 1000 as 1K ,100000 as 100K and 1000000 as 1M etc depending on the chosen style.

    NumberFormat longFmt = NumberFormat
                .getCompactNumberInstance(Locale.US, NumberFormat.Style.LONG);
System.out.println( longFmt.format(1000) );  //1 thousand
NumberFormat shortFmt = NumberFormat
                .getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);
System.out.println( shortFmt.format(1000) ); //1K

You may notice that format short outputs 1K and format long outputs a longer text version. Also, we can use the parse() method to convert 1K to 1000.

2. -XX+ExtensiveErrorReports

Adding this Java command line argument will include extensive error messages in the hs_err<pid>.log file in JVM crash scenarios.

This version doesn't have major standard features. There are very few notable additions though.

3. New switch expression (preview)

In Java 12 switch expression statements were updated with lambda-style statements.

    String day ="MONDAY";
        var weekDay = switch(day) {
            case "MONDAY", "TUESDAY", "WEDNESDAY","THURSDAY","FRIDAY" -> "weekday";
            case "SATURDAY","SUNDAY" -> "weekend";
            default -> "invalid input";
        System.out.println(weekDay); // weekday

A break statement is no longer needed after each case statement, and case statements can have more comma delimited case checks. This has been a preview feature in this release.

Java 13 Release and Features

Java 13 was made GA in Sep 2019. This was a non-LTS minor release, the following features are highlights of this release:

  •  Text block (preview)
  •  Switch with yield

1. Text blocks (preview)

Starting with Java 13 (in preview mode) we can declare text fields in multiple lines. 

    var text = """
        this is line1 \
        this is line 2 \
        this is the end of line\
        System.out.println(text); // this is line1 this is line 2 this is the end of line

2. Switch Case expressions with yield

The switch case statement has been going through some updates, in Java 13 the yield keyword was added as a syntactic sugar to return value from the case statement.

    List greetings = List.of("Enjoy the weekend", "have a nice weekend","TG its a weekend");
        Random rand = new Random();
        String weekDay = switch(day) {
            case "MONDAY", "TUESDAY", "WEDNESDAY","THURSDAY","FRIDAY" -> "weekday";
            case "SATURDAY","SUNDAY" -> {
                yield greetings.get(rand.nextInt(2));
            default -> "invalid input";
        System.out.println(weekDay); //Enjoy the weekend

Notice with the yield, the case statement is enhanced to do a little bit more than the previous versions. The yield statement makes it easier for you to differentiate between switch statements and switch expressions. A switch statement, but not a switch expression, can be the target of a break statement. Conversely, a switch expression, but not a switch statement, can be the target of a yield statement.

Java 14 Release and Features

Java 14 is another non-LTS release which became GA in Mar 2020 with some interesting features. In this section we will cover:

  • Enhanced NullPointerException
  • Records (preview)
  • instanceOf with pattern matching

1. Enhanced NullPointerException

NullPointerExceptions (NPE) are infamous exceptions, which is a direct indication that they are not well unit tested code. Before Java 14, troubleshooting null pointer issues was a lot of guess work.  Java 14 addressed this issue by adding additional information about the null pointer exception, which will take you directly to the offending code.

Null pointer exception in Java 8:

Null pointer exception in Java 8

Null pointer exception in Java 14:

Null pointer exception in Java 14

2. Records (preview)

This feature simplifies the model/entity/data class creation. Before Java 14, we typically had to write the getters and setters, hashCode() and toString() methods. The compiler will now add those for us.

    jshell> record Customer(String firstName,String lastName){}
| created record Customer
jshell> Customer c1= new Customer("Super","Mario");
c1 ==> Customer[firstName=Super, lastName=Mario]
jshell> c1.firstName();
$3 ==> "Super"
jshell> c1.lastName();
$4 ==> "Mario"
jshell> c1.toString();
$5 ==> "Customer[firstName=Super, lastName=Mario]"
jshell> c1.hashCode();
$6 ==> -1733144855
jshell> c1.equals(new Customer("Super","Mario"));
$7 ==> true

Since this is a preview release, I am not adding the additional record features here.

3. instanceOf with Pattern matching(Preview)

It is a general practice where the instanceof validation code is often followed by the explicit type casting statement. This problem was addressed in this release.

Before Java 14:

    Object someObj ="some text";
if(someObj instanceof String){
String k =(String) someObj;

After Java 14:

    Object someObj ="some text";
        if(someObj instanceof String k){

Notice that the explicit casting is no longer required and Java does the object pattern matching for us.

Java 15 Release and Features

Another non-LTS release, Java 15 made its way in Sep 2020 with a handful of new features. In this section we will cover:

  • TextBlocks
  • Sealed classes
  • Garbage collector updates

1. TextBlocks (standard)

Though text block was first released as a preview in Java 13, it has been standard since Java 15. A text block is a multi-line string literal that avoids the need for most escape sequences, automatically formats the string in a predictable way, and gives the developer control over the format when desired. Java 15 onwards will now support below string declarations.

2. Sealed classes

This falls in the inheritance space, as it provides the subclass permission control to the parent class.

    public abstract sealed class Seasons permits Winter,Spring,Summer,Fall {}

In the above example, only the Winter, Spring, Summer, and Fall classes are allowed to extend the Season’s parent class.

3. Garbage collectors

The ZGC (low latency, scalable  and Shenandoah (low pause time) garbage collectors were previously in preview or experimental mode since Java 11 and 12 respectively. However, since Java 15 these garbage collectors have become standard garbage collection options, with G1GC remaining as the default.

ZGC is enabled by using the -XX:+UseZGC. To enable Shenandoah use -XX:+UseShenandoahGC. It’s worth noting that Shenandoah isn’t enabled in the JDK builds that Oracle ships, but other OpenJDK distributors are enabled with Shenandoah.

Java 16 Release and Features

Java 16 is the last GA release in our list and was released in Mar 2021. This release has converted some of the preview features as standard. In this section we will take a look at:

  • Warnings for value-based classes
  • Some miscellaneous items

1. Warnings for value-based classes

If you are not aware of what a value based class is, read here.

There are some classes in Java annotated as value based classes. @jdk.internal.ValueBased. The wrapper class Double is one such value based class. The below code snippet will throw the Warning in Java 16:

    Double d = 1.0;
synchronized (d) {} // this will fail at compile time with an attempt to synchronize on an instance of a value-based class.

2. Miscellaneous items:

  • instanceOf with pattern matching, which has been in preview since Java 14, becomes a standard feature in this release.
  • Records, which was a preview feature since Java 14, has found its place as a standard feature in this release.
  • OpenJDK’s source code has been moved to Github.


We have just scratched the surface with changes to the Java programming language. There have been more minor features additions and removals, deprecated methods, garbage collection improvements, performance fixes, and other bug fixes that have been made in this long list of releases.

Java is celebrating its 25th birthday in 2021 and is still growing! There are a lot of features in the pipeline, with the Java 17 LTS release expected in September 2021. As the language continues growing and additional releases are made, applications still using earlier versions of Java should look to upgrade to Java 11 LTS or above.

Explore #LifeAtCapitalOne

Startup-like innovation with Fortune 100 capabilities.

Learn more

Arunkumar Ganesan, Distinguished Engineer

A seasoned software engineering professional, passionate about system design, software architecture, microservices, automation, testing and backend development. In-depth expertise in building cloud native applications, distributed processing, and big data technologies.

Related Content