Records (Preview)
Record (JEP 359), introduced in Java 14, is a syntax for declaring immutable data classes in a single line. The compiler auto-generates equals(), hashCode(), toString(), and getters.
Using a shipping label analogy: the old class approach was like hand-drawing the label form every time, while Record is like filling in just the name and address on a standard label form.
// === Record declaration: one line is enough ===
record Point(int x, int y) {}
record User(String name, int age, String email) {
// Compact constructor: write validation logic without parameter declarations
User {
if (age < 0) {
throw new IllegalArgumentException("Age must be 0 or greater: " + age);
}
// name, age, email fields are auto-assigned
}
// Custom methods can be added
String displayName() {
return name + " (age " + age + ")";
}
}
public class RecordDemo {
public static void main(String[] args) {
// Create Record instances
Point p1 = new Point(10, 20);
Point p2 = new Point(10, 20);
// Auto-generated toString()
System.out.println("p1 = " + p1);
// Output: p1 = Point[x=10, y=20]
// Auto-generated equals() — value-based comparison
System.out.println("p1.equals(p2) = " + p1.equals(p2));
// Output: p1.equals(p2) = true
// Accessor methods — x() not getX()
System.out.println("p1.x() = " + p1.x());
System.out.println("p1.y() = " + p1.y());
// Output: p1.x() = 10
// Output: p1.y() = 20
// Custom validation and methods
User user = new User("John Doe", 30, "john@example.com");
System.out.println(user.displayName());
// Output: John Doe (age 30)
// Exception on validation failure
try {
new User("Jane Smith", -1, "jane@example.com");
} catch (IllegalArgumentException e) {
System.out.println("Validation failed: " + e.getMessage());
// Output: Validation failed: Age must be 0 or greater: -1
}
}
}
Records cannot do the following: extend other classes (implicitly extends java.lang.Record), add instance fields (only declared components are allowed), or change field values (final). These constraints actually ensure a clear intent as immutable data objects.
Pattern Matching for instanceof (Preview)
The repetitive pattern of checking instanceof then immediately casting was simplified in Java 14 (JEP 305).
public class PatternMatchingDemo {
// Method that prints information for various types
static String formatValue(Object obj) {
// === Old way: repeated instanceof + casting ===
// if (obj instanceof String) {
// String s = (String) obj; // Cast required
// return "String(length=" + s.length() + "): " + s;
// }
// === New way: pattern variable for auto-casting ===
if (obj instanceof String s) {
// s is already String type — no cast needed
return "String(length=" + s.length() + "): " + s;
} else if (obj instanceof Integer i) {
return "Integer: " + i + " (even? " + (i % 2 == 0) + ")";
} else if (obj instanceof Double d) {
return "Double: " + String.format("%.2f", d);
} else if (obj instanceof int[] arr) {
return "Array(size=" + arr.length + ")";
} else {
return "Other: " + obj.getClass().getSimpleName();
}
}
public static void main(String[] args) {
System.out.println(formatValue("Hello"));
System.out.println(formatValue(42));
System.out.println(formatValue(3.14159));
System.out.println(formatValue(new int[]{1, 2, 3}));
// Output:
// String(length=5): Hello
// Integer: 42 (even? true)
// Double: 3.14
// Array(size=3)
// Pattern variables can be combined with logical operators
Object value = "Java 14";
if (value instanceof String s && s.length() > 5) {
System.out.println("String longer than 5 chars: " + s);
}
// Output: String longer than 5 chars: Java 14
}
}
Be aware of the scope of pattern variables. They are only valid inside the if block and cannot be used in the else block. Also, they combine with && but not ||. obj instanceof String s || s.isEmpty() is a compile error because s may not be bound on the right side of ||.
Helpful NullPointerExceptions (JEP 358)
The most commonly seen exception message in Java development became much more useful. It now tells you exactly which variable is null.
import java.util.List;
import java.util.Map;
public class HelpfulNpeDemo {
record Address(String city, String zipCode) {}
record Company(Address address) {}
record Employee(String name, Company company) {}
public static void main(String[] args) {
// Nested null reference
Employee emp = new Employee("John Doe", new Company(null));
try {
// company.address is null -> NPE when calling address.city()
String city = emp.company().address().city();
} catch (NullPointerException e) {
System.out.println("NPE message: " + e.getMessage());
// Before Java 14:
// NullPointerException (no message or null)
//
// Java 14 and later:
// Cannot invoke "HelpfulNpeDemo$Address.city()"
// because the return value of
// "HelpfulNpeDemo$Company.address()" is null
}
// Detailed NPE messages for Maps too
Map<String, List<String>> data = Map.of("fruits", List.of("apple", "pear"));
try {
// "vegetables" key doesn't exist, so get() returns null
int size = data.get("vegetables").size();
} catch (NullPointerException e) {
System.out.println("NPE message: " + e.getMessage());
// Output: Cannot invoke "java.util.List.size()"
// because the return value of
// "java.util.Map.get(Object)" is null
}
}
}
This feature can be enabled in Java 14 with the -XX:+ShowCodeDetailsInExceptionMessages flag, and is enabled by default starting in Java 15. It significantly reduces NPE debugging time even in production environments.
Switch Expressions Finalized
Switch Expressions, which started as a preview in Java 12 and gained yield in Java 13, became a final feature in Java 14 (JEP 361). The --enable-preview flag is no longer needed.
Here’s a summary of the finalized rules:
| Syntax | Value Return Method | Fall-through |
|---|---|---|
case L -> (arrow) | Expression or yield | None |
case L: (colon) | yield required | Yes (prevented with break) |
Text Blocks (Second Preview)
Text Blocks, first previewed in Java 13, continue as a second preview. Two new escape sequences were added:
\s— a single space (for preserving trailing whitespace)\(at line end) — prevents line break (wraps long lines only in source)
Performance: ZGC and G1 Expansion
ZGC macOS/Windows Support (JEP 364, 365): ZGC, previously Linux-only, became experimentally available on macOS and Windows. This allows testing with the same GC as production in development environments.
NUMA-Aware G1 (JEP 345): On multi-socket servers, G1 GC recognizes NUMA (Non-Uniform Memory Access) architecture and prioritizes local memory allocation. This improves GC performance on large servers.
Summary
| Feature | Status | Future Changes |
|---|---|---|
| Records | Preview | Final in Java 16 |
| Pattern Matching (instanceof) | Preview | Final in Java 16 |
| Helpful NPE | Final (flag required) | Default enabled in Java 15 |
| Switch Expressions | Finalized | Complete |
| Text Blocks | Second Preview | Final in Java 15 |
| ZGC macOS/Windows | Experimental | Production-ready in Java 15 |
Java 14 can be called the “flowering of previews.” Records and Pattern Matching — two features that shape Java’s future — appeared simultaneously, and Switch Expressions were finalized. Helpful NPE is a small but highly impactful change for everyday debugging. If you’re using Java 8 or 11 in production, these features are a compelling reason to upgrade to Java 17 LTS.