Topic 103 of 119 13. Java New Features Interactive Tutorial

Modern Java Features: Pattern Matching, Sealed Classes, Text Blocks, and Virtual Threads

10 mins read Last updated: 22 Aug 2026 32,819 views

Direct Answer & Core Concept

Modern Java has continued to evolve beyond the features introduced in Java 8. Newer releases have added language features and concurrency improvements that make Java code safer, more expressive, and easier to maintain.

Four important modern Java features are pattern matching, sealed classes, text blocks, and virtual threads. They solve different problems:

  • Pattern matching simplifies type checking and data extraction.

  • Sealed classes restrict which classes can extend or implement a type.

  • Text blocks make multiline strings easier to write and read.

  • Virtual threads provide lightweight concurrency for applications with large numbers of concurrent tasks.


These features were introduced across different Java releases, so it is important to know that they are not all available in Java 8.

Detailed Explanation & Mechanics

What Are Modern Java Features?

Java has evolved through many releases, with newer versions introducing improvements to the language, APIs, and runtime.

For example:

Main.java
Run Online
Java 8↓Lambda ExpressionsStreamsOptionalJava 14–21↓Pattern MatchingRecordsSealed ClassesText BlocksVirtual Threads

The exact release in which a feature became permanent can differ from the release where it first appeared as a preview feature.


Java Feature Version Overview

FeatureFirst Preview / IntroductionFinalized
Pattern Matching for instanceofJava 14Java 16
Text BlocksJava 13Java 15
Sealed ClassesJava 15Java 17
Virtual ThreadsJava 19Java 21
Record PatternsJava 19Java 21
Pattern Matching for switchJava 17Java 21

This distinction is important when working with a specific Java version.

For example, virtual threads are a Java 21 feature, while basic pattern matching with instanceof became permanent in Java 16.


1. Pattern Matching in Java

Pattern matching allows Java to combine a type check with a variable declaration and, in newer versions, extract data from objects.

Before pattern matching, code often looked like:

Main.java
Run Online
Object value = "Hello Java";
if (value instanceof String) {
    String text =(String) value;
    System.out.println(text.length());
}

The type was checked first, and then an explicit cast was required.

With pattern matching for instanceof:

Main.java
Run Online
Object value = "Hello Java";
if (value instanceof String text) {
    System.out.println(text.length());
}

The variable text is declared as part of the type check.


Pattern Matching with instanceof

The general syntax is:

Main.java
Run Online
object instanceof Type variable

Example:

Main.java
Run Online
Object value = 100;
if (value instanceof Integer number) {
    System.out.println(number + 10);
}

Output:

The pattern performs two operations:

Main.java
Run Online
Type Check +Variable Binding

This eliminates a separate cast.


Pattern Variable Scope

The pattern variable is available where the compiler can prove that the pattern matched.

For example:

Main.java
Run Online
Object value = "Java";
if (value instanceof String text) {
    System.out.println(text.toUpperCase());
}

Here text is available inside the if block.

You can also use pattern matching with logical conditions.

Main.java
Run Online
if (value instanceof String text && text.length() > 3) {
    System.out.println(text);
}

The second condition can use text because the first condition established that it is a String.


Pattern Matching Reduces Boilerplate

Traditional code:

Main.java
Run Online
if (obj instanceof Employee) {
    Employee employee =(Employee) obj;
    System.out.println(employee.getName());
}

Modern Java:

Main.java
Run Online
if (obj instanceof Employee employee) {
    System.out.println(employee.getName());
}

The modern version is shorter while preserving the type-safety of the original approach.


Pattern Matching with Null

A useful detail is that:

evaluates to:

Therefore:

Main.java
Run Online
String name = null;
if (name instanceof String text) {
    System.out.println(text);
}

does not enter the block.

Pattern matching does not make null disappear; normal null-related considerations still apply elsewhere in your code.


Pattern Matching with switch

Modern Java also supports pattern matching with switch.

For example, in Java 21:

Main.java
Run Online
static String describe(Object value) {
    return switch (value) {
        case Integer number -> "Integer: " + number;
        case String text -> "String: " + text;
        case Double number -> "Double: " + number;
        default -> "Unknown type";
    };
}

The switch can inspect the type and bind a variable to the matched value.

This can be much cleaner than a long chain of instanceof checks.


Pattern Matching with Records

Modern Java also supports record patterns.

Consider:

Main.java
Run Online
record Point(int x, int y) {
}

Traditional checking might look like:

Main.java
Run Online
if (object instanceof Point point) {
    int x = point.x();
    int y = point.y();
    System.out.println(x + y);
}

With a record pattern:

Main.java
Run Online
if (object instanceof Point(int x, int y)) {
    System.out.println(x + y);
}

The record components are extracted directly from the pattern.

Record patterns became a permanent feature in Java 21.


2. Sealed Classes

Sealed classes allow developers to control which classes can extend or implement a particular type.

Before sealed classes, a class could be extended by other classes unless it was declared final or otherwise restricted by design.

For example:

Main.java
Run Online
abstract class Payment {
}

Potentially many unrelated classes could extend it.

A sealed class allows the permitted subclasses to be explicitly declared.

Main.java
Run Online
public sealed class Payment permits CreditCardPayment, CashPayment {
}

Now only the permitted classes can directly extend Payment.


Creating a Sealed Class

Example:

Main.java
Run Online
public sealed class Shape permits Circle, Rectangle {
}

Then:

Main.java
Run Online
final class Circle extends Shape {
}final class Rectangle extends Shape {
}

The compiler knows the permitted hierarchy.

This gives developers more control over domain models.


Why Use Sealed Classes?

Sealed classes are useful when a type should have a controlled set of implementations.

For example:

Architecture Diagram
Payment
 ├── CreditCardPayment
 ├── CashPayment
 └── BankTransferPayment

Instead of allowing unknown implementations everywhere, the hierarchy explicitly defines the permitted direct subclasses.

This is useful for:

  • Domain models

  • State machines

  • Business rules

  • Expression trees

  • API models

  • Compiler-like structures

  • Pattern matching


Rules for Sealed Class Subclasses

A direct subclass of a sealed class must specify how it continues the hierarchy.

It must be declared as one of:

For example:

Main.java
Run Online
public sealed class Payment permits CreditCardPayment, OnlinePayment {
}

A final subclass:

Main.java
Run Online
final class CreditCardPayment extends Payment {
}

A sealed subclass:

Main.java
Run Online
sealed class OnlinePayment extends Payment permits UpiPayment, WalletPayment {
}

Or a non-sealed subclass:

Main.java
Run Online
non-sealed class OnlinePayment extends Payment {
}

final, sealed, and non-sealed

These three modifiers communicate different inheritance rules.

final

No further subclassing is allowed.

Main.java
Run Online
final class CreditCardPayment extends Payment {
}

sealed

The subclass itself restricts which classes can extend it.

Main.java
Run Online
sealed class OnlinePayment extends Payment permits UpiPayment {
}

non-sealed

The restriction is opened again.

Main.java
Run Online
non-sealed class OnlinePayment extends Payment {
}

Conceptually:

Main.java
Run Online
sealed↓Controlled hierarchyfinal↓Hierarchy stopsnon-sealed↓Hierarchy opens

Sealed Interfaces

Sealed types are not limited to classes.

You can create a sealed interface:

Main.java
Run Online
public sealed interface Payment permits CardPayment, CashPayment {
}

Then:

Main.java
Run Online
final class CardPayment implements Payment {
}final class CashPayment implements Payment {
}

This is useful when the abstraction should be an interface rather than a base class.


Sealed Classes and Pattern Matching

Sealed classes work particularly well with pattern matching.

Consider:

Main.java
Run Online
sealed interface Shape permits Circle, Rectangle {
}record Circle(double radius) implements Shape {
}record Rectangle(double width, double height) implements Shape {
}

A modern switch can process the known alternatives:

Main.java
Run Online
static double area(Shape shape) {
    return switch (shape) {
        case Circle c -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.width() * r.height();
    };
}

Because the compiler knows the permitted hierarchy, the default case may not be necessary when all permitted alternatives are covered.

This is one of the strongest combinations of modern Java features:

Main.java
Run Online
Sealed Types +Pattern Matching +Records

3. Text Blocks

Text blocks provide a convenient way to write multiline string literals.

They became a permanent feature in Java 15.

Before text blocks, multiline strings could become difficult to read:

Main.java
Run Online
String json = "{\n" + "  \"name\": \"Tanmoy\",\n" + "  \"age\": 35\n" + "}";

A text block is much easier to read:

Main.java
Run Online
String json = """
        {
          "name": "Tanmoy",
          "age": 35
        }
        """;

The """ delimiters indicate a text block.


Basic Text Block Syntax

A text block begins with three double quotes:

and ends with three double quotes:

Example:

Main.java
Run Online
String message = """
        Hello
        Welcome to Java
        Modern Java is powerful
        """;

The resulting string contains multiple lines.


Text Blocks for JSON

Text blocks are especially useful for JSON examples.

Main.java
Run Online
String json = """
        {
            "id": 101,
            "name": "Tanmoy",
            "active": true
        }
        """;

This is considerably easier to maintain than concatenating many strings.


Text Blocks for SQL

Text blocks can also make multiline SQL easier to read:

Main.java
Run Online
String sql = """
        SELECT id, name, email
        FROM users
        WHERE status = 'ACTIVE'
        ORDER BY name
        """;

The SQL structure remains visually similar to the actual query.


Text Blocks and Escape Sequences

Text blocks still support Java escape sequences.

For example:

Main.java
Run Online
String text = """
        Line one
        Line two
        """;

represents a multiline string.

You can also explicitly use:

Main.java
Run Online
String text = """
        Hello\nWorld
        """;

when you specifically need a newline escape.


Text Blocks and Formatting

Text blocks can be combined with formatted().

Main.java
Run Online
String name = "Tanmoy";
int age = 35;
String message = """
        Name: %s
        Age: %d
        """.formatted(name, age);

Output:

This can be convenient for generating readable multiline content.


Text Blocks Do Not Create a New Data Type

A text block is still a Java String.

For example:

Main.java
Run Online
String message = """
        Hello Java
        """;

The type remains:

Text blocks simply provide a more readable syntax for multiline string literals.


4. Virtual Threads

Virtual threads are one of the most important modern Java concurrency features.

They became a permanent feature in Java 21.

Traditional Java threads are commonly associated with operating-system threads. Creating very large numbers of platform threads can consume significant resources.

Virtual threads are lightweight threads managed by the Java runtime and are designed to make high-concurrency applications easier to write.


Traditional Platform Thread

A traditional thread can be created with:

Main.java
Run Online
Thread thread = new Thread(() -> {
    System.out.println("Hello");
});
thread.start();

This creates a platform thread.

For a small number of tasks, this is perfectly reasonable.

The challenge appears when an application needs very large numbers of concurrent tasks, particularly tasks that spend significant time waiting for I/O.


Creating a Virtual Thread

Java 21 provides:

Main.java
Run Online
Thread.startVirtualThread(() -> {
    System.out.println("Hello from virtual thread");
});

This starts a virtual thread.

Another approach is:

Main.java
Run Online
Thread thread = Thread.ofVirtual() .start(() -> {
    System.out.println( "Running in virtual thread");
});

Virtual Thread Executor

A convenient way to execute many independent tasks is:

Main.java
Run Online
try(ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0;
    i < 1000;
    i++) {
        executor.submit(() -> {
            System.out.println( "Processing request");
        });
    }
}

This creates a virtual thread for each submitted task.

The executor is especially useful for applications where each task spends time waiting on operations such as network or database I/O.


Why Virtual Threads Matter

Suppose a web application receives thousands of requests.

A request may perform:

Main.java
Run Online
Request↓Database query↓Wait↓External API↓Wait↓Return response

During the waiting periods, the task may not actively use CPU.

Virtual threads are designed to make it practical to represent such concurrent tasks with ordinary blocking-style code while allowing the runtime to manage the underlying resources efficiently.


Virtual Threads and Blocking I/O

Virtual threads are particularly useful for workloads involving blocking operations.

Examples include:

  • HTTP requests

  • Database calls

  • File operations

  • Network communication

  • Service-to-service calls

A virtual thread can be suspended while waiting, allowing the runtime to use the underlying platform resources for other work.

This is one reason virtual threads can support very high concurrency.


Virtual Threads Are Not Faster Threads

A common misunderstanding is:

Virtual threads make every application faster.

That is not their purpose.

Virtual threads primarily improve concurrency and scalability for suitable workloads.

They do not make CPU calculations inherently faster.

For example, a task performing heavy CPU computation still requires CPU resources:

Main.java
Run Online
for (long i = 0;
i < 1_000_000_000L;
i++) {
    // CPU-intensive work
}

Creating thousands of virtual threads does not magically provide thousands of CPU cores.


Virtual Threads vs Platform Threads

FeaturePlatform ThreadVirtual Thread
Managed byOS/JVMJVM
Resource costHigherMuch lower
Large concurrencyMore limitedDesigned for very high counts
Blocking I/OCan be expensive at scaleWell suited
CPU-bound workSuitableDoes not remove CPU limits
Java versionLong-standingJava 21
Programming modelThread-basedThread-based

The goal is not to replace every platform thread with a virtual thread blindly.

The workload should determine the choice.


Virtual Threads and ExecutorService

Traditional code might use a fixed thread pool:

Main.java
Run Online
ExecutorService executor = Executors.newFixedThreadPool(100);

A virtual-thread-per-task executor can instead be used:

Main.java
Run Online
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

This is particularly useful when you have many independent tasks that may block.


Virtual Threads and Thread Pools

A key idea is that virtual threads are cheap enough that you generally do not use a small pool of virtual threads in the same way you would use a small pool of platform threads.

For example:

Main.java
Run Online
Executors.newVirtualThreadPerTaskExecutor()

creates a new virtual thread for each submitted task.

The runtime manages the scheduling of these virtual threads onto available platform threads.


Virtual Threads and ThreadLocal

Existing Java applications sometimes rely heavily on ThreadLocal.

Virtual threads can create very large numbers of threads, so storing large amounts of data in thread-local variables can become expensive if done carelessly.

When using virtual threads, keep per-thread state small and consider whether ThreadLocal is really needed.


Virtual Threads and Synchronization

Virtual threads support Java synchronization mechanisms, but applications should still avoid unnecessarily long synchronized sections.

For example:

Main.java
Run Online
synchronized (lock) {
    performOperation();
}

If the operation involves blocking or expensive work while holding a monitor, it may reduce concurrency.

Good synchronization design remains important even when using virtual threads.


Virtual Threads and Structured Concurrency

Virtual threads are also closely related to modern concurrency design ideas such as structured concurrency.

Structured concurrency treats related concurrent tasks as a logical unit with a clear lifetime.

The goal is to make concurrent code easier to reason about by connecting child tasks to the operation that created them.

Structured concurrency has evolved through preview APIs across Java releases, so its exact API status depends on the Java version being used.


Combining Modern Java Features

The most interesting part of modern Java is that these features can work together.

Consider a sealed hierarchy:

Main.java
Run Online
sealed interface Payment permits CardPayment, CashPayment {
}

with records:

Main.java
Run Online
record CardPayment(double amount) implements Payment {
}record CashPayment(double amount) implements Payment {
}

Pattern matching can process them:

Main.java
Run Online
static String describe(Payment payment) {
    return switch (payment) {
        case CardPayment p -> "Card: " + p.amount();
        case CashPayment p -> "Cash: " + p.amount();
    };
}

A text block can be used for output:

Main.java
Run Online
String message = """
        Payment processed
        Type: %s
        """.formatted( describe(payment));

And a virtual thread could process the operation concurrently:

Main.java
Run Online
Thread.startVirtualThread(() -> {
    System.out.println(message);
});

This example demonstrates how modern Java features can complement one another.


Modern Java Design Example

Imagine an order-processing application.

The application might model order states using a sealed interface:

Main.java
Run Online
sealed interface OrderStatus permits Pending, Paid, Cancelled {
}

Records represent the states:

Main.java
Run Online
record Pending() implements OrderStatus {
}record Paid(double amount) implements OrderStatus {
}record Cancelled(String reason) implements OrderStatus {
}

Pattern matching can process them:

Main.java
Run Online
static String message(OrderStatus status) {
    return switch (status) {
        case Pending p -> "Order is pending";
        case Paid p -> "Order paid: " + p.amount();
        case Cancelled c -> "Order cancelled: " + c.reason();
    };
}

A text block can format the result:

Main.java
Run Online
String output = """
        ORDER STATUS
        ------------
        %s
        """.formatted(message(status));

A virtual thread can process multiple independent orders:

Main.java
Run Online
Thread.startVirtualThread(() -> {
    System.out.println(output);
});

This is a good example of modern Java's direction:

Main.java
Run Online
Records +Sealed Types +Pattern Matching +Text Blocks +Virtual Threads

When Should You Use Pattern Matching?

Pattern matching is useful when:

  • You frequently check an object's type

  • You want to avoid explicit casts

  • You process multiple known object types

  • You use sealed hierarchies

  • You work with records

  • You need clearer switch logic

It is especially valuable when combined with sealed classes and records.


When Should You Use Sealed Classes?

Sealed classes are useful when:

  • A domain has a known set of alternatives

  • You want controlled inheritance

  • You want exhaustive pattern matching

  • You are designing state hierarchies

  • You want to prevent unknown implementations

For example:

Architecture Diagram
Payment
 ├── Card
 ├── Cash
 └── BankTransfer

is a good candidate when these alternatives are intentionally closed.


When Should You Use Text Blocks?

Text blocks are useful for:

  • JSON

  • XML

  • SQL

  • HTML

  • Templates

  • Multiline messages

  • Documentation examples

They improve source-code readability when the value naturally spans multiple lines.


When Should You Use Virtual Threads?

Virtual threads are particularly useful for applications with many concurrent tasks that spend substantial time waiting on I/O.

Typical examples include:

Main.java
Run Online
Web requestsAPI callsDatabase operationsFile operationsNetwork communication

They are less about reducing CPU execution time and more about making high concurrency practical with straightforward thread-per-task programming.


Common Mistakes

Mistake 1: Treating Virtual Threads as a CPU Speed Boost

Virtual threads do not increase the number of CPU cores.

Use them primarily for high-concurrency workloads, especially blocking I/O.


Mistake 2: Using Pattern Matching Without Understanding Scope

A pattern variable only exists where the compiler can establish that the pattern matched.

For example:

Main.java
Run Online
if (obj instanceof String text) {
    System.out.println(text);
}

text is not generally available outside the matching scope.


Mistake 3: Assuming Sealed Means Immutable

A sealed class controls inheritance.

It does not automatically make objects immutable.

These are separate concepts:

Main.java
Run Online
sealed→ Controls inheritanceimmutable→ Controls state changes

Mistake 4: Thinking Text Blocks Are Templates

A text block is still a string literal.

It does not automatically perform variable substitution.

For example:

Main.java
Run Online
String name = "Tanmoy";
String text = """
        Hello %s
        """.formatted(name);

Here formatted() performs the formatting.


Mistake 5: Creating Thousands of Platform Threads Instead

Virtual threads exist specifically to make large numbers of concurrent tasks more practical.

Do not assume that simply increasing the number of platform threads provides the same benefits.


Modern Java Features at a Glance

FeatureMain Problem Solved
Pattern MatchingSimplifies type checks and extraction
Sealed ClassesControls inheritance
Text BlocksSimplifies multiline strings
Virtual ThreadsEnables lightweight high concurrency

A useful mental model is:

Main.java
Run Online
Pattern Matching→ Cleaner logicSealed Classes→ Safer hierarchiesText Blocks→ Cleaner textVirtual Threads→ Scalable concurrency

Frequently Asked Questions

Which Java version introduced virtual threads?

Virtual threads were introduced as a preview feature in Java 19 and became a permanent feature in Java 21.

Which Java version finalized sealed classes?

Sealed classes became a permanent feature in Java 17.

Which Java version finalized text blocks?

Text blocks became a permanent feature in Java 15.

Which Java version finalized pattern matching for instanceof?

Pattern matching for instanceof became a permanent feature in Java 16.

Are virtual threads faster than normal threads?

Not simply. Their main benefit is that they are lightweight and can support very high numbers of concurrent tasks, especially when tasks spend time waiting on I/O.

Can virtual threads run CPU-intensive tasks?

They can, but virtual threads do not eliminate CPU limitations. CPU-bound workloads still depend on available processor resources.

What is a sealed class?

A sealed class restricts which classes are allowed to directly extend it.

Can a sealed class have a non-sealed subclass?

Yes.

Main.java
Run Online
non-sealed class OnlinePayment extends Payment {
}

This reopens inheritance below that point.

What is pattern matching in Java?

Pattern matching allows Java to combine type checking with variable binding and, in newer features, data extraction.

What are text blocks?

Text blocks are multiline string literals introduced to make long strings such as JSON, SQL, XML, and HTML easier to write and read.

Can records work with pattern matching?

Yes. Modern Java supports record patterns that can extract record components directly.

Can sealed classes and pattern matching be used together?

Yes. This is one of the useful combinations in modern Java because the compiler can reason about the permitted type hierarchy.

Suggested Graphics

Graphic 1 — Modern Java Timeline

Architecture Diagram
Java 14 ── Pattern Matching Preview
↓
Java 15 ── Text Blocks Final
↓
Java 16 ── instanceof Pattern Matching Final
↓
Java 17 ── Sealed Classes Final
↓
Java 21 ── Virtual Threads + Record Patterns + Pattern switch Final

Graphic 2 — Pattern Matching

Main.java
Run Online
TraditionalObject↓instanceof↓Cast↓Use variableModernObject↓instanceof Type value↓Use value

Graphic 3 — Sealed Class Hierarchy

Main.java
Run Online
Payment / \ / \ Card Cash final final

Graphic 4 — Virtual Thread Architecture

Main.java
Run Online
Thousands of Tasks / | \↓↓↓Virtual Virtual Virtual Thread Thread Thread \ | / \ | / JVM Scheduler↓Platform Threads↓CPU

Graphic 5 — Modern Java Feature Map

Architecture Diagram
              MODERN JAVA
                   |
     ┌─────────────┼─────────────┐
↓

↓

↓
Pattern       Sealed         Text
 Matching      Classes       Blocks
     |             |             |
 Cleaner       Controlled     Readable
 Type Logic    Hierarchy      Multiline
                   |
↓
Virtual Threads
                   |
↓
High Concurrency

Runnable Code Example: Modern Java Features

Java 17+
Main.java
Run Online
public class ModernJavaDemo {
    // Sealed class hierarchy
    sealed interface Payment permits CreditCard, Crypto {
    } record CreditCard(String pan) implements Payment {
    } record Crypto(String walletAddress) implements Payment {
    } public static String handlePayment(Payment p) {
        // Pattern Matching for switch (Java 21)
        return switch (p) {
            case CreditCard c -> "Charged Credit Card: " + c.pan();
            case Crypto k -> "Transferred to Wallet: " + k.walletAddress();
        };
    } public static void main(String[] args) {
        Payment payment = new CreditCard("4111-XXXX-XXXX-1111");
        System.out.println(handlePayment(payment));
    }
}

Common Java Technical Interview Questions & Answers

Interview Prep
Q1

What problem do Java 21 Virtual Threads solve?

They eliminate the 1:1 constraint between Java threads and expensive OS kernel threads, enabling high-throughput I/O servers without reactive complexity.
Q2

What is the purpose of Sealed Classes in Java?

To restrict class hierarchies to a known, closed set of subtypes, enabling exhaustive compile-time pattern matching.

Key Takeaways, Best Practices & Clean Code Rules

Java Best Practice & Production Rules

Adopt Java 21 Virtual Threads via Executors.newVirtualThreadPerTaskExecutor() for scalable backend microservices.