Java 16 & 17: The Modern LTS Baseline
Java 16 finalised records and pattern matching for instanceof. Java 17 became the new LTS release, finalising sealed classes and introducing pattern matching for switch in preview. Together they define the modern Java baseline that most teams target today.
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 eachMap<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: DeskPattern 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 branchif (!(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 16List<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, unlikeCollectors.toList()which returns a mutableArrayList. If you need a mutable list, continue usingCollectors.toList()orCollectors.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 resultpublic 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 switchstatic 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 librarytry (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.
| Feature | Version | Key Benefit |
|---|---|---|
| Records (Final) | 16 | Stable, concise immutable data classes |
Pattern Matching instanceof (Final) | 16 | Stable type-safe casting |
Stream.toList() | 16 | Concise, unmodifiable list collection |
| Sealed Classes (Final) | 17 | Stable controlled class hierarchies |
Pattern Matching for switch (Preview) | 17 | Type-based dispatch in switch |
| Foreign Function & Memory API (Incubator) | 17 | Native code without JNI boilerplate |
| Strong JDK Encapsulation | 17 | Enforced API boundaries, no illegal access |