What’s new in Java 17

A closer look at the latest innovations and upgrades

Yash Fofdiya
Simform Engineering
8 min readAug 22, 2023

--

What’s new in Java 17

Java, launched by Sun Microsystems in 1995 (acquired by Oracle in 2010), is a widely used high-level programming language. Known for platform independence and robustness, it’s favored for various applications.

The Java Virtual Machine (JVM) enables the “Write Once, Run Anywhere” (WORA) capability, allowing code to execute on any platform with a suitable JVM. Notably, it powers Android app development. Java boasts a simple syntax, extensive libraries, and various paradigms. Regular upgrades enhance features, speed, and security. With a thriving community, Java remains enduring and versatile.

In this article, we will explore Java 17’s features, released on September 14, 2021, aimed at reducing complexity and improving readability, security, and performance.

Java 17’s New Features List

  1. Sealed Classes and Interfaces
  2. Pattern Matching for Switch
  3. Record Classes
  4. Foreign Function and Memory API in Java
  5. Vector API
  6. Restore or Rebuild the “Always-Strict Floating-Point” Semantics
  7. Enhanced Pseudo-Random Number Generators
  8. Add java.time.InstantSource
  9. Hex Formatting and Parsing Utility
  10. Deserialization Filters
  11. Deprecations and Deletions

1. Sealed Classes and Interfaces

Sealed classes and interfaces restrict other classes by extending and implementing them.

1.1 Sealed Classes

a. Sealed classes are declared using sealed modifiers and permits clauses. The permits clause is useful to specify the child classes that are allowed to extend the super sealed class.

sealed class SealedClass permits PermittedClass1, PermittedClass2 {
// Sealed Class
}

b. Permitted sub-classes must be in the same package or in the same module where sealed classes are declared.

package java17.learning

sealed class SealedClass permits PermittedClass1, PermittedClass2 {
// Sealed Class
}
final class PermittedClass1 extends SealedClass {
// PermittedClass Sub Class
}
final class PermittedClass2 extends SealedClass {
// PermittedClass Sub Class
}

c. Permitted sub-classes must be either final or non-sealed or sealed. Otherwise, you will get compile time error.

sealed class SealedClass permits PermittedClass1, PermittedClass2, PermittedClass3 {
// Sealed Class
}
final class PermittedClass1 extends SealedClass {
// PermittedClass Sub Class
}
non-sealed class PermittedClass2 extends SealedClass {
// PermittedClass Sub Class
}
sealed class PermittedClass3 extends SealedClass {
// PermittedClass Sub Class
}

d. Permitted sub-classes declared as final cannot be extended.

sealed class SealedClass permits PermittedClass1 {
// Sealed Class
}
final class PermittedClass1 extends SealedClass {
// PermittedClass Sub Class
}
class AnotherClass extends PermittedClass1 {
// Compile Time Error
}

e. Sealed permitted sub-classes can be extended by only permitted sub-classes.

sealed class SealedClass permits PermittedClass1, PermittedClass2 {
// Sealed Class
}
sealed class PermittedClass1 extends SealedClass {
// PermittedClass Sub Class
}
non-sealed class PermittedClass2 extends PermittedClass1 {
// Permitted Sub Class extends Permitted Sub Class
}

f. Non-sealed permitted sub-classes can be extended by anyone.

sealed class SealedClass permits PermittedClass1, PermittedClass2 {
// Sealed Class
}
non-sealed class PermittedClass1 extends SealedClass {
// PermittedClass Sub Class
}
class AnotherClass extends PermittedClass1 {
// Another class extends non-sealed permitted class
}

g. Without the permits clause, you will get compile time error.

sealed class SealedClass {
// Compile Time Error
}

h. Permitted sub-classes must extend their sealed superclass.

sealed class SealedClass permits PermittedClass1, PermittedClass2 {
// Sealed Class
}
final class PermittedClass1 {
// Compile Time Error
}
final class PermittedClass2 {
// Compile Time Error
}

1.2 Sealed Interfaces

a. Sealed interfaces can be declared with permitted sub-interfaces or sub-classes.

sealed interface SealedInterface permits PermittedInterface, PermittedClass { 
// Sealed Interface
}

b. Permitted subinterfaces can be either sealed or non-sealed but not final.

sealed interface SealedInterface permits PermittedInterfaceOne, PermittedInterfaceTwo, PermittedInterfaceThree { 
// Sealed Interface
}
sealed interface PermittedInterfaceOne extends SealedInterface {
// Permitted Sealed Interface
}
non-sealed interface PermittedInterfaceTwo extends SealedInterface {
// Permitted Non-sealed Interface
}
final interface PermittedInterfaceThree extends SealedInterface {
// Compile Time Error
}

While declaring sealed classes and sealed interfaces, permits clause is mandatory after extends and implements keywords. Failure to include it will result in a compile-time error.

With the addition of sealed classes and interfaces in Java 17, two methods have been added to the java.lang.Class (Reflection API):
1. getPermittedSubClasses()
2. isSealed()

2. Pattern Matching for Switch

Pattern matching for switches is introduced as a preview feature in Java 17. It provides more flexibility when defining conditions for switch cases.
It is still a preview feature, so we need to enable it using the below command.

java - enable-preview - source 17 PatternMatching.java

2.1 Type Pattern

It is used to convert one type of object to another required type.

Here is an example of a Type pattern using an if-else statement.

static int getIntegerUsingIf(Object o) {
int result;
if (o instanceof Double) {
result = ((Double) o).intValue();
} else if (o instanceof Float) {
result = ((Float) o).intValue();
} else if (o instanceof String) {
result = Integer.parseInt(((String) o));
} else {
result = 0;
}
return result;
}

Now same example with the switch statement using pattern matching.

static int getIntegerUsingSwitch(Object o) {
return switch (o) {
case Double d -> d.intValue();
case Float f -> f.intValue();
case String s -> Integer.parseInt(s);
default -> 0;
};
}

2.2 Guarded Pattern

Type patterns help us transfer control based on type. But if we need to add condition along with type, we should go with a guarded pattern.

static int getIntegerValueUsingGuardedPatterns(Object o) {
return switch (o) {
case String s && s.length() > 0 -> Integer.parseInt(s);
default -> 0;
};
}

2.3 Parenthesized Pattern

We can group conditional logic using parenthesized patterns. When performing additional checks, we can simply use parentheses in boolean expressions.

static int getIntegerValueUsingParenthesizedPatterns(Object o) {
return switch (o) {
case String s && s.length() > 0 && !(s.contains("#") || s.contains("@")) -> Integer.parseInt(s);
default -> 0;
};
}

2.4 Rules while using Switch Pattern Matching

While using pattern matching in switch, the Java compile will check the type coverage.

a. Switch condition accepting any object but covering only the String.

static int getIntegerUsingSwitch(Object o) {
return switch (o) {
case String s -> Integer.parseInt(s);
};
}

Compile Time Error as switch expression does not cover all possible input values

Switch case labels are required to include the type of the selector expression.

b. With pattern matching in the switch, the order of the cases matters.

static int getIntegerUsingSwitch(Object o) {
return switch (o) {
case CharSequence c -> Integer.parseInt(c.toString());
case String s -> Integer.parseInt(s);
default -> 0d;
};
}

Compile Time Error as Label is dominated by a preceding case label ‘CharSequence c’.

Order is important, as here in the above example, the second case will never get a chance to execute.

c. Handling null values with separate case labels.

static int getIntegerUsingSwitch(Object o) {
return switch (o) {
case String s -> Integer.parseInt(s);
case null -> 0;
default -> 0;
};
}

d. If there is no case label specific to null, passing null will result in a NullPointerException.

3. Record Classes

Record classes are valuable for storing immutable objects, effectively serving as data transfer entities.

class Student {
private final int id;
private final String name;
Student(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

With the help of record class, we can write Student class in a single line.

record Student (int id, String name) {}

The record class should have all parameters set as final by default. It will exclusively feature a parameterized constructor, necessitating the creation of a default constructor.

4. Foreign Function and Memory API

Foreign Function Integer (FFI) and Memory API offer Java developers tools to interact seamlessly with code in native languages like C or C++, as well as enable safe and efficient memory interaction.

The process involves several steps:

a. Setting Up the Native Library: Compile the C program into a shared library.
b. Declaring the Native Function: Define the method using the ‘native’ keyword.
c. Loading the Native Library: Utilize the LibraryLoader.load() method to load the library.
d. Compiling and Running the Java Program: Compile the Java code using Javac and run it using Java. Place the native library file in the same directory or specify the path using the java.library.path system property.
e. Linking with Native Code: When invoking the method call, the Java program accesses the native function from C.

Calculations occur within the native function, and the result is returned to the Java program.

import jdk.incubator.foreign.*;
public class MathLibrary {
@CFunction(
library = "mathlib",
name = "calculateSquare",
cdefine = "int calculateSquare(int)"
)
private static native int calculateSquare(int value);
public static void main(String[] args) {
int result = calculateSquare(5);
System.out.println("Square: " + result);
}
static {
LibraryLoader.load();
}
}

5. Vector API

The Single Instruction, Multiple Data (SIMD) procedure, which involves multiple sets of instructions being executed concurrently, is what the Vector API works with. This API leverages specialized CPU hardware, facilitating the execution of instructions such as pipelines and supporting vector instructions.

By utilizing the capability of the underlying hardware, this new API empowers developers to write more effective code. It proves particularly beneficial for applications requiring the application of an operation to numerous independent operands, including image processing, character processing, intensive arithmetic tasks, and linear applications of scientific algebra.

6. Restore or Rebuild the “Always-Strict Floating-Point” Semantics

From Java 17, a strictfp modifier is not required in complex calculations.

public static void main(String[] args) {
double result = sum(4e11, 5e13);
System.out.printf("Result: [%f].", result);
}
public static strictfp double sum(double a, double b) {
return a + b;
}

We will get a warning as Modifier ‘strictfp’ is redundant on Java 17 and later.

7. Enhanced Pseudo-Random Number Generators

Java 17 adds one new interface to generate Pseudorandom Number Generators (PRNG) algorithms.

RandomGenerator randomGenerator = RandomGeneratorFactory.of("L64Y126StarRandom").create();

RandomGenerator and RandomGeneratorFactory are part of java.util package.

8. Add java.time.InstantSource

The java.time.Clock class provides the getZone() method, which specifies a time zone to instantiate a Clock object.

To enhance flexibility, the java.time.InstantSource interface introduces time sources without timezone constraints. In Java 17, InstantSource has been separated from Clock.

From Clock in Java 17, InstantSource was extracted. This new interface provides the methods immediate() and millis() for requesting time.

9. Hex Formatting and Parsing Utility

Previously, we could use String.format() or the toHexString() function of the Integer, Long, Float, and Double classes to print hexadecimal values.

However, Java 17 introduces a new class, java.util.HexFormat. This new utility facilitates the rendering and parsing of hexadecimal numbers through a unified API. It accommodates byte arrays and various primitive data types (int, byte, char, long, short), though it does not encompass floating-point numbers.

HexFormat hexFormat = HexFormat.of();
System.out.println(hexFormat.toHexDigits('B'));
System.out.println(hexFormat.toHexDigits((byte) 11));
System.out.println(hexFormat.toHexDigits((short) 1_100));
System.out.println(hexFormat.toHexDigits(1_000_111));
System.out.println(hexFormat.toHexDigits(100_111_000_000L));
System.out.println(hexFormat.formatHex(new byte[] {4, 5, 61, 127, -5}));

Output:
0b
044c
000f42af
000000174f14a1c0
04053d7ffb

10. Deserialization Filters

We can set serialization filters to get around Java’s deserialization problems, a feature introduced in Java 9. These filters allow setting constraints on the number of streams, graph depth, and total references in an array. Filters can be configured globally by the JVM or tailored to individual streams. However, a challenge arises when a stream-specific filter overrides the global filter.

Java 17 brings a solution by introducing the SerialFilterFactory to ObjectInputFilter.Config. This factory, functioning as a BinaryOperator, defines actions when applying a filter to a specific stream. The new filter can now be seamlessly merged with an existing one using the default merge method.

ObjectInputFilter.Config.setSerialFilterFactory((filter1, filter2) -> ObjectInputFilter.merge(filter2,filter1));

11. Deprecations and Deletions

Java 17 introduces the deprecation and removal of several APIs and features, including the Applet API, Security Manager, RMI Activation, AOT, and JIT compiler.

Conclusion

Java 17 offers heightened security and enhanced functionality, making it an attractive choice for new projects. As an LTS release, it promises long-term support. However, upgrading existing projects requires careful consideration. Factors include compatibility, deployment feasibility, and associated costs. While technology evolves, timing matters.

If your project aligns without compatibility concerns or imminent limitations, upgrading to Java 17 seems beneficial. Evaluate your unique context before making the leap, ensuring a future-proof solution amidst the ever-changing tech landscape.

Stay updated on the latest insights with the Simform Engineering blog.

For more updates, connect with us on Twitter and LinkedIn

--

--