Java 8↓Lambda ExpressionsStreamsOptionalJava 14–21↓Pattern MatchingRecordsSealed ClassesText BlocksVirtual ThreadsModern Java Features: Pattern Matching, Sealed Classes, Text Blocks, and Virtual Threads
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:
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
| Feature | First Preview / Introduction | Finalized |
|---|---|---|
Pattern Matching for instanceof | Java 14 | Java 16 |
| Text Blocks | Java 13 | Java 15 |
| Sealed Classes | Java 15 | Java 17 |
| Virtual Threads | Java 19 | Java 21 |
| Record Patterns | Java 19 | Java 21 |
Pattern Matching for switch | Java 17 | Java 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:
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:
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:
object instanceof Type variableExample:
Object value = 100;
if (value instanceof Integer number) {
System.out.println(number + 10);
}Output:
110The pattern performs two operations:
Type Check +Variable BindingThis eliminates a separate cast.
Pattern Variable Scope
The pattern variable is available where the compiler can prove that the pattern matched.
For example:
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.
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:
if (obj instanceof Employee) {
Employee employee =(Employee) obj;
System.out.println(employee.getName());
}Modern Java:
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:
null instanceof Stringevaluates to:
falseTherefore:
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:
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:
record Point(int x, int y) {
}Traditional checking might look like:
if (object instanceof Point point) {
int x = point.x();
int y = point.y();
System.out.println(x + y);
}With a record pattern:
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:
abstract class Payment {
}Potentially many unrelated classes could extend it.
A sealed class allows the permitted subclasses to be explicitly declared.
public sealed class Payment permits CreditCardPayment, CashPayment {
}Now only the permitted classes can directly extend Payment.
Creating a Sealed Class
Example:
public sealed class Shape permits Circle, Rectangle {
}Then:
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:
Payment
├── CreditCardPayment
├── CashPayment
└── BankTransferPaymentInstead 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:
finalsealednon-sealedFor example:
public sealed class Payment permits CreditCardPayment, OnlinePayment {
}A final subclass:
final class CreditCardPayment extends Payment {
}A sealed subclass:
sealed class OnlinePayment extends Payment permits UpiPayment, WalletPayment {
}Or a non-sealed subclass:
non-sealed class OnlinePayment extends Payment {
}final, sealed, and non-sealed
These three modifiers communicate different inheritance rules.
final
No further subclassing is allowed.
final class CreditCardPayment extends Payment {
}sealed
The subclass itself restricts which classes can extend it.
sealed class OnlinePayment extends Payment permits UpiPayment {
}non-sealed
The restriction is opened again.
non-sealed class OnlinePayment extends Payment {
}Conceptually:
sealed↓Controlled hierarchyfinal↓Hierarchy stopsnon-sealed↓Hierarchy opensSealed Interfaces
Sealed types are not limited to classes.
You can create a sealed interface:
public sealed interface Payment permits CardPayment, CashPayment {
}Then:
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:
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:
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:
Sealed Types +Pattern Matching +Records3. 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:
String json = "{\n" + " \"name\": \"Tanmoy\",\n" + " \"age\": 35\n" + "}";A text block is much easier to read:
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:
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.
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:
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:
String text = """
Line one
Line two
""";represents a multiline string.
You can also explicitly use:
String text = """
Hello\nWorld
""";when you specifically need a newline escape.
Text Blocks and Formatting
Text blocks can be combined with formatted().
String name = "Tanmoy";
int age = 35;
String message = """
Name: %s
Age: %d
""".formatted(name, age);Output:
Name: TanmoyAge: 35This 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:
String message = """
Hello Java
""";The type remains:
StringText 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:
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:
Thread.startVirtualThread(() -> {
System.out.println("Hello from virtual thread");
});This starts a virtual thread.
Another approach is:
Thread thread = Thread.ofVirtual() .start(() -> {
System.out.println( "Running in virtual thread");
});Virtual Thread Executor
A convenient way to execute many independent tasks is:
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:
Request↓Database query↓Wait↓External API↓Wait↓Return responseDuring 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:
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
| Feature | Platform Thread | Virtual Thread |
|---|---|---|
| Managed by | OS/JVM | JVM |
| Resource cost | Higher | Much lower |
| Large concurrency | More limited | Designed for very high counts |
| Blocking I/O | Can be expensive at scale | Well suited |
| CPU-bound work | Suitable | Does not remove CPU limits |
| Java version | Long-standing | Java 21 |
| Programming model | Thread-based | Thread-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:
ExecutorService executor = Executors.newFixedThreadPool(100);A virtual-thread-per-task executor can instead be used:
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:
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:
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:
sealed interface Payment permits CardPayment, CashPayment {
}with records:
record CardPayment(double amount) implements Payment {
}record CashPayment(double amount) implements Payment {
}Pattern matching can process them:
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:
String message = """
Payment processed
Type: %s
""".formatted( describe(payment));And a virtual thread could process the operation concurrently:
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:
sealed interface OrderStatus permits Pending, Paid, Cancelled {
}Records represent the states:
record Pending() implements OrderStatus {
}record Paid(double amount) implements OrderStatus {
}record Cancelled(String reason) implements OrderStatus {
}Pattern matching can process them:
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:
String output = """
ORDER STATUS
------------
%s
""".formatted(message(status));A virtual thread can process multiple independent orders:
Thread.startVirtualThread(() -> {
System.out.println(output);
});This is a good example of modern Java's direction:
Records +Sealed Types +Pattern Matching +Text Blocks +Virtual ThreadsWhen 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
switchlogic
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:
Payment
├── Card
├── Cash
└── BankTransferis 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:
Web requestsAPI callsDatabase operationsFile operationsNetwork communicationThey 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:
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:
sealed→ Controls inheritanceimmutable→ Controls state changesMistake 4: Thinking Text Blocks Are Templates
A text block is still a string literal.
It does not automatically perform variable substitution.
For example:
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
| Feature | Main Problem Solved |
|---|---|
| Pattern Matching | Simplifies type checks and extraction |
| Sealed Classes | Controls inheritance |
| Text Blocks | Simplifies multiline strings |
| Virtual Threads | Enables lightweight high concurrency |
A useful mental model is:
Pattern Matching→ Cleaner logicSealed Classes→ Safer hierarchiesText Blocks→ Cleaner textVirtual Threads→ Scalable concurrencyFrequently 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.
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
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 FinalGraphic 2 — Pattern Matching
TraditionalObject↓instanceof↓Cast↓Use variableModernObject↓instanceof Type value↓Use valueGraphic 3 — Sealed Class Hierarchy
Payment / \ / \ Card Cash final finalGraphic 4 — Virtual Thread Architecture
Thousands of Tasks / | \↓↓↓Virtual Virtual Virtual Thread Thread Thread \ | / \ | / JVM Scheduler↓Platform Threads↓CPUGraphic 5 — Modern Java Feature Map
MODERN JAVA
|
┌─────────────┼─────────────┐
↓
↓
↓
Pattern Sealed Text
Matching Classes Blocks
| | |
Cleaner Controlled Readable
Type Logic Hierarchy Multiline
|
↓
Virtual Threads
|
↓
High ConcurrencyRunnable Code Example: Modern Java Features
Java 17+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));
}
}