BLOG POSTS
Java 15 Features You Should Know

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.

Leave a reply

Your email address will not be published. Required fields are marked