JDK 16 Overview
Java 16, released in March 2021, is a non-LTS release. Records and Pattern Matching for instanceof were finalized after their preview stages, and convenience APIs like Stream.toList() were added. Rather than major innovations, this version focused on stabilizing features that had been experimental since JDK 14-15.
| JEP | Feature | Status |
|---|---|---|
| JEP 395 | Records | Finalized |
| JEP 394 | Pattern Matching for instanceof | Finalized |
| JEP 392 | jpackage (Native Packaging) | Finalized |
| JEP 338 | Vector API | Incubator |
| JEP 376 | ZGC Concurrent Thread-Stack Processing | Improvement |
| JEP 396 | Strong Encapsulation of Internal APIs (Default) | Change |
Records — Immutable Data Carriers (JEP 395)
Records are dedicated classes for holding immutable data. The compiler auto-generates equals(), hashCode(), toString(), and accessor methods. They cleanly replace DTOs (Data Transfer Objects) that were previously filled with boilerplate code.
import java.util.List;
// Record definition — complete immutable data class in one line
record Product(String name, int price, String category) {}
public class RecordExample {
public static void main(String[] args) {
// Create Record instances
Product laptop = new Product("MacBook Pro", 2_490_000, "Electronics");
Product phone = new Product("Galaxy S25", 1_350_000, "Electronics");
// Auto-generated accessor methods (same as field name, not getters)
System.out.println(laptop.name()); // MacBook Pro
System.out.println(laptop.price()); // 2490000
// Auto-generated toString()
System.out.println(laptop);
// Product[name=MacBook Pro, price=2490000, category=Electronics]
// Auto-generated equals() — all field values compared
Product another = new Product("MacBook Pro", 2_490_000, "Electronics");
System.out.println(laptop.equals(another)); // true
// Using with lists
List<Product> products = List.of(laptop, phone);
products.stream()
.filter(p -> p.price() > 2_000_000)
.forEach(p -> System.out.println(p.name() + " -> premium product"));
// MacBook Pro -> premium product
}
}
Here’s a summary of Record’s key characteristics:
finalclass -> cannot be extended- All fields are
private final-> immutable - Static fields/methods and instance methods can be added
- Validation via custom constructors (Compact Constructor)
implementsallowed,extendsnot allowed (implicitly extendsjava.lang.Record)
Pattern Matching for instanceof (JEP 394)
Previously, a separate cast was required after an instanceof check. Starting with JDK 16, type checking and variable declaration happen in one step.
import java.util.List;
public class PatternMatchingExample {
// Method that handles various types
static String describe(Object obj) {
// Old way: repeated instanceof + casting
// if (obj instanceof String) {
// String s = (String) obj;
// return "String(length=" + s.length() + ")";
// }
// JDK 16 way: casting eliminated with pattern variable
if (obj instanceof String s) {
return "String(length=" + s.length() + ")";
} else if (obj instanceof Integer i && i > 0) {
// Pattern variable + condition combination
return "Positive integer: " + i;
} else if (obj instanceof int[] arr) {
return "int array(size=" + arr.length + ")";
} else if (obj instanceof List<?> list && !list.isEmpty()) {
return "Non-empty list(size=" + list.size() + ")";
}
return "Unknown type: " + obj.getClass().getSimpleName();
}
public static void main(String[] args) {
System.out.println(describe("Hello Java 16")); // String(length=14)
System.out.println(describe(42)); // Positive integer: 42
System.out.println(describe(-7)); // Unknown type: Integer
System.out.println(describe(new int[]{1, 2, 3})); // int array(size=3)
System.out.println(describe(List.of("a", "b"))); // Non-empty list(size=2)
}
}
Pattern variable scope is valid only “when the type matches.” Combining with the && operator allows natural expression of additional conditions. This feature later extends to Sealed Classes + switch pattern matching in JDK 17.
Stream.toList() — Concise List Collection
If typing Collectors.toList() every time was tedious, this is welcome news. Stream.toList() was added starting with JDK 16.
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class StreamToListExample {
public static void main(String[] args) {
// Old way: using Collectors.toList()
List<Integer> oldWay = IntStream.rangeClosed(1, 10)
.boxed()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println("Old: " + oldWay); // Old: [2, 4, 6, 8, 10]
// JDK 16 way: Stream.toList() — more concise
List<Integer> newWay = IntStream.rangeClosed(1, 10)
.boxed()
.filter(n -> n % 2 == 0)
.toList();
System.out.println("New: " + newWay); // New: [2, 4, 6, 8, 10]
// Note: toList() returns an unmodifiable list
try {
newWay.add(12); // UnsupportedOperationException thrown!
} catch (UnsupportedOperationException e) {
System.out.println("toList() result is unmodifiable");
}
// If a mutable list is needed, still use Collectors.toList()
List<Integer> mutableList = IntStream.rangeClosed(1, 5)
.boxed()
.collect(Collectors.toList());
mutableList.add(6);
System.out.println("Mutable: " + mutableList); // Mutable: [1, 2, 3, 4, 5, 6]
}
}
| Method | Return Type | Mutable | Allows null |
|---|---|---|---|
Collectors.toList() | ArrayList | Yes | Yes |
Stream.toList() | Unmodifiable List | No | Yes |
Collectors.toUnmodifiableList() | Unmodifiable List | No | No |
Stream.toList() allows null elements, but Collectors.toUnmodifiableList() throws NullPointerException if there are nulls. It’s good to remember this distinction.
Day Period Support
Using the B pattern in DateTimeFormatter enables finer-grained expressions like “morning”, “noon”, “evening”, “night” instead of just “AM” and “PM”.
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public class DayPeriodExample {
public static void main(String[] args) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("a hh:mm / B hh:mm")
.withLocale(Locale.ENGLISH);
// Check Day Period at various times
LocalTime[] times = {
LocalTime.of(6, 0), // Morning
LocalTime.of(12, 0), // Noon
LocalTime.of(18, 30), // Evening
LocalTime.of(23, 0) // Night
};
for (LocalTime time : times) {
System.out.println(time + " -> " + formatter.format(time));
}
// 06:00 -> AM 06:00 / in the morning 06:00
// 12:00 -> PM 12:00 / noon 12:00
// 18:30 -> PM 06:30 / in the evening 06:30
// 23:00 -> PM 11:00 / at night 11:00
}
}
Performance and Tooling Improvements
ZGC Concurrent Thread-Stack Processing (JEP 376): ZGC’s thread-stack processing changed from safepoint-based to concurrent processing. GC pause times are further reduced, enabling sub-millisecond pauses.
jpackage Finalized (JEP 392): jpackage, which started as an incubator in JDK 14, became an official tool. It can create platform-specific native packages (deb, rpm, msi, dmg) from Java applications.
# Package JAR as a Windows installer
jpackage --input target/ \
--name MyApp \
--main-jar my-app.jar \
--main-class com.example.Main \
--type msi
Vector API (Incubator, JEP 338): A vector computation API leveraging SIMD instructions appeared for the first time. It can dramatically improve numerical computation performance and continues to evolve in later JDK versions.
Strong Encapsulation of Internal APIs (JEP 396): The default for --illegal-access changed to deny. Code that directly accesses internal APIs like sun.misc.Unsafe will not run without explicit --add-opens.
Summary
JDK 16 is a “from experiment to stability” release. Here are the key points:
- Records: Define DTOs and value objects in one line. Auto-generated
equals/hashCode/toString - Pattern Matching for instanceof: Eliminate casting boilerplate. Use the
if (obj instanceof String s)pattern - Stream.toList(): Use instead of
collect(Collectors.toList())for conciseness. Note: returned list is unmodifiable - jpackage: Official tool for packaging JARs as native installers
- Vector API: First steps toward SIMD operations (Incubator)
- Internal API Encapsulation:
sun.misc.*direct access blocked by default
JDK 16’s Records and pattern matching combine with Sealed Classes in the next LTS (JDK 17) to form a more powerful type system. Even if you skip JDK 16, it’s worth learning these two features.