
Java 15 Features You Should Know
Java 15, released in September 2020, brought significant improvements to one of the world’s most popular programming languages. This LTS-adjacent version introduced pattern matching enhancements, sealed classes, hidden classes, and garbage collection improvements that directly impact application performance and code maintainability. Whether you’re running Java applications on VPS instances or managing enterprise deployments on dedicated servers, understanding these features will help you write more efficient, readable, and maintainable code while taking advantage of JVM performance optimizations.
Text Blocks (Standard Feature)
Text blocks, which were preview features in Java 13 and 14, became standard in Java 15. This feature eliminates the need for excessive string concatenation and escape sequences when working with multi-line strings.
// Before Java 15 - the painful way
String html = "<html>\n" +
" <head>\n" +
" <title>My Page</title>\n" +
" </head>\n" +
" <body>\n" +
" <h1>Hello World</h1>\n" +
" </body>\n" +
"</html>";
// Java 15 - clean and readable
String html = """
<html>
<head>
<title>My Page</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
""";
Text blocks are particularly useful for SQL queries, JSON templates, and configuration files. The indentation is automatically managed based on the closing delimiter position, making code much more maintainable.
// SQL queries become much cleaner
String query = """
SELECT u.username, u.email, p.title
FROM users u
JOIN posts p ON u.id = p.user_id
WHERE u.created_date > ?
ORDER BY p.created_date DESC
""";
Pattern Matching for instanceof (Second Preview)
Pattern matching for instanceof reduces boilerplate code by combining type checking and casting into a single operation. This feature was in its second preview in Java 15 and became standard in Java 16.
// Old approach - verbose and error-prone
public String formatValue(Object obj) {
if (obj instanceof String) {
String s = (String) obj;
return s.toUpperCase();
} else if (obj instanceof Integer) {
Integer i = (Integer) obj;
return "Number: " + i;
}
return obj.toString();
}
// Java 15 pattern matching - cleaner and safer
public String formatValue(Object obj) {
if (obj instanceof String s) {
return s.toUpperCase();
} else if (obj instanceof Integer i) {
return "Number: " + i;
}
return obj.toString();
}
The pattern variable (like `s` and `i` above) is automatically in scope within the if block, eliminating the need for explicit casting. This reduces the chance of ClassCastException errors and makes code more readable.
Sealed Classes (Preview)
Sealed classes provide a way to control which classes can extend or implement them. This feature was introduced as a preview in Java 15 and helps create more secure and predictable inheritance hierarchies.
// Define a sealed class with permitted subclasses
public sealed class Shape
permits Circle, Rectangle, Triangle {
public abstract double area();
}
// Permitted subclasses
public final class Circle extends Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public final class Rectangle extends Shape {
private final double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
public non-sealed class Triangle extends Shape {
private final double base, height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double area() {
return 0.5 * base * height;
}
}
Sealed classes work particularly well with pattern matching, creating exhaustive switch expressions that the compiler can verify:
public double calculateArea(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.getRadius() * c.getRadius();
case Rectangle r -> r.getWidth() * r.getHeight();
case Triangle t -> 0.5 * t.getBase() * t.getHeight();
// No default case needed - compiler knows all possibilities
};
}
Hidden Classes
Hidden classes are a JVM feature that allows frameworks to define classes that cannot be discovered by other classes. This improves security and performance for dynamically generated classes.
// Example of creating a hidden class using Lookup API
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
public class HiddenClassExample {
public static void createHiddenClass() throws Exception {
Lookup lookup = MethodHandles.lookup();
// Bytecode for a simple class (would typically be generated)
byte[] classBytes = generateClassBytes();
// Create hidden class
Lookup hiddenLookup = lookup.defineHiddenClass(
classBytes,
true, // initialize the class
Lookup.ClassOption.NESTMATE
);
Class<?> hiddenClass = hiddenLookup.lookupClass();
System.out.println("Created hidden class: " + hiddenClass.getName());
}
}
Hidden classes are primarily used by:
- Dynamic language runtimes (like those for Kotlin, Scala)
- Proxy generation frameworks
- Bytecode manipulation libraries
- Serialization frameworks
Records (Second Preview)
Records were in their second preview in Java 15, providing a concise way to create immutable data carrier classes. They automatically generate constructors, accessors, equals(), hashCode(), and toString() methods.
// Traditional class - lots of boilerplate
public class PersonOld {
private final String name;
private final int age;
private final String email;
public PersonOld(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
public String getName() { return name; }
public int getAge() { return age; }
public String getEmail() { return email; }
@Override
public boolean equals(Object obj) {
// ... lots of boilerplate
}
@Override
public int hashCode() {
// ... more boilerplate
}
@Override
public String toString() {
// ... even more boilerplate
}
}
// Record - concise and clean
public record Person(String name, int age, String email) {
// Custom validation in compact constructor
public Person {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
}
// Custom methods can still be added
public boolean isAdult() {
return age >= 18;
}
}
Records work excellently with pattern matching and switch expressions:
public String processUser(Person person) {
return switch (person.age()) {
case 0, 1, 2 -> "Toddler: " + person.name();
case int age when age < 13 -> "Child: " + person.name();
case int age when age < 18 -> "Teenager: " + person.name();
default -> "Adult: " + person.name();
};
}
ZGC and Shenandoah Garbage Collector Improvements
Java 15 brought significant improvements to both ZGC (Z Garbage Collector) and Shenandoah GC, focusing on low-latency garbage collection for applications requiring minimal pause times.
Feature | ZGC | Shenandoah | G1GC |
---|---|---|---|
Max Pause Time | < 10ms | < 10ms | ~200ms |
Heap Size Support | 8MB – 16TB | Up to 16TB | Up to heap size |
Concurrent Collection | Yes | Yes | Partially |
Memory Overhead | 2-16% | 2-8% | 5-10% |
To enable ZGC in your application:
# Enable ZGC with specific heap settings
java -XX:+UseZGC -Xmx4g -XX:+UnlockExperimentalVMOptions MyApplication
# For production monitoring
java -XX:+UseZGC \
-Xmx8g \
-XX:+UnlockExperimentalVMOptions \
-XX:+LogVMOutput \
-XX:LogFile=gc.log \
-XX:+UseTransparentHugePages \
MyApplication
For Shenandoah GC:
# Enable Shenandoah GC
java -XX:+UseShenandoahGC -Xmx4g MyApplication
# With tuning options
java -XX:+UseShenandoahGC \
-Xmx8g \
-XX:ShenandoahGCHeuristics=adaptive \
-XX:+ShenandoahUncommit \
MyApplication
Edwards-Curve Digital Signature Algorithm (EdDSA)
Java 15 added support for the EdDSA signature algorithm, providing better security and performance compared to traditional ECDSA.
import java.security.*;
import java.security.spec.*;
public class EdDSAExample {
public static void demonstrateEdDSA() throws Exception {
// Generate Ed25519 key pair
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("Ed25519");
KeyPair keyPair = keyGen.generateKeyPair();
// Create signature
Signature signature = Signature.getInstance("Ed25519");
signature.initSign(keyPair.getPrivate());
String message = "Hello, Java 15 EdDSA!";
signature.update(message.getBytes());
byte[] digitalSignature = signature.sign();
// Verify signature
Signature verifier = Signature.getInstance("Ed25519");
verifier.initVerify(keyPair.getPublic());
verifier.update(message.getBytes());
boolean isValid = verifier.verify(digitalSignature);
System.out.println("Signature valid: " + isValid);
// Key information
System.out.println("Public key format: " + keyPair.getPublic().getFormat());
System.out.println("Private key algorithm: " + keyPair.getPrivate().getAlgorithm());
}
}
EdDSA advantages over traditional algorithms:
- Faster signature generation and verification
- Smaller signature size (64 bytes for Ed25519)
- Better security properties
- Resistance to timing attacks
- No need for secure random number generation during signing
Helpful NullPointerException Messages
Java 15 improved NullPointerException messages to show exactly which variable was null, making debugging much easier.
public class NPEExample {
static class User {
String name;
Address address;
User(String name, Address address) {
this.name = name;
this.address = address;
}
}
static class Address {
String street;
String city;
Address(String street, String city) {
this.street = street;
this.city = city;
}
}
public static void main(String[] args) {
User user = new User("John", null);
// This will throw NPE with helpful message
try {
String city = user.address.city.toUpperCase();
} catch (NullPointerException e) {
// Java 15 message: Cannot read field "city" because "user.address" is null
// Old message: null
System.out.println("Helpful message: " + e.getMessage());
}
}
}
Enable detailed NPE messages with:
# Enable helpful NPE messages
java -XX:+ShowCodeDetailsInExceptionMessages MyApplication
JEP 383: Foreign-Memory Access API (Second Incubator)
The Foreign-Memory Access API allows Java programs to safely access memory outside the Java heap, which is crucial for performance-critical applications and native library integration.
import jdk.incubator.foreign.*;
public class ForeignMemoryExample {
public static void demonstrateForeignMemory() {
try (MemorySegment segment = MemorySegment.allocateNative(1024)) {
// Write data to off-heap memory
MemoryAddress base = segment.baseAddress();
MemoryAccess.setInt(base, 42);
MemoryAccess.setInt(base.addOffset(4), 84);
// Read data back
int first = MemoryAccess.getInt(base);
int second = MemoryAccess.getInt(base.addOffset(4));
System.out.println("First value: " + first);
System.out.println("Second value: " + second);
// Working with arrays
int[] data = {1, 2, 3, 4, 5};
MemorySegment arraySegment = MemorySegment.ofArray(data);
MemoryAddress arrayBase = arraySegment.baseAddress();
// Modify array through foreign memory API
for (int i = 0; i < data.length; i++) {
int current = MemoryAccess.getInt(arrayBase.addOffset(i * 4));
MemoryAccess.setInt(arrayBase.addOffset(i * 4), current * 2);
}
System.out.println("Modified array: " + Arrays.toString(data));
}
}
}
Performance and Migration Considerations
When upgrading to Java 15, consider these performance implications:
Area | Improvement | Impact | Best Practice |
---|---|---|---|
Text Blocks | Reduced string concatenation overhead | 5-15% faster string operations | Replace complex string concatenations |
Pattern Matching | JIT optimization opportunities | 2-8% performance gain | Use in hot code paths |
ZGC/Shenandoah | Sub-10ms pause times | Better response times | Enable for latency-sensitive apps |
Hidden Classes | Reduced memory footprint | Lower GC pressure | Use in dynamic class generation |
Migration steps for existing applications:
# 1. Update build configuration (Maven example)
<properties>
<maven.compiler.source>15</maven.compiler.source>
<maven.compiler.target>15</maven.compiler.target>
</properties>
# 2. Enable preview features if needed
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
</configuration>
</plugin>
# 3. Runtime with preview features
java --enable-preview MyApplication
Real-World Use Cases and Integration
Java 15 features shine in specific scenarios:
**Microservices with Text Blocks:**
@RestController
public class ConfigController {
@GetMapping("/docker-compose")
public String getDockerCompose() {
return """
version: '3.8'
services:
app:
image: myapp:latest
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- DATABASE_URL=jdbc:postgresql://db:5432/mydb
db:
image: postgres:13
environment:
- POSTGRES_DB=mydb
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
""";
}
}
**API Response Handling with Records:**
public record ApiResponse<T>(
boolean success,
T data,
String message,
int statusCode,
Instant timestamp
) {
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, data, "Success", 200, Instant.now());
}
public static <T> ApiResponse<T> error(String message, int statusCode) {
return new ApiResponse<>(false, null, message, statusCode, Instant.now());
}
}
// Usage in controller
@GetMapping("/users/{id}")
public ApiResponse<User> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(ApiResponse::success)
.orElse(ApiResponse.error("User not found", 404));
}
**High-Performance Data Processing:**
# JVM arguments for data processing applications
java -XX:+UseZGC \
-Xmx32g \
-XX:+UnlockExperimentalVMOptions \
-XX:+UseTransparentHugePages \
-XX:+ShowCodeDetailsInExceptionMessages \
--enable-preview \
DataProcessingApp
For comprehensive information about Java 15 features, refer to the official OpenJDK documentation and the Oracle Java 15 documentation.
Java 15 represents a significant step forward in language evolution, offering features that improve both developer productivity and application performance. Whether you're deploying on cloud infrastructure or managing on-premises servers, these features provide concrete benefits for modern Java development workflows.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.