Table of Contents
- 16.1.Vector API Incubator
- 16.2.Enable C++14 Language Features
- 16.3 Migrate from Mercurial to Git - Migrate to GitHub
- 16.4 Concurrent Thread-Stack Processing
- 16.5 +Alpine Linux Port - Elastic Metaspace
- 16.6 Windows/AArch64 Port
- 16.7 Invoke Default Methods From Proxy Instance
- 16.8.Day Period Suppor
- 16.9 Records
- 16.10 Pattern Matching for instanceof
- 16.11 Sealed Classes
- 16.12 New Additions to Sealed Classes in Java 16
- 16.13 Other Changes
16.1. Vector API Incubator
The Vector API is in its initial incubation phase for Java 16. The idea of this API is to provide a means of vector computations that will ultimately be able to perform more optimally (on supporting CPU architectures) than the traditional scalar method of computations.
Let's look at how we might traditionally multiply two arrays:
int[] a = {1, 2, 3, 4}; int[] b = {5, 6, 7, 8}; var c = new int[a.length]; for (int i = 0; i < a.length; i++) { c[i] = a[i] * b[i]; }
This example of a scalar computation will, for an array of length 4, execute in 4 cycles. Now, let's look at the equivalent vector-based computation:
int[] a = {1, 2, 3, 4}; int[] b = {5, 6, 7, 8}; var vectorA = IntVector.fromArray(IntVector.SPECIES_128, a, 0); var vectorB = IntVector.fromArray(IntVector.SPECIES_128, b, 0); var vectorC = vectorA.mul(vectorB); vectorC.intoArray(c, 0);
The first thing we do in the vector-based code is to create two IntVectors from our input arrays using the static factory method of this class fromArray. The first parameter is the size of the vector, followed by the array and the offset (here set to 0). The most important thing here is the size of the vector that we're getting to 128 bits. In Java, each int takes 4 bytes to hold.
Since we have an input array of 4 ints, it takes 128 bits to store. Our single Vector can store the whole array.
On certain architectures, the compiler will be able to optimize the byte code to reduce the computation from 4 to only 1 cycle. These optimizations benefit areas such as machine learning and cryptography.
We should note that being in the incubation stage means this Vector API is subject to change with newer releases.
16.2. Enable C++14 Language Features
To allow the use of C++ 14 capabilities in JDK C++ source code and give specific guidance about which of these features may be used in HotSpot VM code.
Through JDK 15, language features used by C++ code in the JDK have been limited to the C++98/03 language standards. With JDK 11, the source code was updated to support building with newer versions of the C++ standard.
This includes being able to build with recent versions of compilers that support C++ 11/14 language features.
This proposal does not propose any style or usage changes for C++ code that is used outside of HotSpot. But to take advantage of C++ language features, some build-time changes are required, depending on the platform compiler.
16.3 Migrate from Mercurial to Git - Migrate to GitHub
, related to the Mercurial-to-Git migration, with JDK 16 source code repositories to be on the popular code-sharing site. JDK feature releases and JDK update releases for Java 11 and later would be part of this plan.
The transition to Git, GitHub, and Skara for the Mercurial JDK and JDK-sandbox was done on September 5 and is open for contributions.
16.4 Concurrent Thread-Stack Processing
Moving ZGC (Z Garbage Collector) thread-stack processing from safepoints to a concurrent phase. The goals of this plan include removing thread-stack processing from ZGC safepoint.
making stack processing lazy, cooperative, concurrent, and incremental; removing all other per-thread root processing from ZGC safepoints; and providing a mechanism for other HotSpot VM subsystems to lazily process stacks.
ZGC is intended to make GC pauses and scalability issues in HotSpot a thing of the past. So far, GC operations that scale with the size of the heap and the size of metaspace have been moved out of safepoint operations and into concurrent phases.
These have included marking, relocation, reference processing, class unloading, and most root processing. The only activities still done in GC safepoints are a subset of root processing and a time-bounded marking termination operation.
These roots have included Java thread stacks and other thread roots, with these roots being problematic because they scale with the number of threads. To move beyond the current situation, per-thread processing, including stack scanning, must be moved to a concurrent phase.
With this plan, the throughput cost of the improved latency should be insignificant and the time spent inside ZGC safepoints on typical machines should be less than one millisecond.
16.5 +Alpine Linux Port - Elastic Metaspace
Porting of the JDK to Alpine Linux and to other Linux distributions that use musl as their primary C library, on x64 and AArch64 architectures.
Musl is a Linux implementation of the standard library functionality described in the ISO C and Posix standards. Alpine Linux is widely adopted in cloud deployments, microservices, and container environments due to its small image size.
A Docker image for Linux is smaller than 6MB. Letting Java run out-of-the-box in such settings will allow Tomcat, Jetty, Spring, and other popular frameworks to work in these environments natively. By using jlink to reduce the size of the Java runtime, a user can create an even smaller image tailored to run a specific application.
16.6 Windows/AArch64 Port
Porting the JDK to the Windows/AArch64 platform. With the release of new server-class and consumer AArch64 (ARM64) hardware, Windows/AArch64 has become an important platform due to demand.
While the porting itself is already mostly complete, the focus of this proposal involves the integration of the port into the mainline JDK repository.
16.7 Invoke Default Methods From Proxy Instance
As an enhancement to the default method in Interfaces, with the release of Java 16, support has been added to java.lang.reflect.InvocationHandler invoke default methods of an interface via a dynamic proxy using reflection.
To illustrate this, let's look at a simple default method example:
interface HelloWorld { default String hello() { return "world"; } }
With this enhancement, we can invoke the default method on a proxy of that interface using reflection :
Object proxy = Proxy.newProxyInstance(getSystemClassLoader(), new Class<?>[] { HelloWorld.class }, (prox, method, args) -> { if (method.isDefault()) { return InvocationHandler.invokeDefault(prox, method, args); } // ... } ); Method method = proxy.getClass().getMethod("hello"); assertThat(method.invoke(proxy)).isEqualTo("world");
16.8. Day Period Suppor
LocalTime date = LocalTime.parse("15:25:08.690791"); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("h B"); assertThat(date.format(formatter)).isEqualTo("3 in the afternoon");
Instead of something like “3pm“, we get an output of “3 in the afternoon“. We can also use the “B“, “BBBB“, or “BBBBB” DateTimeFormatter pattern for short, full, and narrow styles respectively.
The aim is to reduce the boilerplate with some commonly used Stream collectors, such as Collectors.toList and Collectors.toSet:
List<String> integersAsString = Arrays.asList("1", "2", "3"); List<Integer> ints = integersAsString.stream().map(Integer::parseInt).collect(Collectors.toList()); List<Integer> intsEquivalent = integersAsString.stream().map(Integer::parseInt).toList();
Our ints example works the old way, but the intsEquivalent has the same result and is more concise.
16.9 Records
Records are similar to enums in the fact that they are a restricted form of class. Defining a record is a concise way of defining an immutable data holding object.
16.9.1. Example Without Records
First, let's define a Book class:
public final class Book { private final String title; private final String author; private final String isbn; public Book(String title, String author, String isbn) { this.title = title; this.author = author; this.isbn = isbn; } public String getTitle() { return title; } public String getAuthor() { return author; } public String getIsbn() { return isbn; } @Override public boolean equals(Object o) { // ... } @Override public int hashCode() { return Objects.hash(title, author, isbn); } }
Creating simple data holding classes in Java requires a lot of boilerplate code. This can be cumbersome and lead to bugs where developers don't provide all the necessary methods, such as equals and hashCode.
Similarly, sometimes developers skip the necessary steps for creating proper immutable classes. Sometimes we end up reusing a general-purpose class rather than defining a specialist one for each different use case.
Most modern IDEs provide an ability to auto-generate code (such as setters, getters, constructors, etc.) that helps mitigate these issues and reduces the overhead on a developer writing the code. However, Records provide an inbuilt mechanism to reduce the boilerplate code and create the same result.
16.9.2. Example with Records :
public record Book(String title, String author, String isbn) { }
By using the record keyword, we have reduced the Book class to two lines. This makes it a lot easier and less error-prone.
16.9.3. New Additions to Records in Java 16
With the release of Java 16, we can now define records as class members of inner classes. This is due to relaxing restrictions that were missed as part of the incremental release of Java 15 under JEP-384:
class OuterClass { class InnerClass { Book book = new Book("Title", "author", "isbn"); } }
16.10 Pattern Matching for instanceof
Pattern matching for the instanceof keyword has been added as of Java 16.
Previously we might write code like this::
Object obj = "TEST"; if (obj instanceof String) { String t = (String) obj; // do some logic... }
Instead of purely focusing on the logic needed for the application, this code must first check the instance of obj, then cast the object to a String and assign it to a new variable t.
With the introduction of pattern matching, we can re-write this code:
Object obj = "TEST"; if (obj instanceof String t) { // do some logic }
We can now declare a variable – in this instance t – as part of the instanceof check.
16.11. Sealed Classes
Sealed classes, first introduced in Java 15, provide a mechanism to
determine which sub-classes are allowed to extend or implement a parent
class or interface
8.1 Example
Let's illustrate this by defining an interface and two implementing classes:
public sealed interface JungleAnimal permits Monkey, Snake { } public final class Monkey implements JungleAnimal { } public non-sealed class Snake implements JungleAnimal { }
The sealed keyword is used in conjunction with the permits keyword to determine exactly which classes are allowed to implement this interface. In our example, this is Monkey and Snake.
All inheriting classes of a sealed class must be marked with one of the following:
- sealed – meaning they must define what classes are permitted to inherit from it using the permits keyword.
- final – preventing any further subclasses
- non-sealed – allowing any class to be able to inherit from it.
A significant benefit of sealed classes is that they allow for exhaustive pattern matching checking without the need for a catch for all non-covered cases. For example, using our defined classes, we can have logic to cover all possible subclasses of JungleAnimal:
JungleAnimal j = // some JungleAnimal instance if (j instanceof Monkey m) { // do logic } else if (j instanceof Snake s) { // do logic }
We don't need an else block as the sealed classes only allow the two possible subtypes of Monkey and Snake.
16.12 New Additions to Sealed Classes in Java 16
There are a few additions to sealed classes in Java 16. These are the changes that Java 16 introduces to the sealed class:
- The Java language recognizes sealed, non-sealed, and permits as contextual keywords (similar to abstract and extends)
- Restrict the ability to create local classes that are subclasses of a sealed class (similar to the inability to create anonymous classes of sealed classes).
-
Stricter checks when casting sealed classes and classes derived from
sealed classes
Continuing from JEP-383 in the Java 15 release, the foreign linker API provides a flexible way to access native code on the host machine. Initially, for C language interoperability, in the future, it may be adaptable to other languages such as C++ or Fortran. The goal of this feature is to eventually replace the Java Native Interface.
Another important change is that JDK internals is now strongly encapsulated by default. These have been accessible since Java 9. However, now the JVM requires the argument –illegal-access=permit. This will affect all libraries and apps (particularly when it comes to testing) that are currently using JDK internals directly and simply ignoring the warning messages.
Conclusion
In this article, we covered some of the features and changes introduced as part of the incremental Java 16 release. The complete list of changes in Java 16 is in the JDK release notes.