New Features In Java 19

Introduction

Hi readers, with this article we will review the most significant changes delivered as part of JDK 19, the most recent release of the Java platform.

JDK 19 is the second release of 2022 and the 11th release since Java switched to a semi-annual release cadence. JDK 19 replaces JDK 18 and will be suppressed by JDK 20 in upcoming March 2023. The next LTS release is JDK 21 due in September 2023. JDK 19 is the release at no fee terms & conditions, so anyone can use it in production at no cost. 

JDK 19 brings up 7 enhancement proposals it would be a preview reach release with 6 of those new features delivering either incubators or preview features. Those 7 enhancement proposals are listed below.

Language Features

  1. Pattern Matching for Switch (Preview)
  2. Record Patterns (Preview)

Libraries

  1. Foreign Function and Memory API (Preview)
  2. Vector API (4th Incubator)

Port

  1. Linux/RISC-V Port

Loom Related

  1. Virtual Threads (Preview)
  2. Structured Concurrency (Preview)

There are two features with Language Features from Project Amber, two with library enhancement from Project Panama, and the first set of preview features from Project Loom which aims at making it easier to write multi-threaded applications that properly utilize available resources. Lastly, the only non-preview or incubator feature is the Port to Linux RISC-V. Let’s look at each one of these steps.

Language Features

Starting with language features newly introduced in JDK 19, 

Pattern Matching for a switch (Preview)

Pattern matching for the switch builds upon the work started in JDK 14 with the changes to the switch statement and Pattern matching for instance from JDK 16. This feature brings those two together and introduces pattern matching into switch statements and expressions.

Let us see an example,

Pattern Matching for a switch - Before

static String formatter(Object o) {
    String formatted = "unknown";
    if (o instanceof Integer) {
        formatted = String.format("int %d", i);
    } else if (o instanceof Long) {
        formatted = String.format("long %d", l);
    } else if (o instanceof Double) {
        formatted = String.format("double %f", d);
    } else if (o instanceof String) {
        formatted = String.format("string %s", s);
    }
    return formatted;
}

Switch statements used to work only on a few types numeric, enum, and string with switch expressions. Java introduced other case labels but we still couldn’t use a switch statement to test expression against multiple patterns until JDK 19. We still would have had to change a bunch of else-if statements. 

Pattern Matching for a switch - After

static String formattedPatternSwitch(Object o) {
    return switch (o) {
        case null - > "null";
        case Integer i - > String.format("int %d", i);
        case Long l - > String.format("long %d", l);
        case Double d - > String.format("double %f", d);
        case String s - > String.format("string %s", s);
        default - > o.toString();
    };
}

Using Pattern Matching in a switch expression we can make this clear and as a bonus, this code is optimizable by the compiler.

Remember that the case statement has to be exhaustive so in most cases, you want to add a default value. Notice that you can also add a test for null as another case making the code even easier to record and maintain. To maintain backward compatibility the default label doesn’t match a null selector. 

Pattern Matching for a switch - Guarded Pattern Label

static void test(Object o) {
    switch (o) {
        case String s:
            if (s.length() == 1) {
                ...
            } else {
                ...
            }
            break;...
    };
}

The desired test: if o is the string of length 0 is split between the case and the if statement.

After a successful pattern match, we often need further tests which can lead to cumbersome code like this. We can improve readability if the pattern supported a combination of pattern and boolean expressions. 

Pattern Matching for a switch - When clauses

static void test(Object o) {
    switch (o) {
        case String s when s.length() == 1 - > ...
        case String s - > ......
    };
}

So allowing when clauses in switch blocks allow us to write these more cleanly. 

The first case matches the string and is guarded by s.length == 1

The second case matches strings of other lengths.

The when clause can use any pattern variables declared by the pattern in the case label. In previous pattern matching rather than when we use the short circuit and operator that’s the double ampersand but that become confusing if the expression used boolean operators. So in this preview, this was chained to use the when keyword.

Pattern Matching for a switch - Possible Issues

There are a couple of new issues that arise with a pattern for a switch. 

The dominance of pattern labels

It is a compile-time error if a pattern label in a switch block is dominated by an earlier pattern.

The first one is that patterns could dominate later patterns; it is possible to have an expression that matches multiple labels in a switch block this is allowed but they must be placed in the right order.

static void test(Object o) {
    switch (o) {
        case String s - > ...
        case String s when s.length() == 1 - > ... //Error - pattern is dominated by previous pattern
        ...
    };
}

Here, we have code very similar to what we just used above but with the order of the case statements reversed. Since the first label will catch all strings including strings of length equal to 1, the second label is never reached, it is dominated by an earlier case. This will result in compile time error.

This is one area that has received some improvements from the first premium JDK 17. Constant labels now have to appear before a gathered pattern of the same type.

No fall-through is allowed when declaring a pattern variable 

Another new possible error is to have fallen through when declaring a pattern variable.

switch (o) {
    case Character c:
        System.out.println("Character");
    case Integer i: //Error. Can't fall through!
        System.out.println("Integer" + i);...
}

In this example, if the variable was a character it would print Character but again no break statement execution will flow to the next case label, and since the variable ‘I’ declared in the second pattern would not have been initialized we would get an error. It is therefore a compile-time error to allow flow through to a case that declares a pattern following a label that doesn’t declare a pattern is fine. 

Record Patterns

Enhance the Java Programming Language with record patterns to deconstruct record values. Allow the nesting of record patterns and type patterns. Don’t change the semantics of type patterns. 

Pattern matching for instanceof was introduced in JDK 16 with JEP 394 while Record patterns were introduced in JDK 16 with JEP 395.

Pattern matching and record classes - Deconstructing a record

The next step in our list records patterns preview also merges functionality from two prior enhancements, it allows to use of record and pattern matching both of which were introduced in JDK 16. Let’s look at this example;

record Point(int x, int y) {}
static void printSum(Object o) {
    if (o instanceof Point p) {
        integer x = p.x;
        integer y = p.y;
        System.out.println(x + y);
    }
}

Here, the pattern variable “p” is used solely to invoke the accessor methods X and Y. It would be better if the pattern could not only test whether a value is an instanceof Point but also extract the X and Y components from the value directly. 

record Point(int x, int y) {}
static void printSum(Object o) {
    if (o instanceof Point(int x, int y)) {
        System.out.println(x + y);
    }
}

In this example, point int X and int Y is a record pattern it leaves the declaration of docker variables for extracted components into the pattern itself and it initializes those variables with the right values from the past object. 

Pattern matching and record classes - More complicated Object Graphs

The true power of pattern matching is that it scales elegantly to much more compilated object graphs. 

Consider the following declarations and let's make the Point more complicated, we introduce a color then we define a colored point as a Point and a color and finally, a rectangle is defined by two colored Points.

record Point(int x, int y) {}
enum Color(RED, GREEN, BLUE)
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}

We know we can extract the components of an object with a record pattern. If we want to extract the color upperLeft point we can write but the coloredPoint for the upperLeft is also a record that we can decompose further with nested record patterns.

record Point(int x, int y) {}
enum Color(RED, GREEN, BLUE)
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}
static void printUpperLeftColoredPoint(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
        System.out.println(ul.c);
    }
}

Using a nested pattern we can decompose the outer and inner records at the same time.

record Point(int x, int y) {}
enum Color(RED, GREEN, BLUE)
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}
static void printUpperLeftColoredPoint(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint(Point p, Color c), ColoredPoint lr)) {
        System.out.println(c);
    }
}

Pattern matching and record classes - Using var

A record pattern can use “var” to make against the record component without stating the type of component.

record Point(int x, int y) {}
enum Color(RED, GREEN, BLUE)
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}
static void printUpperLeftColoredPoint(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint(var p, Color c),
            var lr)) {
        System.out.println(c);
    }
}

Keynote: The names of the pattern variables don’t have to be the same as the names of the record components. 

Libraries

Foreign Function and Memory API

Foreign function and Memory API incubated in JDK 17 and JD18. This feature promotes those APIs from the incubator to preview features this also illustrates the relationship between incubators and previews. API by which java programs can interoperate with code and data outside of the java runtime.

These APIs make it much easier and safer to have Java code interoperate with C libraries. The API enables the java program to call native libraries and process native data without the brittleness and danger of JNI. 

Goals

  • Replace Java Native Interface (JNI) with a superior, pure Java development model.
  • Provide performance comparable to or better than that of existing JNI API.
  • Allow operations on different kinds of foreign memory and foreign functions in languages other than C.
  • Allow unsafe operations of foreign memory, but by default warn users of those operations.

The goal of these APIs is to eventually replace the existing Java Native Interface with something better in terms of lower complexity, superior or equivalent performance, and grant access to foreign functions within other languages besides C. These APIs won’t lock developers out off and save operations but they will be better at warning them when they’re doing something potentially unsafe. 

Vector API

Vector computations allow systems to execute operations on vectors instead of applying the same operation to several values one at a time this can result in significant performance improvements. Hotspots support auto-vectorization, so sometimes it can detect when scalar operations can be transformed into super word operations and mapped to vector instructions. 

The set of transformable operations however is limited and fragile to changes in the code shape. These APIs will make it possible to write complex vector algorithms in java in a way it’s far more predictable and robust. 

API to express vector computations that reliably compile at runtime to optimal vector instructions on supported CPU architectures.

Summary 

To finish, let us sum up what we discussed in the whole article. Below are mentioned some points,

  • Pattern Matching for Switch (Preview)
  • Record Patterns (Preview)
  • Foreign Functions and Memory API (Preview)
  • Vector API  (4th Incubator)

There are other features also introduced in JDK 19 which I will be letting you know about in the next article.