JavaModern Javaintermediate
Updated:

Java 17 Features: Records, Sealed Classes and Pattern Matching

6 min read

Everything you gain moving from Java 8 or 11 to 17: records, sealed classes, pattern matching, switch expressions and text blocks — with runnable examples.

TL;DR – Quick Answer

Java 17 is a long-term support (LTS) release whose headline features are records for concise immutable data classes, sealed classes and interfaces for controlled inheritance, and pattern matching for instanceof that removes cast boilerplate. Teams upgrading from Java 8 or 11 also gain switch expressions, text blocks and helpful NullPointerException messages accumulated across releases 9 to 16.

On This Page

Java 17 is the version job descriptions started naming after a decade of "Java 8 required." As a long-term support (LTS) release, it became the default upgrade target for teams leaving 8 and 11 — which means "what's new in Java 17" is now a genuine interview question, not trivia.

Strictly speaking, Java 17 finalized one headline language feature (sealed classes). But nobody migrates one version at a time: when people say "Java 17 features," they mean everything you gain arriving at 17 from an older LTS. That is how this guide treats it — records, sealed types, pattern matching, switch expressions, text blocks, and the runtime improvements underneath.

Why Java 17 matters more than 12–16

Between LTS releases, Java ships a feature release every six months. Almost nobody runs 13 or 15 in production; the six-month versions are where features preview and stabilize, and the LTS versions are where they land for real use. Java 17 (September 2021) collected everything stabilized since 11 into a supported baseline — and vendors back LTS releases with years of patches.

For your career the practical read is simple: Java 8 gave the language its functional core — lambdas, streams and Optional — and Java 17 gave it modern data modeling. Interviews now sample from both. Where 17 sits in the bigger learning sequence is mapped in the Java developer roadmap.

Records: data classes without the ceremony

A classic DTO — private final fields, constructor, getters, equals, hashCode, toString — is 40+ lines for two fields. A record is one line, and the compiler generates all of it:

public class RecordDemo {

    record Course(String name, int weeks) {
        // Compact constructor: validation without repeating assignments
        Course {
            if (weeks <= 0) {
                throw new IllegalArgumentException("weeks must be positive");
            }
        }

        boolean isIntensive() {          // ordinary methods are allowed
            return weeks <= 16;
        }
    }

    public static void main(String[] args) {
        Course a = new Course("Java Full Stack", 16);
        Course b = new Course("Java Full Stack", 16);

        System.out.println(a);              // Course[name=Java Full Stack, weeks=16]
        System.out.println(a.equals(b));    // true — value-based equality
        System.out.println(a.name());       // accessor is name(), not getName()
        System.out.println(a.isIntensive()); // true
    }
}

What to notice: two distinct instances with the same data are equals — records implement value semantics out of the box, which is exactly what you want for DTOs, API responses, map keys and stream results. The compact constructor validates without restating this.name = name.

Rules worth remembering: record fields are final, a record cannot extend a class (it implicitly extends java.lang.Record), it can implement interfaces, and it can have static members.

Common mistake: A common mistake beginners make is calling records "immutable" without qualification. The record's own fields cannot be reassigned, but if a component is a mutable type — a List, a Date — its contents can still change. For true immutability, defensively copy in the compact constructor: items = List.copyOf(items);.

Pro tip: Do not reach for records as JPA entities. JPA needs a no-arg constructor, mutable state and proxying — everything records deliberately forbid. Records shine one layer up: projections, API request/response bodies and value objects, while entities stay regular classes.

Sealed classes: inheritance with a guest list

Ordinary inheritance is open to the world: anyone can extend your public class. Interfaces and abstract classes define what subtypes look like, but never which subtypes exist. Sealed types close that gap with a permits clause:

public class SealedDemo {

    sealed interface Payment permits Upi, Card, NetBanking {}

    record Upi(String vpa) implements Payment {}
    record Card(String network, String last4) implements Payment {}
    record NetBanking(String bank) implements Payment {}

    static String describe(Payment p) {
        // Pattern matching for instanceof: test, cast and bind in one step
        if (p instanceof Upi u) {
            return "UPI payment to " + u.vpa();
        } else if (p instanceof Card c) {
            return c.network() + " card ending " + c.last4();
        } else if (p instanceof NetBanking n) {
            return "NetBanking via " + n.bank();
        }
        return "Unreachable — the hierarchy is sealed";
    }

    public static void main(String[] args) {
        System.out.println(describe(new Upi("ravi@okbank")));
        System.out.println(describe(new Card("Visa", "4242")));
        System.out.println(describe(new NetBanking("SBI")));
    }
}

What to notice: Payment can never have a fourth implementation — the compiler rejects any class not on the permits list. Your describe logic is therefore complete by construction; a new payment mode forces a compile-time conversation instead of a runtime surprise.

Each permitted subclass must declare its own stance: final (closed), sealed (continues the controlled hierarchy) or non-sealed (opens its branch back up). Records are implicitly final, which is why records and sealed interfaces pair so naturally — together they model "this type is exactly one of these N shapes," a style long available in Kotlin and Scala.

Pattern matching: the end of cast boilerplate

You saw it above; here is the before and after isolated. Old style:

if (obj instanceof String) {
    String s = (String) obj;   // test, then cast, then use — three steps
    System.out.println(s.length());
}

Java 16+ style:

if (obj instanceof String s) { // test, cast and bind in one
    System.out.println(s.length());
}

The binding variable s is in scope only where the match is guaranteed — the compiler's flow scoping even lets you write if (!(obj instanceof String s)) return; and use s after the guard. This is runtime polymorphism's awkward cousin done right: sometimes you genuinely must branch on concrete type, and now the language makes it safe and short.

Note the version boundary interviewers probe: instanceof patterns are standard in 17; pattern matching inside switch was only a preview in 17 and became standard in Java 21, where sealed hierarchies get compiler-checked exhaustive switches without a default branch.

Switch expressions and text blocks

Two quality-of-life features from the 11→17 span you will use daily. Switch expressions (standard since 14) return a value, use arrow labels, don't fall through, and must be exhaustive:

int weeks = switch (track) {
    case "java-full-stack"      -> 16;
    case "data-analyst"         -> 12;
    case "data-science", "gen-ai" -> 20;
    default -> throw new IllegalArgumentException("Unknown: " + track);
};

Text blocks (standard since 15) end the escaped-quotes-and-concatenation era for embedded JSON, SQL and HTML:

String json = """
        {
          "course": "Java Full Stack",
          "city": "Hyderabad"
        }
        """;

The closing delimiter's position controls how much incidental indentation is stripped — move it left to keep more leading whitespace.

Under the hood: what 17 improves without new syntax

  • Helpful NullPointerExceptions (default since 15): the message now names the exact variable or call that was null — Cannot invoke "String.length()" because "user.name" is null — turning the JVM's most famous error into a self-diagnosing one.
  • Better garbage collectors: ZGC became production-ready in 15 and G1 gained steady pause-time improvements, part of why latency-sensitive teams upgrade — see how modern GC works.
  • Strong encapsulation of JDK internals (JEP 403): illegal reflective access to sun.misc.* and friends now fails instead of warning. This is the change that breaks old libraries on 17 and the reason dependency upgrades are step one of any migration.
  • RandomGenerator API (17): a clean interface family over new and old random number generators.

Interview note: "You migrated a service from Java 8 to 17 — what broke?" is a favorite scenario question. Strong encapsulation of internals (old Lombok/Hibernate/ASM versions), the removal of Nashorn, and third-party libraries doing deep reflection are the honest answers. Saying "nothing, it just worked" reads as never having done it.

Records + sealed + streams: the payoff

The features compose. A sealed interface of records flowing through the Stream API reads like a domain specification:

List<Payment> payments = List.of(
        new Upi("ravi@okbank"), new Card("Visa", "4242"), new Upi("sita@upi"));

long upiCount = payments.stream()
        .filter(p -> p instanceof Upi)
        .count();                        // 2

This combination — closed hierarchies, immutable data, functional pipelines — is the house style of modern Java codebases, and it is the style we teach from day one in the Java Full Stack program because it is what you will meet in a 2026 code review.

How to practice this

Take any class in your practice projects that is pure data and convert it to a record — then delete the tests that only verified getters and equals, because the language now guarantees them. Next, find one if-else chain that branches on type and rebuild it as a sealed interface with record implementations. Two refactors like that and Java 17 stops being a feature list you memorized and becomes the way you write.

Frequently Asked Questions

Why is Java 17 called an LTS release?
Oracle and other JDK vendors designate certain versions for long-term support, meaning years of security patches and bug fixes rather than the six-month lifespan of feature releases. Java 8, 11, 17 and 21 are the LTS line, which is why production systems and job descriptions cluster around them.
What is a record in Java?
A record is a concise class for immutable data. Declaring record Point(int x, int y) generates a constructor, private final fields, accessors named x() and y(), plus value-based equals, hashCode and toString. Records cannot extend other classes and their fields cannot be reassigned after construction.
Can records replace Lombok?
For immutable value carriers, largely yes: records eliminate the getter, constructor, equals, hashCode and toString boilerplate without an annotation processor. Lombok still covers cases records cannot, such as mutable beans with setters, builders for classes with many fields, and logging annotations.
What problem do sealed classes solve?
They let a type author list exactly which classes may extend or implement it, using the permits clause. This turns a class hierarchy into a closed set the compiler knows completely, enabling exhaustive handling of all subtypes and preventing unknown implementations from third-party code.
Is pattern matching for switch available in Java 17?
Only as a preview feature behind the --enable-preview flag. Java 17 includes pattern matching for instanceof as a standard feature; pattern matching in switch statements and expressions became standard later, in Java 21. In production Java 17 code you combine instanceof patterns with if-else chains.
Should I learn Java 8 or Java 17 first?
Learn the Java 8 fundamentals first because lambdas, streams and Optional remain the daily working vocabulary and the core of interviews. Then layer on the 17 features, which mostly reduce boilerplate rather than change how you think. Most Indian service and product companies now run on 11, 17 or 21, so you will meet both styles.

Want to Build Your Career in Java Full Stack with AI?

Join CodeBegun and train with working industry engineers — Explore the Program

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 14 July 2026 LinkedIn
Chat with us