JAVA 17 Features Interview Q & A

  1. List out some Java 17 features 
  2. What are Records in Java 17 ?
  3. Can records have custom constructors, methods, or fields?
  4. What are Sealed Classes/Interfaces in Java 17?
  5. Why use sealed classes?
  6. What is Pattern Matching for instanceof (enhanced in Java 16/17)?
  7. What is Pattern Matching for switch (preview in Java 17, stable later)?
  8. What are Text Blocks in Java 15/17? Advantages of text blocks.
  9. Other Java 17 features (helpful NullPointerExceptions, etc.). 
  10. Records vs POJO, sealed hierarchies design, pattern matching vs if-else, text blocks for configuration, when to use each feature.

List out some Java 17 features

  1. Records 
  2. Sealed classes
  3. Pattern Matching for Switch 
  4. Pattern Matching for instanceof
  5. Text Blocks 

What are Records in Java 17 ?

=> Records are immutable data carrier classes to reduce boilerplate code for DTOs (DTO - Data Transfer Object) - JEP 395

Traditional Class vs Record

Traditional Class (POJO/DTO)

Record (Java 16+, stable in 17)

General purpose class for holding data (DTO, entitty, etc)

Immutable data carrier class to reduce boiler plate code (DTOs, API models, Configuration objects)

Verbose – Need fields, constructor, getter, setter, equals, hashcode, toString

Concise: Single line is enoough. Compiler automatically generates other stuff

We need to set the fields final explicitly if we need them as final

Components are implicitly private final by default

No-arg constructor is automatically generated implicitly by default


We need to write paremeterized constructor explictly if needed

No-arg constructor wont be generated, we need to write explictly if we need

Parameterized constructor is automatically generated by default 
implicitly (by matching header components). This constructor is called as Canonical constructor

Traditional getters (eg. getName(), getAge())


Traditional setters (eg. setName(), setAge())

Here, the getter names are different. (Eg. name() instead of getName()

No setters allowed

equals(), hascode(), toString() - We must implement manually if needed

equals(), hascode(), toString() - Automatically generated (based on all components)

Validation can be done via constructor or setter methods

Valication can be done via constructor

Custom instance/static methods we can have

Here also, custom instance/static methods we can have

Can extend other classes and extended by other classes

Cannot extend any class and also cannot be extended by any classes

Use case : Use traditional Class when we need mutability, inheritance or complex logic

Use case : Use Record when we need immutable, concise code (Most DTOs, API models, configuration objects)

Example :

Consider we try to create a Class to match the behavior of Record 

//Traditional Class 

public final class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String name() { return name; }
    public int age() { return age; }

    @Override
    public boolean equals(Object o) { ... }
    @Override
    public int hashCode() { ... }
    @Override
    public String toString() { return "Person[name=" + name + ", age=" + age + "]"; }
}


//Record

public record Person(String name, int age) { }

The above single line is equivalent to writing a full immutable class with:

private final String name;
private final int age;
Constructor public Person(String name, int age)
public String name()
public int age()
equals(), hashCode(), toString()

=> Records are not completely restricted, we can have compact constructor, instance methods, static fields/methods

Compact constructor : 

public record Person(String name, int age) {
    public Person {
        if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
        if (name == null || name.isBlank()) throw new IllegalArgumentException("Name required");
    }
}

Instance methods : 

public record Person(String name, int age) {
    public boolean isAdult() {
        return age >= 18;
    }
}

Static Fields and Methods : 

public record Person(String name, int age) {
    public static final int ADULT_AGE = 18;

    public static Person of(String name) {
        return new Person(name, 0);
    }

What is Canonical constructor in the context of Java Records ? 

=> In Records parameterized constructor is automatically generated by default implicitly (by matching header components). This constructor is called as Canonical constructor

=> It is called "canonical" because it is the standard, primary constructor for the record

Example : 

public record Person(String name, int age) { }

The compiler automatically generates this canonical constructor:

public Person(String name, int age) {
    this.name = name;
    this.age = age;
}

Parameters match the components exactly (type and order).

We can call it like 

Person p = new Person("Virat", 36);

What Happens If You Add Your Own Constructor to the Record ?

=> We can add additional constructors, but the canonical one is still generated unless we explicitly override it.

=> We can write a compact constructor to add validation — it implicitly calls the canonical one: 

=> Example :  

1. Compact Canonical Constructor (Most Common Customization)

public record Person(String name, int age) {
    public Person {  // Compact form — no parameter list
        if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
        if (name == null || name.isBlank()) throw new IllegalArgumentException("Name required");
    }

How we can call the compact constructor ?

=> We cannot call compact constructor directly 

new Person ()  // Compile error — not how you instantiate

=> How to Call It (Correct Way) : 

// Normal constructor call — this triggers the compact constructor validation
Person p1 = new Person("Virat", 36);     // Valid → OK
Person p2 = new Person("Sachin", -5);    // Throws IllegalArgumentException: Age cannot be negative
Person p3 = new Person(null, 30);       // Throws IllegalArgumentException: Name required
Person p4 = new Person("", 30);         // Throws IllegalArgumentException: Name required (isBlank())

=> What Happens Behind the Scenes :

The compiler expands it to something like:

public Person(String name, int age) {
    // Compact constructor code runs first
    if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
    if (name == null || name.isBlank()) throw new IllegalArgumentException("Name required");

    // Then assigns to fields
    this.name = name;
    this.age = age;
}

So the calling code remains the same — new Person("name", age).  

2. Explicit Canonical Constructor (Full Override)

public record Person(String name, int age) {
    public Person(String name, int age) {  // Explicit canonical
        this.name = Objects.requireNonNull(name, "Name cannot be null");
        if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
        this.age = age;
    }
}

Key points about Canonical Constructor 

=> Canonical constructor always exists in record (compiler generates it if we don't).
=> Matches header exactly (same parameters, order, types).
=> Used for immutability — assigns to final fields.
=> Compact form is preferred for validation — cleaner.
=> No default no-arg constructor is generated for Record. The default constructor is Canonical constructor (parameterized version only) for Records 

How to Add a No-Arg Constructor to the Record if we need ?  

=> We can manually add an overloaded constructor (but we must call the canonical constructor using this(...)):

=> Example : 

public record Person(String name, int age) {
    // Custom no-arg constructor with default values
    public Person() {
        this("Unknown", 0);  // Must delegate to canonical constructor
    }
}

Now, we can use Person p1 = new Person();           // Uses custom constructor → "Unknown", 0
Person p2 = new Person("Virat", 36); // Uses canonical constructor

NOTE : 
=> Avoid no-arg constructors in Records as much as possible 
=> Prefer factory methods instead :
public static Person unknown() {
    return new Person("Unknown", 0);
}
=> Use default values only when it makes sense (e.g., configuration records). 

Can records have custom constructors, methods, or fields? ? 

=> Yes, we can have custom constructors, instance methods, static field and methods in Record 
=> In records, we use compact constructor for validation   

What are Sealed Classes/Interfaces in Java 17?

=> Sealed Classes/Intefaces restrict which classes can extend/implement them using the permits clause. The permitted classes are called as subclasses 

=> In other words, we can say, only permitted subclasses are allowed to extend/implement the sealed Class/Interface 

=> Permitted subclasses must be in the same module (or same package if no module) and must explicitly state their further extension status as any one of the following 

        final - cannot be extended further
        sealed - can be extended but only by listed permits
        non-sealed - open for extension by anyone 

=> Remember the key rules for Record :
        "Records are implicitly final by default" 
        "Records cannot extend and they cannot be extended by any"

=> Permitted Records can implement the sealed interface and those permitted Records need not to state their relation explicitly because, Records are implicitly final always.  

=> Record Has Nothing to Do with Sealed Class because, Record cannot extend Any Class

=> Records cannot extend any class (not even Object explicitly — they implicitly extend java.lang.Record). 

=> Code example : 

// Sealed class — only permitted subclasses can extend it
public sealed class Shape 
    permits Circle, Rectangle, Triangle { }  // List of permitted subclasses

// Permitted subclasses must be in same module (or package if no module)
final class Circle extends Shape { }
non-sealed class Rectangle extends Shape { }  // Can be extended further
sealed class Triangle extends Shape permits Equilateral, Isosceles { }

// Example of non-sealed allowing further extension
class Square extends Rectangle { }  // Allowed because Rectangle is non-sealed

Why Were Sealed Classes Introduced?

=> Before sealed classes, inheritance was completely open (any class could extend/implement) or completely closed (final). There was no middle ground. 

=> Sealed classes provide a middle ground ie. we can define exactly which classes are permitted to extend/implement

Benefits of Sealed Classes  

=> Provides API safety ie prevents unwanted extensions

=> Enables compiler-checked exhaustiveness in pattern matching 

Limitations of Sealed Classes 

=> Permitted classes must be known at compile time

=> Permitted classes must be in the same module (or package)

Example code with Modeling Closed Hierarchies and Exhaustive Pattern Matching (with switch) 

package java8;

// Modeling Closed Hierarchies
sealed interface PaymentStatus 
    permits PaymentStatus.Success, PaymentStatus.Failure, PaymentStatus.Pending { }

record Success(String txId) implements PaymentStatus { }
record Failure(String reason) implements PaymentStatus { }
record Pending() implements PaymentStatus { }

public class Java17Exercises {

        //Exhaustive Pattern Matching (with switch) 
        public static String describe(PaymentStatus status) {
        return switch (status) {
            case Success s -> "Success: " + s.txId();
            case Failure f -> "Failed: " + f.reason();
            case Pending p -> "Pending";
            // No default needed — compiler knows all cases covered!
        };
    }

    public static void main(String[] args) {
        // Test cases
        PaymentStatus success = new Success("TXN123");
        PaymentStatus failure = new Failure("Insufficient funds");
        PaymentStatus pending = new Pending();

        System.out.println(describe(success));  // Success: TXN123
        System.out.println(describe(failure));  // Failed: Insufficient funds
        System.out.println(describe(pending));   // Pending
    }
}

What is Exhaustive Pattern Matching with switch and Why It's Called "Exhaustive"?         

=> The sealed interface/class explicitly lists all permitted subtypes (via permits).
=> The compiler knows the complete set of possible values.
=> So when you handle every permitted subtype in the switch, the compiler can guarantee exhaustiveness ie nothing is missing.
=> No need for a default case — if you miss one, compile error! ie no runtime surprise
=> The compiler checking all the possible list, so thats why its called as Exhaustive 
=> This compile time check for permits-sealed class is called as Exhaustive Pattern Matching 

//Exhaustive Pattern Matching (with switch) 
        public static String describe(PaymentStatus status) {
        return switch (status) {
            case Success s -> "Success: " + s.txId();
            case Failure f -> "Failed: " + f.reason();
            case Pending p -> "Pending";
            // No default needed — compiler knows all cases covered!
        };
    } 

What is Pattern Matching for instanceof (enhanced in Java 16/17)?

=> Before pattern matching (Pre-Java14), we used to write code like this : 

Object obj = "Hello Java";

if (obj instanceof String) {
    String s = (String) obj;  // Explicit cast needed
    System.out.println(s.toUpperCase());
}

=> The above code has some repetitive ie checking the type in if condition, and then casting to the same type inside if block. ie boiler plate code and also has the risk of ClassCastException

=> Pattern Matching for instanceof feature combines the type-check and variable declaration into a single expression. It reduces the boiler plate code and also eliminates the risk of ClassCastException 

=> Code with pattern matching for instanceof (Java16+)

Object obj = "Hello Java";

if (obj instanceof String s) {  // Type check + variable declaration in one step
    System.out.println(s.toUpperCase());  // s is already String — no cast!
}

=> In the above code,
        String s is a pattern variable
        If obj is a String, it is automatically cast to s and available in the block
        Scope of s: Only inside the if block (or else if used)

More examples : 

Code with pattern matching for instanceof (Java16+) - with custom class 

class Animal { }
class Dog extends Animal {
    void bark() { System.out.println("Woof"); }
}

Animal animal = new Dog();

if (animal instanceof Dog dog) {
    dog.bark();  // No cast needed
}

Code with pattern matching for instanceof (Java16+) - with else block

if (obj instanceof String s) {
    System.out.println("String: " + s.length());
} else {
    System.out.println("Not a string");
}
// s not accessible here

Code with pattern matching for instanceof (Java16+) - nested pattern matching

if (obj instanceof List<?> list && !list.isEmpty() && list.get(0) instanceof String s) {
    System.out.println("First element: " + s);
}

Example where ClassCastException Occurs (if we don't use pattern matching for instanceof feature) for understanding purpose

Now the Dangerous Change (common bug):

import java.util.*;

public class CastExample {
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        list.add("Hello");
        list.add(new StringBuilder("World"));

        for (Object obj : list) {
            if (obj instanceof CharSequence) {  // CharSequence is superinterface of String
       
     String s = (String) obj;        // ← Now risky cast!
     
       System.out.println(s.toUpperCase());
            }
        }
    }
}

"Hello" is both String and CharSequence → check passes → cast safe.
But if list contains a different CharSequence (e.g., StringBuilder):

list.add(new StringBuilder("World"));

StringBuilder is CharSequence → instanceof true.

But (String) cast on StringBuilder → ClassCastException at runtime:

java.lang.ClassCastException: java.lang.StringBuilder cannot be cast to java.lang.String 

Why Pattern Matching (Java 16+) Eliminates This Risk : 

if (obj instanceof String s) {
    System.out.println(s.toUpperCase());  // s is safely String
}

The type check and binding happen atomically.
Compiler guarantees s is String if the condition is true.
No manual cast → no possibility of ClassCastException. 

What is Pattern Matching for switch (preview in Java 17, stable later)?

=> Pattern Matching for switch introduced as a preview feature in Java 17 and made stable in Java 21 

=> Traditional Switch expression was limited to constant values (int, String, enum, etc) 

=> This pattern matching feature extends the Switch to match patterns (types, records, guards) 

=> In the traditional Switch, handling objects required instanceof if conditions 

// Old way — verbose
if (obj instanceof Circle c) {
    return "Circle radius " + c.radius();
} else if (obj instanceof Rectangle r) {
    return "Rectangle " + r.width() + "x" + r.height();
} else {
    return "Unknown";
}

In the old way, 
    => No exhaustive check (miss cases -> runtime bugs)
    => Verbose casting and type checks
    => No deconstruction (e.g., extract fields from records).

Basic syntax for the pattern matching with Switch

switch (expression) {
    case Type pattern -> result;
    case Type varName -> result;
    case Type varName when condition -> result;  // Guarded pattern
    default -> result;
}  

Examples :

Basic Type Patterns  

Object obj = "Hello";

String result = switch (obj) {
    case String s -> s.toUpperCase();
    case Integer i -> "Number: " + i;
    default -> "Unknown";
};
// result = "HELLO"

With Sealed Interface (Exhaustive – No Default)

sealed interface Shape permits Circle, Rectangle { }
record Circle(double radius) implements Shape { }
record Rectangle(double width, double height) implements Shape { }

String describe(Shape shape) {
    return switch (shape) {
        case Circle c -> "Circle r=" + c.radius();
        case Rectangle r -> "Rectangle " + r.width() + "x" + r.height();
        // No default — compiler verifies exhaustiveness
    };

Record Patterns (Java 21) (Record Deconstruction)

record Point(int x, int y) { }

String describe(Object obj) {
    return switch (obj) {
        case Point(int x, int y) -> "Point (" + x + "," + y + ")";
        case Integer i -> "Number " + i;
        default -> "Unknown";
    };
}

Guarded Patterns (when)

String result = switch (shape) {
    case Circle c when c.radius() > 10 -> "Large circle";
    case Circle c -> "Small circle";
    case Rectangle r when r.width() == r.height() -> "Square";
    case Rectangle r -> "Rectangle";
};

Can pattern matching switch be used with permitted classes, not just records?

Yes — it works with any permitted subtypes of a sealed class/interface, including regular classes, records, or enums. The compiler ensures exhaustiveness

Benefits of this pattern matching with Switch

=> No instanceof + casting.
=> Exhaustiveness check (compile error if missing case).
=> Readable ie Intent clear in one place.
=> Deconstruction: Extract fields directly (records).

What are Text Blocks in Java 15/17? Advantages of text blocks.

=> Text blocks are multi-line string literals using triple quotes """

=>  They eliminate escape sequences for new lines/quotes and preserve formatting 

=> Incidental leading white space gets trimmed automatically 

 Old Way (Pre-Java 15)  

 String html = "<html>\n" +
              "    <body>\n" +
              "        <p>Hello, world!</p>\n" +
              "    </body>\n" +
              "</html>\n";

=> Lots of " and + concatenation — verbose and noisy.
=> Manual \n for line breaks.
=> Easy to misalign indentation.
=> Hard to read and maintain (especially for JSON/SQL).

Text Blocks (Java 15+)

String html = """
    <html>
        <body>
            <p>Hello, world!</p>
        </body>
    </html>
    """;

=> Clean, readable, preserves formatting.
=> No escape sequences needed for newlines or quotes inside.
=> What you see is what you get
=> Delimiter: """ (three double quotes) to open and close. 
=> Incidental whitespace (common leading whitespace) is automatically removed.
=> Example : 
String text = """
      Line 1
        Line 2
      Line 3
      """;
// Result: 
// "Line 1\n  Line 2\nLine 3\n"

=> Trailing whitespace : preserved (not trimitted automatically)

=> Use \" only if you need literal " inside 

=> No need to escape new lines 

=> Always uses line terminators (\n) internally regardless of platform

=> Examples : 

JSON 

String json = """
    {
        "name": "Sachin",
        "role": "Batsman",
        "runs": 18426
    }
    """;

SQL Query 

String query = """
    SELECT id, name, salary
    FROM employees
    WHERE department = 'IT'
    ORDER BY salary DESC
    """;

HTML

String html = """
    <!DOCTYPE html>
    <html>
    <head><title>Hello</title></head>
    <body><h1>Welcome</h1></body>
    </html>
    """; 

 Best Practices

=> Use text blocks for any multi-line embedded text (JSON, SQL, HTML, scripts).
=> Align the closing """ with the left-most content for predictable trimming.
=> Avoid mixing with string concatenation unless necessary.
=> Combine with formatted() for dynamic values (Java 15+):
String message = """
    Hello %s,
    Your age is %d.
    """.formatted("Virat", 36);

 

 

 

 

 


_______________________________________________________________________________

Practice the following coding exercises

  1. Create record Employee(String name, int salary).
  2. Add compact constructor for validation (salary > 0).
  3. Add instance method to record (e.g., isHighEarner()).
  4. Sealed class Vehicle with permitted Car, Bike (final).
  5. Non-sealed class in hierarchy.
  6. Pattern matching instanceof with String, Integer, Employee.
  7. Deconstruct record in instanceof.
  8. Text block for JSON string.
  9. Text block for SQL query.
  10. Text block with indentation handling.
  11. Pattern matching in switch (enable preview in IntelliJ: Project Settings → Compiler → Java compiler → --enable-preview).
  12. Guarded pattern in switch.
  13. Trigger enhanced NPE and read message.
  14. Record with custom equals/hashCode (override if needed).
  15. Sealed interface with multiple implementations.
  16. 16–20. Mixed: Record + pattern matching, text block formatting, sealed hierarchy with switch, etc.