Java 17 Features with Detailed Explanation


Java 17 was released on September 14, 2021, and it includes several new features and improvements that developers can use to build better and more efficient applications. In this tutorial, we’ll take a closer look at some of the most important features of Java 17 and how to use them in your projects.

In this tutorial, we’ll cover the following features:

  1. Sealed Classes and Interfaces
  2. Pattern Matching for instanceof
  3. Records
  4. Text Blocks
  5. Switch Expressions
  6. Helpful NullPointerExceptions
  7. Foreign-Memory Access API (Incubator)
  8. Vector API (Incubator)
  9. Enhanced Pseudo-Random Number Generators
  10. Enhanced NUMA-Aware Memory Allocation for G1

1. Sealed Classes and Interfaces:

Sealed Classes and Interfaces, a new language feature that allows developers to restrict the inheritance hierarchy of a class or interface. Sealed classes and interfaces provide greater control over how classes and interfaces can be extended, improving the design of object-oriented systems and making them more secure and maintainable.

Sealed classes and interfaces are defined using the sealed keyword, which restricts the set of classes or interfaces that can extend or implement the sealed class or interface. This restricts the inheritance hierarchy, preventing unauthorized subclasses or interfaces from being created.

The syntax for defining a sealed class or interface is as follows:

public sealed class MyClass permits SubClass1, SubClass2, ... {
    // class definition
}

In this example, the sealed keyword is used to define the class MyClass as a sealed class, and the permits keyword is used to list the permitted subclasses SubClass1, SubClass2, and so on. This restricts the set of classes that can extend MyClass to the specified subclasses.

The same syntax applies to sealed interfaces, as shown in the following example:

public sealed interface MyInterface permits SubInterface1, SubInterface2, … {<br>// interface definition<br>}

In this example, the sealed keyword is used to define the interface MyInterface as a sealed interface, and the permits keyword is used to list the permitted subinterfaces SubInterface1, SubInterface2, and so on. This restricts the set of interfaces that can extend MyInterface to the specified subinterfaces.

Sealed classes and interfaces provide several benefits, including:

  • Improved design: Sealed classes and interfaces provide greater control over the inheritance hierarchy, improving the overall design of the system and making it easier to reason about.
  • Security: Sealed classes and interfaces prevent unauthorized subclasses or interfaces from being created, reducing the risk of security vulnerabilities.
  • Maintainability: Sealed classes and interfaces make it easier to maintain the system over time, as changes to the inheritance hierarchy can be made more safely and with greater confidence.

In summary, sealed classes and interfaces are a new language feature in Java 17 that allow developers to restrict the inheritance hierarchy of a class or interface. By providing greater control over the inheritance hierarchy, sealed classes and interfaces improve the design of object-oriented systems and make them more secure and maintainable.

2. Pattern Matching for instanceof

Pattern matching for instanceof is a new language feature in Java 17 that allows developers to write more concise and expressive code when checking the type of an object. With pattern matching for instanceof, developers can combine a type check with a type cast into a single expression, making the code more readable and less error-prone.

Prior to Java 17, developers would typically use an if statement to check the type of an object and then cast it to the appropriate type. For example:

if (myObject instanceof MyClass) {
    MyClass myClass = (MyClass) myObject;
    // use myClass
}

With pattern matching for instanceof, the above code can be simplified into a single expression:

if (myObject instanceof MyClass myClass) {<br>// use myClass<br>}

In this example, the type check and the cast are combined into a single expression. If myObject is an instance of MyClass, it will be cast to MyClass and assigned to the new variable myClass, which can be used within the if block.

Pattern matching for instanceof also supports the use of the else keyword to specify a default branch, as shown in the following example:

if (myObject instanceof MyClass myClass) {
    // use myClass
} else {
    // handle other types
}

In this example, if myObject is not an instance of MyClass, the code in the else block will be executed instead.

Pattern matching for instanceof provides several benefits, including:

  • Concise and expressive code: Pattern matching for instanceof allows developers to write more concise and expressive code, making it easier to read and understand.
  • Fewer errors: By combining the type check and the cast into a single expression, pattern matching for instanceof reduces the risk of errors that can arise from separate type checks and casts.
  • Improved performance: Pattern matching for instanceof can improve performance by reducing the number of unnecessary casts.

In summary, pattern matching for instanceof is a new language feature in Java 17 that allows developers to write more concise and expressive code when checking the type of an object. By combining the type check and the cast into a single expression, pattern matching for instanceof reduces the risk of errors and improves performance.

3. Records

Records is a new feature introduced in Java 16 and finalized in Java 17 that provides a concise and immutable way to declare classes whose main purpose is to hold data. Records are essentially classes that are designed to store data rather than represent objects with behavior.

In Java, classes are typically created to represent objects that have both data and behavior. However, sometimes we need to create classes that are only used to hold data without any additional behavior. In such cases, creating a traditional class with fields, getters, setters, equals, hashCode, and toString methods can be quite verbose and repetitive.

With records, the syntax is much simpler and more concise. A record is defined using the record keyword, followed by the class name, and then a list of properties within parentheses. Here’s an example of a record definition:

public record Person(String name, int age) {}

In this example, we’ve created a record called Person with two properties: name of type String and age of type int. Note that we didn’t need to explicitly declare constructors, getters, setters, or other methods, because they are automatically generated by the compiler.

With records, you can also add additional methods, such as custom constructors or instance methods. Here’s an example:

public record Person(String name, int age) {
    public Person {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
    }
    
    public String getName() {
        return name.toUpperCase();
    }
}

In this example, we’ve added a custom constructor that checks if the age is negative, and an instance method that returns the uppercase name.

Records also provide a compact and readable way to override the default equals, hashCode, and toString methods. For example, the following record definition:

public record Person(String name, int age) {
    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

overrides the default toString method to return a string representation of the Person record.

In summary, records are a new feature in Java 16/17 that provide a concise and immutable way to declare classes whose main purpose is to hold data. They simplify the creation of classes that are only used to hold data without any additional behavior, and provide automatic generation of constructors, getters, setters, equals, hashCode, and toString methods. With records, you can also add additional methods and override default methods in a compact and readable way.

4. Text Blocks

Text blocks provide a more readable way to declare multi-line strings in Java 17. Text blocks can contain line breaks and other whitespace characters without requiring special escape sequences.

String html = """
    <html>
        <head>
            <title>Hello, world!</title>
        </head>
        <body>
            <h1>Hello, world!</h1>
        </body>
    </html>
""";

In this example, the html string contains an HTML document declared using a text block. The text block starts with """ and ends with """, and the document is indented for readability.

Here’s an example that demonstrates how to use placeholders and expressions inside text blocks:

String name = "Alice";
int age = 30;
String message = """
                 Hello, ${name}!
                 
                 You are ${age} years old.
                 
                 Your age in dog years is ${age * 7}.
                 """;
System.out.println(message);

In this example, we define two variables (name and age) and use them inside a text block to create a message. The ${expression} syntax is used to include the values of the variables inside the message, and we also include an expression (age * 7) to calculate the age in dog years.

Text blocks can also be used with other features in Java, such as switch expressions and lambda expressions. For example, you can use a text block inside a switch expression to define a case label:

String day = "Monday";
String message = switch (day) {
    case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" -> """
                                                                   It's a weekday.
                                                                   
                                                                   Time to go to work.
                                                                   """;
    case "Saturday", "Sunday" -> """
                                 It's the weekend.
                                 
                                 Time to relax and have fun!
                                 """;
    default -> """
               Invalid day.
               
               Please enter a valid day of the week.
               """;
};
System.out.println(message);

In this example, we use a text block to define the message for each case label in the switch expression. This makes the code easier to read and maintain, and reduces the amount of boilerplate code that is required.

Overall, text blocks are a useful feature that can make Java code more concise and readable, especially in cases where you need to write multiline strings or include formatting whitespace.

5. Switch Expressions

Switch expressions are a new feature introduced in Java 17 that provide a more concise and expressive syntax for switch statements. Switch statements are commonly used in Java to evaluate a single value and perform different actions based on different cases. Prior to Java 17, switch statements could only be used to execute a block of code, but with switch expressions, you can now assign the result of the switch statement to a variable.

The syntax for switch expressions is similar to the syntax for switch statements, with a few differences. In switch expressions, the cases are defined using the -> operator instead of the : operator, and the switch expression returns a value instead of executing a block of code.

Here’s an example that demonstrates how to use switch expressions in Java 17:

String day = "Monday";
String result = switch (day) {
    case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" -> "Weekday";
    case "Saturday", "Sunday" -> "Weekend";
    default -> "Invalid day";
};
System.out.println(result); // Output: Weekday

In this example, we first define a string variable day with the value “Monday”. We then use a switch expression to evaluate the value of day and assign the result to a string variable called result. The switch expression has two cases: one for weekdays and one for weekends. If the value of day matches one of the weekdays, the switch expression will return the string “Weekday”, and if it matches one of the weekends, it will return the string “Weekend”. If day does not match any of the defined cases, the switch expression will return the string “Invalid day”.

One of the benefits of switch expressions is that they can make code more concise and easier to read. They can also reduce the amount of code you need to write in some cases. For example, consider the following code snippet that uses a switch statement to perform an action based on the value of a variable:

int value = 10;
switch (value) {
    case 1:
        System.out.println("One");
        break;
    case 2:
        System.out.println("Two");
        break;
    case 3:
        System.out.println("Three");
        break;
    default:
        System.out.println("Unknown");
        break;
}

With switch expressions, you can write the same code in a more concise way:

int value = 10;
String result = switch (value) {
    case 1 -> "One";
    case 2 -> "Two";
    case 3 -> "Three";
    default -> "Unknown";
};
System.out.println(result); // Output: Unknown

Switch expressions can be especially useful in situations where you need to perform a switch statement and assign the result to a variable, or when you need to perform complex operations based on the value of a variable.

6. Helpful NullPointerExceptions

Helpful NullPointerExceptions aims to provide more detailed information about null pointer exceptions (NPEs) at runtime. The goal of this feature is to make it easier for developers to identify the source of null pointer exceptions and fix them more quickly.

In previous versions of Java, when a null pointer exception occurred, the error message provided limited information about where the exception occurred and which variable was null. This made it difficult for developers to debug their code and find the root cause of the problem.

With the new Helpful NullPointerExceptions feature, the error message now includes additional details that can help developers identify the source of the problem. For example, the error message might now include information about the method or line number where the exception occurred, as well as the name of the variable that was null.

Here’s an example of how the error message for a null pointer exception might look with the Helpful NullPointerExceptions feature enabled:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "s" is null
	at com.example.MyClass.myMethod(MyClass.java:10)

In this example, the error message includes the name of the method (myMethod) where the exception occurred, as well as the line number (10) and the name of the variable that was null (s).

To enable the Helpful NullPointerExceptions feature, you can use the -XX:+ShowCodeDetailsInExceptionMessages option when running your Java application. This option is only available in JDK 17 and later versions.

Overall, the Helpful NullPointerExceptions feature is a useful addition to Java that can make it easier for developers to debug their code and find and fix null pointer exceptions more quickly. By providing more detailed error messages, developers can spend less time searching for the source of the problem and more time fixing it.

7. Foreign-Memory Access API (Incubator)

Foreign-Memory Access API, which provides a way for Java developers to directly access and manipulate memory outside of the Java heap. This API is designed for use cases where high-performance access to memory is required, such as in graphics processing, machine learning, and database systems.

The Foreign-Memory Access API allows developers to create and manage direct buffers that are backed by native memory. These buffers can be used to read and write data directly to and from the memory, without going through the Java heap. This can significantly improve the performance of memory-intensive operations, as it avoids the overhead of copying data between the Java heap and native memory.

To use the Foreign-Memory Access API, you first need to create a memory segment that represents the native memory. This can be done using the MemorySegment class, which provides methods for allocating, deallocating, and accessing memory segments. Once you have a memory segment, you can create a direct buffer that is backed by the segment using the MemorySegment.asByteBuffer() method. This buffer can be used to read and write data to and from the memory segment, as you would with any other byte buffer.

Here’s an example of how to use the Foreign-Memory Access API to allocate a memory segment and create a direct buffer:

import jdk.incubator.foreign.*;
public class MemoryExample {
    public static void main(String[] args) {
        // Allocate a memory segment of 1024 bytes
        MemorySegment segment = MemorySegment.allocateNative(1024);
        // Create a direct buffer backed by the memory segment
        ByteBuffer buffer = segment.asByteBuffer();
        // Write some data to the buffer
        buffer.putInt(0, 123);
        buffer.putDouble(4, 3.14);
        // Read the data back from the buffer
        int i = buffer.getInt(0);
        double d = buffer.getDouble(4);
        // Print the values
        System.out.println("i = " + i);
        System.out.println("d = " + d);
        // Deallocate the memory segment
        segment.close();
    }
}

In this example, we first allocate a memory segment of 1024 bytes using the MemorySegment.allocateNative() method. We then create a direct buffer backed by the memory segment using the MemorySegment.asByteBuffer() method. We write some data to the buffer using the putInt() and putDouble() methods, and then read the data back using the getInt() and getDouble() methods. Finally, we deallocate the memory segment using the close() method.

Note that the Foreign-Memory Access API is an incubating feature in Java 17, which means that it is still under development and subject to change in future releases. It should only be used in production environments with caution and after thorough testing.

8. Vector API (Incubator)

Vector API provides a set of vectorized operations for working with SIMD (Single Instruction Multiple Data) instructions on modern CPU architectures. This API is designed for use cases where high-performance processing of large data sets is required, such as in scientific computing, machine learning, and graphics processing.

The Vector API allows developers to perform arithmetic and logical operations on vectors of data in a way that takes advantage of SIMD instructions, which can perform multiple calculations in parallel. This can significantly improve the performance of certain types of computations, as it reduces the number of instructions that need to be executed and maximizes the use of available CPU resources.

To use the Vector API, you first need to create a vector using one of the factory methods provided by the API. These factory methods create vectors of a specific type (such as IntVector or FloatVector) and with a specific size (such as 128 bits or 256 bits). Once you have a vector, you can perform various operations on it, such as addition, subtraction, multiplication, and comparison.

Here’s an example of how to use the Vector API to perform a vectorized addition operation:

In this example, we first create two vectors of four floats each using the FloatVector.fromArray() method. We then add the two vectors together using the add() method and store the result in a third vector. Finally, we print the result.

import jdk.incubator.vector.*;
public class VectorExample {
    public static void main(String[] args) {
        // Create two vectors of four floats each
        FloatVector a = FloatVector.fromArray(VectorSpecies_128.F_128, new float[]{1, 2, 3, 4});
        FloatVector b = FloatVector.fromArray(VectorSpecies_128.F_128, new float[]{5, 6, 7, 8});
        // Add the two vectors together
        FloatVector c = a.add(b);
        // Print the result
        System.out.println("c = " + c);
    }
}

Note that the Vector API is an incubating feature in Java 17, which means that it is still under development and subject to change in future releases. It should only be used in production environments with caution and after thorough testing. Additionally, the Vector API requires hardware support for SIMD instructions, which may not be available on all systems.

9. Enhanced Pseudo-Random Number Generators

Java 17 introduces enhancements to the existing Pseudo-Random Number Generators (PRNG) in the java.util.random package. These enhancements provide developers with more flexibility and control over the generation of random numbers, as well as improved security.

The enhancements include three new algorithms, new methods for generating random bytes and random integers, and improvements to the existing SplittableRandom class.

New PRNG Algorithms

Java 17 introduces three new PRNG algorithms:

  • LXM
  • PCG64
  • Xoshiro

These algorithms provide different trade-offs between performance and randomness, and allow developers to choose the one that best fits their specific use case.

New Methods for Generating Random Bytes and Integers

Java 17 also introduces new methods in the java.util.random package for generating random bytes and random integers. These methods include:

  • RandomGenerator.nextInt(int bound) and RandomGenerator.nextLong(long bound): These methods generate random integers and longs respectively within the specified range.
  • RandomGenerator.nextBytes(byte[] bytes): This method generates random bytes and fills them into the specified array.

These new methods provide more convenience and flexibility to developers, making it easier to generate random numbers with specific characteristics.

Improvements to SplittableRandom

Java 17 also introduces improvements to the SplittableRandom class, which provides a way to generate repeatable sequences of random numbers. The improvements include:

  • A new split() method that returns a new instance of the SplittableRandom class with a different seed, allowing for the generation of independent sequences of random numbers.
  • Improved performance for generating large numbers of random numbers in parallel.

These improvements make the SplittableRandom class more useful for applications that require large amounts of random data, such as Monte Carlo simulations and statistical analysis.

The enhancements to the Pseudo-Random Number Generators in Java 17 provide developers with more flexibility and control over the generation of random numbers, as well as improved security. With the introduction of new algorithms and methods, and improvements to the SplittableRandom class, Java 17 makes it easier to generate random numbers with specific characteristics, and to generate large amounts of random data efficiently.

10. Enhanced NUMA-Aware Memory Allocation for G1

Java 17 introduces an enhancement to the Garbage-First Garbage Collector (G1) that improves its ability to allocate memory in a Non-Uniform Memory Access (NUMA) architecture. This enhancement is designed to improve the performance of applications running on NUMA systems, which are increasingly common in modern high-performance computing environments.

In NUMA architectures, memory is distributed across multiple nodes, each with its own local memory and access latency. Applications running on these systems can experience performance degradation if memory allocation is not optimized to take into account the NUMA topology.

The enhanced NUMA-aware memory allocation in G1 improves performance by allocating memory in a way that takes into account the NUMA topology of the system. Specifically, it attempts to allocate memory on the local node whenever possible, reducing the need for remote memory accesses that can result in increased latency and reduced performance.

The enhanced allocation strategy works by first identifying the NUMA topology of the system and then using that information to allocate memory in a way that maximizes locality. The strategy also takes into account the current state of the system, such as the availability of free memory and the current load on each node, to ensure that allocations are made in an efficient and effective manner.

To enable NUMA-aware memory allocation in G1, developers can set the -XX:+UseNUMA flag when running their application. This flag tells the JVM to use the enhanced allocation strategy, which can result in improved performance on NUMA architectures.

In addition to the -XX:+UseNUMA flag, developers can also use the -XX:NumAProximityPolicy flag to control the proximity policy used by G1 when allocating memory. The default policy is compact, which attempts to allocate memory on the closest node first. Other policies, such as scatter and balance, are also available, allowing developers to fine-tune the allocation strategy to meet the specific needs of their application.

In summary, the enhanced NUMA-aware memory allocation in G1 in Java 17 provides a valuable tool for developers working with applications running on NUMA architectures. By taking into account the NUMA topology of the system, G1 can allocate memory in a way that maximizes locality and minimizes remote memory accesses, resulting in improved performance and reduced latency.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.