JDK 14 Key Features — Records, Pattern Matching, and Helpful NPE Messages

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:

SyntaxValue Return MethodFall-through
case L -> (arrow)Expression or yieldNone
case L: (colon)yield requiredYes (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

FeatureStatusFuture Changes
RecordsPreviewFinal in Java 16
Pattern Matching (instanceof)PreviewFinal in Java 16
Helpful NPEFinal (flag required)Default enabled in Java 15
Switch ExpressionsFinalizedComplete
Text BlocksSecond PreviewFinal in Java 15
ZGC macOS/WindowsExperimentalProduction-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.

Was this article helpful?