🧩
Java 8+

Java 17 Features

Records, Sealed Classes & Pattern Matching
💡 Java 17 is like a smart blueprint for a new house — Records are a ready-made furniture kit (use it directly without assembling anything yourself), Sealed Classes are a housing society's rule ("only these specific house types can be built, and nothing else"), and Pattern Matching is a smart lock that hands you the right key the moment it checks you.

Java 17 (September 2021) is the second major LTS (Long-Term Support) release after Java 11 — many companies migrate straight to this version because it bundles several years of improvements at once (every feature from Java 12 through 17).

The biggest highlights: Records (data classes in one line), Sealed Classes (controlled inheritance), Pattern Matching for instanceof (auto-casting), Text Blocks (multi-line strings made easy), and Switch Expressions.

Java 17 features come up a lot in interviews, especially Records and Sealed Classes — because they change the way modern Java code gets written, cutting down on boilerplate.

// Record — ek line mein data class
record User(String name, int age) {}
User u = new User("Aarav", 25);
System.out.println(u); // User[name=Aarav, age=25]

// Pattern Matching for instanceof
Object obj = "Hello";
if (obj instanceof String s) {
  System.out.println(s.length()); // seedha use, cast nahi chahiye
}
🧩
Java 17 is like a smart blueprint for a new house — Records are a ready-made furniture kit (use it directly without assembling anything yourself), Sealed Classes are a housing society's rule ("only these specific house types can be built, and nothing else"), and Pattern Matching is a smart lock that hands you the right key the moment it checks you.
1 / 6
⚡ Quick Recap
  • Java 17 = LTS release (Sept 2021)
  • Records = data classes in one line
  • Sealed Classes = controlled inheritance
  • Pattern Matching for instanceof = auto-cast
  • Text Blocks = multi-line strings made easy
On this page (8 subtopics)

Creating a simple data-holder class (like a Point with just x and y) in a normal class means writing a lot of boilerplate — a constructor, getters, equals(), hashCode(), toString(), all by hand (or generated by an IDE). The record keyword generates all of this automatically in one line.

Writing record Point(int x, int y) {} makes the compiler generate: a constructor (that sets both fields), getter methods (x() and y() — no "get" prefix), and properly implemented equals(), hashCode(), toString(). Records are implicitly final (can't be extended), and their fields are implicitly final too (immutable).

Records are best when you just need to carry data, with no complex behavior — like API responses, DTOs (Data Transfer Objects), or coordinate pairs. If a class needs methods/logic too (not just data), a normal class is still the better fit.

record Point(int x, int y) {}

Point p1 = new Point(3, 4);
System.out.println(p1.x());       // 3 (getter, "get" prefix nahi)
System.out.println(p1);           // Point[x=3, y=4] (auto toString)

Point p2 = new Point(3, 4);
System.out.println(p1.equals(p2)); // true (auto equals, values compare hote hain)
💡Tip: You can add custom validation in a record with a "compact constructor" — record Point(int x, int y) { Point { if (x < 0) throw new IllegalArgumentException("x can't be negative"); } }

Anyone can normally extend/implement a class/interface — the sealed keyword lets you control *exactly* who's allowed to extend/implement it. This is especially useful when you know upfront exactly what all the possible subtypes are (like a Shape that can only be Circle, Square, or Triangle, and nothing else).

Writing sealed class Shape permits Circle, Square, Triangle {} means only these three classes can extend Shape — no outside code can create a new subtype. Every permitted subclass has to explicitly declare itself: final (can't be extended further), sealed (controls its own further extension), or non-sealed (back to normal, anyone can extend it).

The benefit: in switch statements, the compiler knows exactly what all the possible types are, so "exhaustiveness checking" becomes possible (we'll see this with pattern matching in the next subtopic) — the compiler can confirm every case is covered even without a default case.

sealed interface Shape permits Circle, Square, Triangle {}

final class Circle implements Shape { double radius; }
final class Square implements Shape { double side; }
final class Triangle implements Shape { double base, height; }

// Koi doosri class Shape implement NAHI kar sakti — compiler error dega
⚠️Common Mistake: All permitted classes of a sealed hierarchy (if they're in separate files) must be in the same module/package as where the sealed type is defined — this is a compiler-enforced rule.

In older Java, after an instanceof check you had to manually cast — two steps, a bit repetitive: if (obj instanceof String) { String s = (String) obj; ... }. In Java 16+ (stable in 17), instanceof can now "bind" a variable itself if the check passes.

Writing if (obj instanceof String s) means that if obj is a String, s automatically points to that already-cast value — you can use s directly inside that if block, no manual cast needed. This makes code shorter and safer (less risk of a ClassCastException, since the compiler itself does the type-check).

Object obj = "Hello Java 17";

// Purana tareeka:
if (obj instanceof String) {
  String s = (String) obj;
  System.out.println(s.length());
}

// Java 16+ pattern matching:
if (obj instanceof String s) {
  System.out.println(s.length()); // s seedha use ho sakta hai, cast nahi chahiye
}

Writing a multi-line String (like JSON, HTML, a SQL query) in older Java was messy — a \n at the end of every line, escaping quotes, concatenating with +. Text blocks (starting and ending with """) get rid of all of that.

A text block automatically strips leading whitespace "smartly" (removes the common indentation across all lines), converts line breaks to \n, and you don't need to escape quotes (except in edge cases).

// Purana tareeka:
String json1 = "{\n" +
    "  \"name\": \"Aarav\",\n" +
    "  \"age\": 25\n" +
    "}";

// Text block (Java 15+):
String json2 = """
    {
      "name": "Aarav",
      "age": 25
    }
    """;
💡Tip: The position of the closing """ in a text block matters — it decides how much leading whitespace is considered "common" and gets stripped. If you place the closing """ further back than even the least-indented line, the extra indentation gets preserved.

Switch Expressions (Java 14+, stable) are shorter and safer than the old switch statement — they use arrow syntax (case X -> result), there's no fall-through (no need for a break at the end of every case), and they can "return" a value too (usable directly in an assignment).

In Java 17, Pattern Matching for switch arrived as a preview feature (stabilized in later versions) — now you can branch in a switch based on type too, with pattern binding just like instanceof: case Integer i -> ..., case String s -> ....

// Purana switch statement:
int day = 3;
String name;
switch (day) {
  case 1: name = "Monday"; break;
  case 2: name = "Tuesday"; break;
  default: name = "Other";
}

// Switch Expression (Java 14+):
String name2 = switch (day) {
  case 1 -> "Monday";
  case 2 -> "Tuesday";
  default -> "Other";
};

// Pattern Matching for switch (preview, Java 17):
Object obj = 42;
String result = switch (obj) {
  case Integer i -> "Integer: " + i;
  case String s -> "String: " + s;
  default -> "Unknown";
};

The old java.util.Random used a single algorithm, and adding new/better random algorithms was hard (everything was hardcoded into the Random class itself). Java 17 introduced the RandomGenerator interface, giving a common API for all random number generators, old and new.

New algorithms arrived too — like Xoshiro256PlusPlus, which offers better statistical quality and performance than the old Random, especially for parallel/multi-threaded use (like SplittableRandom, which can give independent streams to different threads without lock overhead).

RandomGenerator rng = RandomGenerator.of("Xoshiro256PlusPlus");
System.out.println(rng.nextInt(100)); // 0-99 ke beech ek random number

// Streams of random numbers bhi easily ban sakte hain:
rng.ints(5, 1, 10).forEach(System.out::println); // 5 numbers, 1-9 ke beech

The module system (JPMS) arrived in Java 9, but for backward compatibility, internal JDK APIs (like sun.misc.Unsafe) could still be accessed with the --illegal-access flag. Since Java 17, these internals are strongly encapsulated — they're no longer accessible by default, no matter what flag you use (only a few critical ones need explicit --add-opens).

This is less directly relevant to your own application code, but if a library/framework you use relied on internal JDK classes (via reflection), it could break when upgrading to Java 17 — which is why this is a common gotcha in migration guides.

The Security Manager was an old mechanism that let Java applications run sandboxed (like for browser applets) — restricting what code was allowed to do (file access, network, etc.). Modern applications rarely use it (container/OS-level sandboxing has become much more common), and it was costly for the JDK team to maintain.

In Java 17 it was marked "deprecated for removal" (fully removed in later versions) — if a legacy application uses it, you'll get a warning before it stops working, and you'll eventually need to find an alternative.

⚠️Common Mistake: Interviews often ask "what got deprecated in Java 17" — the Security Manager is the most common answer. Applets were already deprecated earlier.