Originally published in sendilkumarn.com
Few months ago, Java 17 was released. The new LTS version comes with a lot of performance improvements, new features, and API enhancements. Here are my top features in this release.
- Sealed Classes or Interfaces
- Pattern Matching in switch
- Vector API
- Foreign Function and Memory API
Sealed Classes or Interfaces
With Java 17, Sealed classes or interfaces are finally out of preview. They allow classes or interfaces to specify which other classes or interfaces can extend or implement them. This feature enables the superclasses to be accessible but restricts extending superclasses.
You can define a sealed class as follows:
public abstract sealed class CollectionAdapter
permits ListAdapter, SetAdapter, MapAdapter { ... }
The sealed classes have a sealed
modifier followed by the class definition. We have to explicitly define the allowed subclasses for the sealed class. We can do it either by defining the sub classes directly in the same source file (as inner or auxiliary classes) or using permits
modifier followed by a list of allowed subclasses. Only these classes can extend the sealed
classes.
Note: Sealed and its permitted subclasses should live on the same package or module (who uses a module ;) ).
The subclass can extend the sealed class just like any other class. But the subclass should either be final
, sealed
, or non-sealed
(yeah! hyphenated modifier ;)).
final
The final completely prevents further extension of the sealed class.
public final class ListAdapter extends CollectionAdapter { ... }
sealed
The sealed classes allow its subclasses to extend the sealed superclasses via themselves.
public sealed class SetAdapter extends CollectionAdapter
permits SortedSetAdapter, HashSetAdapter { ... }
Now both SortedSetAdapter and HashSetAdapter can (indirectly) extend the CollectionAdapter via SetAdapter.
Note: If SorterSetAdapter and HashSetAdapter directly extend CollectionAdapter then it will error during compilation.
non-sealed
The non-sealed makes unrestricted extension of a superclass possible.
public non-sealed class MapAdapter extends CollectionAdapter { ... }
Record classes & Sealed Interfaces Best Friends Forever
Record classes landed on Java 16. They provide a concise way to represent immutable DTOs in Java. Sealed classes or interfaces work great with records.
public abstract sealed interface Geometry
permits Point, Line { ... }
public record Point(int x, int y) implements Geometry { ... }
public record Line(Point x, Point y) implements Geometry { ... }
Note: Record classes do not support declaring a superclass type as they are implicitly
final
.
Both Record and Sealed classes are algebraic data types (ADT). ADTs are a fancy way of saying composite types (the type formed by combining other types). Record classes are product types. Sealed classes are sum types.
Being sum
type, the Sealed classes make it easy for the compiler to understand its various type forms. Our next feature pattern matching
rides on this exhaustive type checking.
Pattern Matching in switch
This is a preview feature in Java 17
Pattern matching is a great feature. It is one of the many features I enjoyed while learning Rust. The pattern matching makes the programs concise and readable. It also empowers the compiler to do exhaustive type checks. The pattern matching instanceof
was released on Java 16. It reduced the boilerplate code when using instanceof
.
Instead of two separate statements to check the type and create a variable with casting, the pattern matching instanceof
introduced a syntactic sugar that combines them into one.
// before Java 16
if (snow instanceof Water) {
Water w = (Water) snow;
// do whatever with w
}
// from Java 16
if (snow instance of Water w) {
// do the same thing with w
}
Java 17 extends these features further and introduces pattern matching for switch
expressions. Together with exhaustive type check (introduced in Java 14) and pattern matching switch
expressions are delightful to work with.
switch (snow) {
case null -> // well it is a null value
case Water w -> // Do something with Water
case Fire f -> // Do something with Fire
default -> // Do something with something
}
Before Java 17, switch cases were not exhaustive. It introduced runtime exceptions. This exhaustive type checking in switch statement removes the runtime exceptions. When the switch cases are not exhaustive the compiler throws an error. The case
labels should cover all the possible variations of the type. Also, the case expressions support the null
case.
The pattern matching in instanceof
and switch
provides a building block for allowing destructuring of the classes in the future. Let's jump on to two new cool features that are still in incubation but have potential.
Vector API
This feature is in incubation.
In the Machine Learning ecosystem, Java is not the primary language. The main reason is that the language lacks a few essential constructs. Vector APIs addresses a few problems in the Machine Learning area.
Vector is a primitive data structure. In the initial release, vectors support Byte
, Short
, Integer
, Long
types.
// var vectorized = new Vector<E>();
var vectorInt = new Vector<Integer>();
var vectorLong = new Vector<Long>();
var vectorShort = new Vector<Short>>();
var vectorShort = new Vector<Byte>>();
If you are thinking, What is a big deal it is another data structure?
Vector API uses Single Instruction Multiple Data (SIMD). Modern CPU architectures allow you to perform multiple tasks in a single CPU cycle. That is the machine (CPU) allows simultaneous computation of a single instruction. For example, you have 1Million pixels image and you want to increase the contrast for each pixel in that image. In a normal instruction, the CPU has to fetch the pixel from a particular location and increase its RGB (Red Green Blue) value and continue doing that one million times. If the CPU supports SIMD, then the CPU can load a bunch of pixels (say n) and apply the single instruction (in our case increasing RGB values) on them. They do this in parallel for multiple bunches. Thus significantly increasing the performance of the overall operation.
Thus Vector APIs provide an enormous performance boost and thus makes Java easier to use for tensor calculations in the future.
Although most of the modern CPU architectures support SIMD computation, there are still some CPU architectures out there that do not support SIMD. Vector APIs allows graceful degradation for them too.
Foreign Function & Memory API
This feature is in incubation.
Foreign Function and Memory API is another feature on incubation in Java 17. They provide necessary APIs for Java programs to access foreign functions and memory outside of the JVM. Java introduced Java Native Interface(JNI) long back. JNI is complex, slow, and dangerously unsafe to use. The new Foreign Function and Memory API were built from the ground up to provide simple, fast, and safer APIs when going out of the JVM.
Why is it a big deal?
The Java runtime keeps its object in the heap. This heap is called on-heap data
and is fast to access. That is the main reason why Java is super fast. But when Java wants to call outside of its runtime for example C/C++ code, it becomes too slow to access the data. This is primarily because the data is outside the JVM's heap (on-heap), this heap is called off-heap data
. JVM has to translate the value from off-heap
to on-heap
and vice versa. This makes accessing the off-heap
data slower than accessing on-heap
data.
Note: Garbage Collection happens only on the "on-heap" data.
Java provides a couple of ways to call foreign functions or memory, like ByteBuffer
, Unsafe API
, or JNI
. The ByteBuffer API
allows accessing "off-heap" data, but they are not built for this use-case and they lack a few key features (like customised byte alignment). The sun.misc.Unsafe API
, this exposes "on-heap" memory access. While it addresses the performance but as the name implicates it is "unsafe". JNI
allows accessing "off-heap" data, but it provides significantly low performance.
JNI has a ton of performance issues, platform-dependency (because of the foreign language), restrictions on the language to use. The translation of object representation between Java and the foreign language is too costly and uses the "Unsafe" API.
With the new API we can allocate a memory segment using the following code:
// Alloactes 100bytes on the native memory using malloc
var nativeSegment = MemorySegment.allocateNative(100, newImplicitScope());
The Memory Segment is a contiguous region on memory (i.e., they are allocated together). The implicitScope
on the above code specifies that the memory can be cleaned up once the nativeSegment
goes out of scope. Additionally, the API also provides MemoryLayout
to specify how the data is laid out on the memory.
To load the function from C
, we can use the CLinker
. The CLinker
implements the C Application Binary Interface (ABI) and helps us to load the C
file and look up the functions defined in the C file.
// The below line loads the `cFunction` method from the C code.
MethodHandle cFunction = CLinker.getInstance().downcallHandle(
CLinker.systemLookup().lookup("cFunction"), ...);
// Create the required memory segment and provide the layout here.
// ...code is elided...
// The below line invokes the Foreign function from the Java code.
cFunction.invoke(
// address where the memory lives
address,
// length of the memory
usedAddressLength,
// ... other args
)
The Foreign Function API makes the invocation of native code as simple as method handles inside Java. Foreign function and Memory API together will open a lot of new scopes in Java.
The Sealed classes and Record classes provide sum and product types for Java. These algebraic data types are great and they provide a cleaner way to pattern match. The pattern matching provides concise, readable code, and exhaustive type checks by the compiler. These three features make the Java language more modern and alleviate a few of the pain points.
I am particularly excited about the Vector APIs and Foreign Function and Memory APIs changes. This lays a foundation for a much bigger and exciting future for Java. Both these features provide a big boost on performance and will introduce Java into the Machine Learning world (Tensors here we come!). They are focused on performance, efficiency, and safety while not giving up on the readability of Java.
If you are looking for a full feature update on Java 17 then check the following links:
Oracle official release announcement / JEPs since Java 11 / Updates from Java 8 to Java 17 / Java 13 features
Special thanks to Billy Korando for the review.
Top comments (0)