Java Evolution — Part 7

Java 16 (March 16, 2021) and Java 17 (September 14, 2021) completed the delivery of features that had been in preview for several releases. Java 17 is the current LTS release for most organisations and defines the modern Java baseline.


Records — Finalised (Java 16)

Records (JEP 395), previewed in Java 14 and 15, were finalised in Java 16. They are now a stable, permanent part of the language.

Records as Local Classes

Java 16 also allows records to be declared as local classes inside methods, which is useful for temporary data structures within a method:

List<String> topWords(List<String> words, int n) {
record WordCount(String word, long count) {}
return words.stream()
.collect(Collectors.groupingBy(w -> w, Collectors.counting()))
.entrySet().stream()
.map(e -> new WordCount(e.getKey(), e.getValue()))
.sorted(Comparator.comparingLong(WordCount::count).reversed())
.limit(n)
.map(WordCount::word)
.toList();
}

Records in Streams and Collections

Records work seamlessly with the Stream API and collections because they have correct equals() and hashCode() implementations by default:

record Product(String name, double price, String category) {}
List<Product> products = List.of(
new Product("Laptop", 999.99, "Electronics"),
new Product("Phone", 699.99, "Electronics"),
new Product("Desk", 299.99, "Furniture"),
new Product("Chair", 199.99, "Furniture")
);
// Group by category, find most expensive in each
Map<String, Optional<Product>> mostExpensive = products.stream()
.collect(Collectors.groupingBy(
Product::category,
Collectors.maxBy(Comparator.comparingDouble(Product::price))
));
mostExpensive.forEach((cat, p) ->
System.out.printf("%s: %s%n", cat, p.map(Product::name).orElse("none"))
);
// Electronics: Laptop
// Furniture: Desk

Pattern Matching for instanceof — Finalised (Java 16)

Pattern matching for instanceof (JEP 394), previewed in Java 14 and 15, was finalised in Java 16.

// Stable in Java 16+
Object obj = "Hello, World!";
if (obj instanceof String s && s.length() > 5) {
System.out.println(s.toUpperCase()); // HELLO, WORLD!
}
// Negation — s is in scope in the else branch
if (!(obj instanceof String s)) {
System.out.println("Not a string");
} else {
System.out.println(s.length()); // 13
}

Stream toList() (Java 16)

Java 16 added Stream.toList() as a convenient shorthand for the verbose collect(Collectors.toList()):

// Before Java 16
List<String> result = stream.filter(s -> s.length() > 3)
.collect(Collectors.toList());
// Java 16+
List<String> result = stream.filter(s -> s.length() > 3)
.toList();

Note: Stream.toList() returns an unmodifiable list, unlike Collectors.toList() which returns a mutable ArrayList. If you need a mutable list, continue using Collectors.toList() or Collectors.toCollection(ArrayList::new).


Java 17: The New LTS Release

Java 17, released on September 14, 2021, is the LTS release that succeeded Java 11. It is the version most organisations adopted as their new production baseline.

Sealed Classes — Finalised (Java 17)

Sealed classes (JEP 409), previewed in Java 15 and 16, were finalised in Java 17. They are now a stable, permanent language feature.

// Sealed interface modelling a payment result
public sealed interface PaymentResult
permits PaymentResult.Success, PaymentResult.Failure, PaymentResult.Pending {
record Success(String transactionId, double amount) implements PaymentResult {}
record Failure(String reason, int errorCode) implements PaymentResult {}
record Pending(String referenceId) implements PaymentResult {}
}
// Usage — exhaustive switch (no default needed)
String describe(PaymentResult result) {
return switch (result) {
case PaymentResult.Success s -> "Paid: " + s.transactionId();
case PaymentResult.Failure f -> "Failed: " + f.reason();
case PaymentResult.Pending p -> "Pending: " + p.referenceId();
};
}

The combination of sealed interfaces and records is extremely powerful for modelling algebraic data types — a pattern common in functional languages like Haskell and Scala, now available in Java.

Pattern Matching for switch (Preview — Java 17)

Java 17 introduced pattern matching for switch (JEP 406) as a preview feature. This extends the instanceof pattern matching to work inside switch expressions and statements.

// Java 17 preview — type patterns in switch
static String describe(Object obj) {
return switch (obj) {
case Integer i -> "Integer: " + i;
case Long l -> "Long: " + l;
case Double d -> "Double: " + d;
case String s -> "String of length " + s.length();
case null -> "null";
default -> "Something else: " + obj.getClass().getSimpleName();
};
}
describe(42); // "Integer: 42"
describe("hello"); // "String of length 5"
describe(null); // "null"

This feature was finalised in Java 21.

Foreign Function & Memory API (Incubator — Java 17)

Java 17 continued the incubation of the Foreign Function & Memory API (JEP 412), which is a modern replacement for JNI (Java Native Interface). It allows Java code to:

  • Call native functions in C libraries without writing JNI glue code
  • Safely access off-heap memory with well-defined lifetime management
// Java 17 incubator — calling strlen from the C standard library
try (var session = MemorySession.openConfined()) {
var cString = session.allocateUtf8String("Hello, native world!");
var strlen = CLinker.systemCLinker().lookup("strlen").get();
var handle = CLinker.systemCLinker().downcallHandle(
strlen,
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
long length = (long) handle.invoke(cString);
System.out.println(length); // 20
}

This API was finalised in Java 22.

Deprecating the Security Manager

The Security Manager (JEP 411), a feature since Java 1.0 that allowed applications to restrict what code could do at runtime, was deprecated in Java 17 and removed in Java 24. It had been largely ineffective in practice and was rarely used correctly.

Strong Encapsulation of JDK Internals

Java 17 made the strong encapsulation of JDK internal APIs the default and permanent (JEP 403). The --illegal-access flag, which had been a migration escape hatch since Java 9, was removed. Code that relied on reflective access to JDK internals (like sun.misc.Unsafe) must now use --add-opens explicitly.


Summary

Java 16 and 17 completed a multi-release delivery cycle and established a clean, modern foundation. Java 17 is the LTS release that most teams should be targeting today.

FeatureVersionKey Benefit
Records (Final)16Stable, concise immutable data classes
Pattern Matching instanceof (Final)16Stable type-safe casting
Stream.toList()16Concise, unmodifiable list collection
Sealed Classes (Final)17Stable controlled class hierarchies
Pattern Matching for switch (Preview)17Type-based dispatch in switch
Foreign Function & Memory API (Incubator)17Native code without JNI boilerplate
Strong JDK Encapsulation17Enforced API boundaries, no illegal access