How do I use CompletableFuture for async tasks?

CompletableFuture is a powerful tool in Java to implement asynchronous programming. It allows you to perform tasks in the background, chain multiple async tasks together, handle both success and failure scenarios, and combine multiple async computations.

Here’s a summary of how to use CompletableFuture with examples:


1. Run a task asynchronously

Use supplyAsync() (if the task produces a result) or runAsync() (if the task doesn’t return anything).

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    System.out.println("Running task in background...");
});

Or with a result:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return "Hello, World!";
});

2. Process the result

You can process the result of a CompletableFuture using methods like thenAccept or thenApply.

future.thenApply(result -> {
    System.out.println("Received result: " + result);
    return result.toUpperCase();
}).thenAccept(uppercaseResult -> {
    System.out.println("Transformed result: " + uppercaseResult);
});

3. Combine multiple tasks

You can run multiple tasks in parallel and combine their results.

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Task1");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Task2");

CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (task1Result, task2Result) -> {
    return task1Result + " and " + task2Result;
});

combinedFuture.thenAccept(result -> {
    System.out.println("Combined Result: " + result);
});

4. Wait for all tasks

If you have multiple tasks and want to wait for all of them to complete, use CompletableFuture.allOf.

CompletableFuture<Void> allTasks = CompletableFuture.allOf(
    CompletableFuture.runAsync(() -> System.out.println("Task 1 completed")),
    CompletableFuture.runAsync(() -> System.out.println("Task 2 completed"))
);

allTasks.join(); // Blocks the thread and waits for completion
System.out.println("All tasks completed.");

5. Handle errors

You can handle exceptions in async tasks using exceptionally().

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    throw new RuntimeException("Oops, something went wrong!");
}).exceptionally(ex -> {
    System.out.println("Error: " + ex.getMessage());
    return null;
});

6. Compose dependent tasks

Use thenCompose to chain dependent tasks where the second task depends on the result of the first one.

CompletableFuture.supplyAsync(() -> "Task 1 Result")
    .thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " Task 2 Result"))
    .thenAccept(finalResult -> System.out.println("Final Result: " + finalResult));

7. Custom Executor

By default, CompletableFuture uses the ForkJoinPool.commonPool for async tasks. You can provide a custom executor for better control over threads.

ExecutorService executor = Executors.newFixedThreadPool(10);

CompletableFuture.runAsync(() -> {
    System.out.println("Running task on custom executor");
}, executor);

Key Methods in CompletableFuture

Method Description
runAsync Run a task asynchronously (does not return result).
supplyAsync Run a task asynchronously and return a result.
thenApply Transform the result of a CompletableFuture.
thenAccept Consumes the result of a CompletableFuture (no further processing).
thenCompose Chains dependent tasks where the next uses the result of the previous.
thenCombine Combines two CompletableFuture results.
allOf / anyOf Wait for all or any of multiple futures to complete.
exceptionally Handle exceptions thrown during the async computation.
join / get Block and wait for the task to complete and retrieve its result (not recommended for non-blocking).

Example Workflow with Pipeline

Here’s an example pipeline:

  1. Fetch data
  2. Process it
  3. Save it
  4. Notify the user
ExecutorService executor = Executors.newFixedThreadPool(4);

CompletableFuture.supplyAsync(() -> {
    System.out.println("Fetching data...");
    return "Raw Data";
}, executor).thenApply(data -> {
    System.out.println("Processing data...");
    return data.toUpperCase();
}).thenAccept(processedData -> {
    System.out.println("Saving: " + processedData);
}).thenRun(() -> {
    System.out.println("Notification: Done!");
}).exceptionally(ex -> {
    System.err.println("Pipeline failed due to: " + ex.getMessage());
    return null;
}).join();

Using CompletableFuture, you can build flexible, high-performance, and non-blocking applications.

How do I use the Virtual Threads API Project Loom?

The Virtual Threads API is part of Project Loom in the Java platform. With Java 19, virtual threads became available as a preview feature, enabling the creation of lightweight threads that can run concurrently. They work similarly to traditional threads but are much cheaper in terms of memory and thread management because they are managed by the Java runtime, not the operating system. This makes it possible to scale the number of threads easily, even in the millions.

Here’s how you can use the Virtual Threads API:

1. Enable Virtual Threads

Virtual threads are available in Java 19+ as an incubating feature. To use them:

  • Ensure that you’re using a compatible version of Java (Java 19 or later).
  • Add the JVM flag --enable-preview to enable preview features when running your program.

2. Creating Virtual Threads

Java provides the java.lang.Thread class and the Executors utility to work with virtual threads. For example:

Creating a Virtual Thread

You can create and start a virtual thread like this:

Thread.startVirtualThread(() -> {
    System.out.println("This is a virtual thread!");
});

Using Virtual Threads with Executors

The Executors.newVirtualThreadPerTaskExecutor() method creates an ExecutorService that launches a new virtual thread for each task:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> System.out.println("Task on a virtual thread"));
    executor.submit(() -> System.out.println("Another virtual thread task"));
}

3. Advantages of Virtual Threads

  1. Lightweight: Virtual threads are less resource-intensive because they use the Java runtime scheduler rather than the OS scheduler. Millions of threads can be created.
  2. Non-blocking: Blocking operations in virtual threads don’t block OS resources, making them very efficient for I/O-intensive workloads like web servers or concurrent network communication.
  3. Easier Scaling: They simplify concurrent programming by allowing you to continue using the familiar thread-per-task model without worrying about resource limits.
  4. Works with Existing Code: Virtual threads integrate well with existing Java APIs like java.util.concurrent.

4. When to Use Virtual Threads

Virtual threads are ideal for:

  • Concurrent I/O tasks like HTTP servers or database connections.
  • High-concurrency environments where traditional threads might run out of OS resources.
  • Migrating legacy multithreaded code to take advantage of better scalability.

5. Example: HTTP Server with Virtual Threads

Here’s a minimal example showcasing how to use virtual threads for handling multiple HTTP requests:

import java.net.ServerSocket;
import java.net.Socket;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.PrintWriter;

public class VirtualThreadHttpServer {
    public static void main(String[] args) throws Exception {
        try (var serverSocket = new ServerSocket(8080)) {
            while (true) {
                Socket clientSocket = serverSocket.accept();
                Thread.startVirtualThread(() -> handleClient(clientSocket));
            }
        }
    }

    private static void handleClient(Socket clientSocket) {
        try (var in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
             var out = new PrintWriter(clientSocket.getOutputStream(), true)) {
            out.println("Hello from the Virtual Thread server!");
            String input;
            while ((input = in.readLine()) != null) {
                System.out.println("Received: " + input);
                if ("exit".equalsIgnoreCase(input)) {
                    break;
                }
                out.println("You said: " + input);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

This makes use of virtual threads to handle each incoming socket connection, which scales efficiently for high-concurrency workloads.

6. Integration with Structured Concurrency

Virtual threads can be combined with structured concurrency (introduced in Java 21) for safer and more manageable multithreading. Structured concurrency allows parent threads to manage the lifecycle of child threads.

Example of Structured Concurrency:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class StructuredConcurrencyExample {
    public static void main(String[] args) throws Exception {
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            var future1 = executor.submit(() -> {
                Thread.sleep(500);
                return "Task 1 completed";
            });

            var future2 = executor.submit(() -> {
                Thread.sleep(300);
                return "Task 2 completed";
            });

            // Wait for results
            System.out.println(future1.get());
            System.out.println(future2.get());
        }
    }
}

Keynotes:

  • Virtual threads require no changes in application logic. Code written for traditional Thread can immediately benefit from using virtual threads.
  • They simplify thread management while maintaining excellent performance for non-blocking I/O operations.

Limitations:

  • Virtual threads won’t improve performance for CPU-bound tasks; you still need to consider the number of logical CPUs in your system.
  • JVM preview features need to be enabled since virtual threads are not yet finalized in the standard Java API. Check the latest Java release notes for updates.

How do I use the Java Util Concurrent Flow API?

The java.util.concurrent.Flow API, introduced in Java 9, is a low-level implementation of the Reactive Streams specification, providing an asynchronous, non-blocking framework for handling streams of data. It allows you to build reactive systems that support backpressure to handle large or variable amounts of data more efficiently.

Here’s a quick guide on how to use java.util.concurrent.Flow API:


Key Interfaces in java.util.concurrent.Flow

The API comprises four core interfaces:

  1. Flow.Publisher:
    Represents the producer of data. It publishes items to one or more subscribers.

  2. Flow.Subscriber:
    Represents the consumer of data. It subscribes to a publisher to receive data.

  3. Flow.Subscription:
    Represents a link between a publisher and a subscriber, allowing the subscriber to control how much data it receives (backpressure).

  4. Flow.Processor:
    Both a subscriber and a publisher, used to transform or process elements as they flow through the stream.


Implementation Workflow

To use the Flow API, you need to implement these interfaces. Below is a step-by-step explanation:

1. Create a Publisher

  • The Flow.Publisher interface has a single method subscribe(Subscriber<? super T> subscriber).
  • The publisher is responsible for connecting with subscribers and managing their subscriptions.
package org.kodejava.util.concurrent;

import java.util.concurrent.Flow;

public class SimplePublisher implements Flow.Publisher<String> {
    private final String[] items = {"Item 1", "Item 2", "Item 3"};

    @Override
    public void subscribe(Flow.Subscriber<? super String> subscriber) {
        SubscriptionImpl subscription = new SubscriptionImpl(subscriber, items);
        subscriber.onSubscribe(subscription);
    }

    private static class SubscriptionImpl implements Flow.Subscription {
        private final Flow.Subscriber<? super String> subscriber;
        private final String[] items;
        private int currentIndex = 0;
        private boolean canceled = false;

        public SubscriptionImpl(Flow.Subscriber<? super String> subscriber, String[] items) {
            this.subscriber = subscriber;
            this.items = items;
        }

        @Override
        public void request(long n) {
            if (n <= 0) {
                subscriber.onError(new IllegalArgumentException("Must request a positive number of items"));
                return;
            }

            for (int i = 0; i < n && currentIndex < items.length; i++) {
                if (canceled) {
                    return;
                }
                subscriber.onNext(items[currentIndex++]);
            }

            if (currentIndex == items.length) {
                subscriber.onComplete();
            }
        }

        @Override
        public void cancel() {
            canceled = true;
        }
    }
}

2. Create a Subscriber

  • Implement the Flow.Subscriber interface (four methods) for receiving events from a publisher:
    • onSubscribe(Flow.Subscription subscription): Receive the subscription. You must request data here.
    • onNext(T item): Handle the next item of the stream.
    • onError(Throwable throwable): Handle any errors.
    • onComplete(): Called when the publisher finishes sending data.
package org.kodejava.util.concurrent;

import java.util.concurrent.Flow;

public class SimpleSubscriber implements Flow.Subscriber<String> {
    private Flow.Subscription subscription;

    @Override
    public void onSubscribe(Flow.Subscription subscription) {
        this.subscription = subscription;
        System.out.println("Subscribed!");
        subscription.request(1); // Request the first item
    }

    @Override
    public void onNext(String item) {
        System.out.println("Received: " + item);
        subscription.request(1); // Request the next item
    }

    @Override
    public void onError(Throwable throwable) {
        System.err.println("Error: " + throwable.getMessage());
    }

    @Override
    public void onComplete() {
        System.out.println("Complete!");
    }
}

3. Connect the Publisher to the Subscriber

  • Instantiate and link your Publisher and Subscriber.
package org.kodejava.util.concurrent;

public class FlowExample {
    public static void main(String[] args) {
        SimplePublisher publisher = new SimplePublisher();
        SimpleSubscriber subscriber = new SimpleSubscriber();

        publisher.subscribe(subscriber);
    }
}

4. (Optional) Create a Processor

  • A Processor acts as both a Subscriber to transform data from an upstream publisher and a Publisher to pass it downstream.
package org.kodejava.util.concurrent;

import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;

public class UppercaseProcessor extends SubmissionPublisher<String> implements Flow.Processor<String, String> {
    private Flow.Subscription subscription;

    @Override
    public void onSubscribe(Flow.Subscription subscription) {
        this.subscription = subscription;
        subscription.request(1);
    }

    @Override
    public void onNext(String item) {
        submit(item.toUpperCase());
        subscription.request(1);
    }

    @Override
    public void onError(Throwable throwable) {
        throwable.printStackTrace();
        close();
    }

    @Override
    public void onComplete() {
        System.out.println("Processing complete!");
        close();
    }
}

Example with SubmissionPublisher

Java also provides a SubmissionPublisher class, an implementation of Flow.Publisher, which simplifies creating Publishers.

package org.kodejava.util.concurrent;

import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;

public class SubmissionPublisherExample {
    public static void main(String[] args) throws InterruptedException {
        SubmissionPublisher<String> publisher = new SubmissionPublisher<>();

        Flow.Subscriber<String> subscriber = new Flow.Subscriber<>() {
            private Flow.Subscription subscription;

            @Override
            public void onSubscribe(Flow.Subscription subscription) {
                this.subscription = subscription;
                System.out.println("Subscribed!");
                subscription.request(1);
            }

            @Override
            public void onNext(String item) {
                System.out.println("Received: " + item);
                subscription.request(1);
            }

            @Override
            public void onError(Throwable throwable) {
                throwable.printStackTrace();
            }

            @Override
            public void onComplete() {
                System.out.println("Done!");
            }
        };

        publisher.subscribe(subscriber);

        System.out.println("Publishing data...");
        publisher.submit("Hello");
        publisher.submit("World");
        publisher.submit("!");

        Thread.sleep(100); // Allow time for processing
        publisher.close();
    }
}

Output of the Above Example

Publishing data...
Subscribed!
Received: Hello
Received: World
Received: !
Done!

Points to Remember

  1. Backpressure:
    • The subscriber can control how many items it wants to receive using the request() method of Flow.Subscription.
    • If the subscriber requests fewer items, the publisher will slow down and send only the requested number.
  2. Error Handling:
    • If something goes wrong, the onError() callback is invoked, allowing you to handle errors gracefully.
  3. Completion:
    • Once all elements are processed, the publisher calls onComplete() to indicate the sequence is finished.
  4. Threading:
    • The Flow API itself doesn’t mandate the use of specific threads for dealing with publishers/subscribers, but it’s often paired with asynchronous mechanisms (e.g., the SubmissionPublisher uses a default ForkJoinPool to process items).

By combining custom implementations with the provided SubmissionPublisher and additional libraries, you can build reactive systems that are both powerful and resource-efficient.

How to use the new @ImplicitlyDeclared annotation in Java 25

The @ImplicitlyDeclared annotation in Java 25 introduces a way to explicitly mark certain declarations as inherently implied by the compiler. It is primarily targeted to pave the way for future enhancements in the Java language, such as compiler-backed, implicit declarations.

Purpose of @ImplicitlyDeclared

This annotation:

  1. Identifies elements (like methods, fields, or constructors) implicitly added for specific language features or frameworks.
  2. Makes it easier for tooling, introspection, and reflection to recognize generated members without needing extra libraries or custom logic.
  3. Helps maintain clean code by distinguishing user-defined elements from compiler-generated or implicit ones.

Basic Usage

The @ImplicitlyDeclared annotation is not intended for manual application by developers in most scenarios. Instead, it is typically used internally by the compiler or tools generating code. However, understanding its purpose is useful for debugging or when extending reflective tools.

Here’s an example scenario where the annotation might come into play:

Example Code:

import java.lang.annotation.ImplicitlyDeclared;

public record Person(String name, int age) {
    @ImplicitlyDeclared
    public Person {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative!");
        }
    }
}

In this example:

  • The @ImplicitlyDeclared annotation is applied by the compiler to certain constructs (like canonical or additional constructors, accessors, or default methods) that the developer did not explicitly write but are part of the language specification of record types.

Key Points:

  • Developers rarely need to apply @ImplicitlyDeclared directly.
  • It supports tools like modern IDEs and reflection APIs to cleanly separate user-defined declarations from language-backed or compiler-generated declarations.
  • Frameworks and annotation processors may use this for better code generation and validation.

Reflective Usage:

When working with reflection, you may encounter elements annotated with @ImplicitlyDeclared. You can filter out these implicit declarations when processing or inspecting classes programmatically:

Example Using Reflection

import java.lang.annotation.ImplicitlyDeclared;
import java.lang.reflect.Method;

public class ReflectionExample {
    public static void main(String[] args) {
        Class<?> clazz = Person.class;

        for (Method method : clazz.getDeclaredMethods()) {
            if (method.isAnnotationPresent(ImplicitlyDeclared.class)) {
                System.out.println("Implicitly declared: " + method.getName());
            }
        }
    }
}

In this example:

  • The program filters and lists all methods in the Person record that are marked as implicitly declared (like accessors or synthesized methods).

Context in Modern Java Features

The @ImplicitlyDeclared annotation is closely tied to language innovations in Java 25, including:
1. Unnamed classes and methods.
2. Record patterns and deconstruction.
3. Implicit behavior implementations like canonical constructors.

For deeper conceptual understanding, review features like records, sealed interfaces, and future unnamed constructs, where the compiler often injects behavior without explicit developer code.

For further detailed documentation, consider checking the JDK’s proposed update records, where changes to annotations and their application are discussed [1], or refer to Oracle’s language updates for Java SE [2].

How to Simplify Control Flow with Enhanced Switch Statements in Java 25

Java 25 introduced Enhanced switch Statements to simplify control flow, making type checks, value comparisons, and complex branching cleaner and more expressive.

Here’s a guide to simplify control flow using this feature:


Key Enhancements in switch

  1. Type Pattern Matching: Directly match and work with variable types in patterns.
  2. Guarded Patterns: Add conditions (when) to patterns for finer control.
  3. Exhaustive Matching: Ensures all possible branches are accounted for (especially useful with sealed classes).
  4. Simplified Null Handling: Handles null without redundant checks.
  5. Nested Patterns: Combine patterns within switch for complex logic.
  6. Constant Matching: Patterns can match constants, combining value comparison and type matching.

How switch is Enhanced

1. Type Pattern Matching

No need for explicit type casting; switch can directly match types and assign to variables.

public static String handleInput(Object input) {
    return switch (input) {
        case String s -> "It's a String: " + s;
        case Integer i -> "It's an Integer: " + (i + 5);
        case Double d -> "It's a Double: " + (d * 2);
        case null -> "Input is null!";
        default -> "Unknown type";
    };
}
  • Why? Simplifies logic by avoiding explicit instanceof checks and casting.

2. Guarded Patterns

Patterns now include when clauses for additional checks within cases.

public static String analyzeNumber(Number number) {
    return switch (number) {
        case Integer i when i > 0 -> "Positive Integer: " + i;
        case Integer i -> "Non-Positive Integer: " + i;
        case Double d when d.isNaN() -> "It's NaN";
        case Double d -> "A Double: " + d;
        default -> "Unknown type of Number";
    };
}
  • Why? Adds flexibility to handle sub-conditions in patterns.

3. Exhaustiveness with sealed Classes

Combining sealed class hierarchies with switch enforces completeness at compile-time by covering all subclasses.

public sealed interface Shape permits Circle, Rectangle {}

public record Circle(double radius) implements Shape {}
public record Rectangle(double width, double height) implements Shape {}

public static String describeShape(Shape shape) {
    return switch (shape) {
        case Circle c -> "Circle with radius: " + c.radius();
        case Rectangle r -> "Rectangle: " + r.width() + "x" + r.height();
    };
}
  • Why? Ensures all cases are handled, or the compiler alerts you of missing subclasses.

4. Null Handling Simplification

Design cases explicitly for null without separate checks.

public static void handleString(String str) {
    switch (str) {
        case null -> System.out.println("String is null!");
        case "Hello" -> System.out.println("Greeting identified!");
        default -> System.out.println("Unrecognized input.");
    }
}
  • Why? Eliminates external if (str == null) checks, merging all logic into switch.

5. Nested Patterns for Complex Scenarios

switch supports nested patterns for deeper matching logic.

public static String processNested(Object obj) {
    return switch (obj) {
        case Circle(double r) when r > 10 -> "Large Circle, radius: " + r;
        case Rectangle(double w, double h) when w == h -> "Square with side: " + w;
        case Rectangle(double w, double h) -> "Rectangle: " + w + "x" + h;
        default -> "Unknown Shape";
    };
}
  • Why? Makes complex decision trees concise and readable.

Advantages of Enhanced switch

  • Cleaner Syntax: Removes verbose if-else or legacy switch cases.
  • More Declarative: Focus on what you’re branching on, not how.
  • Compile-Time Safety: Ensures all branches are accounted for with exhaustive checks.
  • Improved Null Safety: Explicit null cases reduce runtime errors.
  • Seamless with Modern Java Features: Works beautifully with records, sealed classes, and type inference.

When to Use Enhanced switch

  • Type-based control flows where type and values matter (e.g., handling polymorphism elegantly).
  • Complex branching conditions are consolidated into a clean declarative structure.
  • Improved readability and maintainability for large branching logic.

This feature is a step toward making Java code more concise, safer, and expressive!

How to Write Cleaner Code with String Templates in Java

String templates in Java 25 introduce a cleaner, more efficient, and safer way to work with strings. They allow embedding expressions inside strings without relying on concatenation or external APIs. Using string templates can lead to code that is easier to understand and maintain.

Here’s how you can write cleaner and more efficient code with string templates in Java 25:


1. Basics of String Templates

String templates allow you to define a string that contains placeholders for expressions. These placeholders are evaluated at runtime. In Java 25, this is done using the STR.""" syntax (or StringTemplate API).

Example:

String name = "John";
int age = 30;

String greeting = STR."""
    Hello, my name is \{name} and I am \{age} years old.
    """;
System.out.println(greeting);

Output:

Hello, my name is John and I am 30 years old.

2. Key Features

  • Dynamic Expressions
    You can embed any expression within the \{} placeholders inside the template.

    int x = 10;
    int y = 20;
    
    String result = STR."""
        Sum of x and y is \{x + y}.
        """;
    System.out.println(result);
    
  • Multiline Support
    String templates natively support multiline strings and formatting, making it easier to work with larger templates.

    String paragraph = STR."""
        This is a multiline
        string template with
        expressions like \{"Java " + 25}.
        """;
    

3. Benefits Over Traditional String Handling

a. Eliminates Boilerplate

Previously, concatenating variables into strings required explicit concatenation or String.format(). This is no longer needed.

// Before Java 25 - verbose
String name = "Alice";
String message = "Hello, " + name + "!";
// or
String message = String.format("Hello, %s!", name);

// Java 25
String message = STR."Hello, \{name}!";

b. Improved Readability

String templates allow templates to resemble the final output, improving readability.

c. Type-Safe

String templates are type-safe, ensuring that runtime errors related to improper formatting are minimized.


4. Compatibility with Existing APIs

String templates can simplify working with APIs like SQL or HTML without extensive external libraries.

Example (SQL):

String tableName = "users";
String query = STR."""
    SELECT * FROM \{tableName}
    WHERE age > 18
    ORDER BY name;
    """;
System.out.println(query);

Example (HTML):

String title = "Welcome";
String template = STR."""
    <html>
        <head><title>\{title}</title></head>
        <body><h1>Hello, \{title}</h1></body>
    </html>
    """;
System.out.println(template);

5. Advanced Use Cases

a. Use with External Formatting Libraries

String templates integrate well with JSON or XML serialization/deserialization.

Example (JSON):

String username = "john_doe";
int userID = 123;

String json = STR."""
    {{
        "username": "\{username}",
        "id": \{userID}
    }}
    """;
System.out.println(json);

b. Avoid Code Injection

String templates are safer, as they encourage proper escaping of user-provided data when combined with API interactions such as SQL or HTML. Proper escaping ensures no code injection vulnerabilities.


6. Custom Formatters

String templates in Java 25 can leverage custom formatters for advanced needs. This allows developers to define how specific types (like dates or numbers) are formatted in the string.

Custom formatting is achieved by extending the template processor.

Example: Formatting a date into a readable format:

import java.time.LocalDate;

LocalDate today = LocalDate.now();

String message = STR."""
   Today's date is \{today.toString()}.
   """;
System.out.println(message);

To include formatting logic, custom processors can modify such outputs.


7. Example: Building APIs with Readable Responses

Here’s an example of using string templates for building responses in web APIs:

public String buildUserResponse(String username, String email) {
    return STR."""
        {
            "username": "\{username}",
            "email": "\{email}"
        }
        """;
}

// Usage
String response = buildUserResponse("alice", "[email protected]");
System.out.println(response);

8. Combining String Templates with Switch Expressions

Java 25 also brings improvements to switch expressions, which can combine well with string templates.

int code = 404;

String message = STR."""
    Status: \{
        switch (code) {
            case 200 -> "Success";
            case 404 -> "Not Found";
            case 500 -> "Server Error";
            default -> "Unknown";
        }
    }
    """;
System.out.println(message);

Summary: Cleaner Code with String Templates

  • Readability: Cleaner and less verbose syntax.
  • Efficiency: Reduces reliance on external formatting libraries or manual concatenation.
  • Safety: Minimized risk of runtime errors and injection vulnerabilities.
  • Integration: Seamlessly used with existing APIs and libraries.

Adopting Java 25 string templates improves the workflow significantly, making your apps cleaner and less error-prone.

How to use record patterns with instanceof in Java 25

Java 25 introduces improvements such as record patterns with instanceof, which allow more concise and expressive type matching and data extraction in one step. Here’s a guide on how to use them:


What are record patterns?

A record pattern enables matching and extracting components of a record class, which is essentially a class with immutable data. Record patterns simplify operations by combining type checking and field extraction syntactically.


Using instanceof with Record Patterns

In Java 25, you can use a record pattern directly with instanceof to both:
1. Match the type of the object.
2. Decompose its contents in a single expression.


Example of Record Patterns with instanceof

record Point(int x, int y) {}

public class Main {
    public static void main(String[] args) {
        Object obj = new Point(10, 20);

        // Using instanceof with a record pattern
        if (obj instanceof Point(int x, int y)) {
            System.out.println("Point coordinates: x = " + x + ", y = " + y);
        } else {
            System.out.println("Not a Point object");
        }
    }
}

Explanation

  • obj instanceof Point(int x, int y):
    • Pattern Matching: Verifies if obj is an instance of the Point record.
    • Decomposition: Extracts the x and y fields of the record into variables x and y.

As a result:

  • If obj matches the type, the fields are extracted automatically in the same step.
  • There’s no need to cast obj to Point explicitly or manually call getters.

Nesting Record Patterns

Record patterns can also be nested for more complex records containing other records or collections.

Example: Nested Record Patterns

record Rectangle(Point topLeft, Point bottomRight) {}

public class Main {
    public static void main(String[] args) {
        Object obj = new Rectangle(new Point(0, 0), new Point(10, 10));

        if (obj instanceof Rectangle(Point(int x1, int y1), Point(int x2, int y2))) {
            System.out.println("Rectangle corners: (" + x1 + ", " + y1 + ") to (" + x2 + ", " + y2 + ")");
        } else {
            System.out.println("Not a Rectangle object");
        }
    }
}

Explanation

  • Rectangle(Point(int x1, int y1), Point(int x2, int y2)) is a nested pattern:
    • Matches top-level Rectangle.
    • Decomposes its topLeft and bottomRight fields into Point objects.
    • Further extracts x and y coordinates from each Point.

Benefits

  1. Conciseness: Eliminates the need for explicit casting or redundant getter calls.
  2. Readability: Patterns declaratively show what is being matched and extracted.
  3. Flexibility: Works seamlessly with nested structures.

Good-to-Know Details

  1. Exhaustive Matching: Combine switch with record patterns for exhaustive, cleaner matching:
    void printShapeInfo(Object shape) {
       switch (shape) {
           case Point(int x, int y) -> System.out.println("Point: (" + x + ", " + y + ")");
           case Rectangle(Point topLeft, Point bottomRight) -> System.out.println("Rectangle with corners: " +
                   topLeft + " to " + bottomRight);
           default -> System.out.println("Unknown shape");
       }
    }
    
  2. Null Handling: instanceof with patterns doesn’t match null values directly. An explicit null check is still required.

  3. Restrictions: The immutability of records ensures safety and predictability when decomposing data and matching patterns.


Conclusion

The introduction of record patterns in Java 25 significantly enhances pattern matching and makes working with immutable objects far more intuitive and concise. Whether you’re matching simple records or nested structures, this feature saves you from boilerplate code and improves code readability.

How to use improved pattern matching for switch in Java 25

Java 25 introduces an improved feature for pattern matching with switch, further streamlining type checks, instance checks, and value comparisons.

Here’s how you can effectively use the enhanced pattern matching for switch in Java 25:

Key Features

  1. Exhaustive Matching: Ensures that all possible branches are accounted for.
  2. Simplification of Null Handling: Handles null conditions without extra boilerplate.
  3. Nested Patterns in Switch: Allows patterns to be nested for cleaner and more expressive logical flows.
  4. Constant Matching: Can combine constants with patterns.
  5. Sealed Class Support: Works seamlessly with sealed classes, auto-detecting subclasses for exhaustive pattern checks.

Syntax Examples

1. Type Pattern Matching

Allows you to handle specific types directly in a switch.

public static String process(Object obj) {
    return switch (obj) {
        case String s -> "It's a String: " + s;
        case Integer i -> "It's an Integer: " + (i + 10);
        case null -> "It's null!";
        default -> "Unknown type!";
    };
}

2. Guarded Patterns

You can add additional conditions to patterns with when clauses.

public static String process(Number num) {
    return switch (num) {
        case Integer i when i > 0 -> "Positive Integer: " + i;
        case Integer i -> "Other Integer: " + i;
        case Double d -> "Double: " + d;
        default -> "Unknown Number type!";
    };
}

3. Exhaustive Matching with sealed Classes

For sealed class hierarchies, switch ensures all subclasses are accounted for.

public sealed interface Shape permits Circle, Rectangle {}

public record Circle(double radius) implements Shape {}
public record Rectangle(double length, double width) implements Shape {}

public static String shapeInfo(Shape shape) {
    return switch (shape) {
        case Circle c -> "Circle with radius: " + c.radius();
        case Rectangle r -> "Rectangle with dimensions: " + r.length() + " x " + r.width();
    };
}

In this case, if you miss a subclass (like Rectangle), the compiler will throw an exhaustiveness error.

4. Null Handling Simplification

Switch patterns now handle null explicitly or exclude it in non-nullable cases.

public static void handleInput(String input) {
    switch (input) {
        case null -> System.out.println("Input is null!");
        case "SpecificValue" -> System.out.println("Matched SpecificValue");
        default -> System.out.println("Fallback case");
    }
}

Benefits of Improved Pattern Matching

  • Cleaner Code: Avoid type casts and complex if-else chains.
  • More Readable: Logic becomes more declarative and expressive.
  • Compile-Time Safety: Exhaustive checking ensures safer code.
  • Null-Safety: Simplifies handling of null values in branching.

These improvements make switch not just a control-flow statement but a powerful tool for type- and value-based pattern matching.

How to Write Simplified Entry Points with Java 25

Java 25 introduces several advancements focusing on simplified and modernized entry points to write cleaner main methods for applications. Here’s an overview of how to leverage these improvements to write simplified entry points:


Understanding Unnamed Classes and Instance Main

Java 25 introduces new features that make defining the main entry point of an application more flexible and concise.

1. Classless Main

You no longer need to define a named class with a main method. Instead, you can use a file containing only the main method logic by employing Unnamed Classes. This simplifies bootstrapping small Java programs.

Example:

void main() {
    System.out.println("Hello, World!");
}

Key Points:

  • The void main() behaves as a class-free entry point.
  • This reduces boilerplate (public class wrappers), improving readability for small programs and scripts.

2. Instance Main

The traditional static void main() requirement is relaxed to allow instance-level main methods. Instance main methods simplify cases when state or instance-specific contexts need initialization.

Example:

void main(String... args) {
    System.out.println("Arguments: " + String.join(", ", args));
}

Benefits:

  • No need to initialize a separate main instance for flexibility.
  • Useful for parameter handling or lightweight application state management.

Improved Argument Handling

Another subtle improvement is streamlined handling of command-line arguments. Java natively supports String... args expansions in a cleaner way with instance-level flexibility.

Example with arguments:

void main(String... args) {
    for (var arg : args) {
        System.out.printf("Received Arg: %s%n", arg);
    }
}

Better Alignment with Scripting Use Cases

Java 25 aims to make it easier to use Java for scripting-style tasks. The addition of Unnamed Classes combined with simplified main points brings Java closer to languages like Python or Kotlin for lightweight scripting purposes.

Example use case: A simple utility script:

void main() {
    int sum = java.util.stream.IntStream.range(1, 10).sum();
    System.out.println("Sum: " + sum);
}

How to Compile and Execute Simplified Java 25 Entry Points

  1. Save the code to a file (e.g., MyScript.java).
  2. Compile the code:
    javac MyScript.java
    
  3. Run the compiled file:
    java MyScript
    

For unnamed classes, simply use:

java MyScript.java

This eliminates the need for compiling separately before execution.


Advantages of Java 25 Simplified Entry Points

  • Less Boilerplate: No need for class wrappers or public static void definitions for lightweight applications.
  • Script-Like Usage: Java becomes better suited for quick, single-purpose scripts.
  • Enhanced Readability: Especially useful for quick prototyping or teaching Java.

Use Cases for Modern Java Entry Points

  • Scripting: Replace or complement command-line scripts.
  • Tiny CLI Tools: Build simple tools with minimal boilerplate effort.
  • Teaching Java: Simplify examples for teaching or early onboarding for new developers.

Java 25’s enhancements complement the move toward modern and developer-friendly Java programming. By introducing these features, Java bridges the gap between strict static typing and lightweight flexible scripting needs.

How to Leverage Unnamed Classes and Instance Main in Java 25

In Java 25, the introduction of classless main methods and unnamed classes significantly simplifies writing small programs, scripts, and experiments. Here’s how you can leverage these features effectively:


Classless Main Methods

This functionality is aimed at reducing boilerplate for small Java applications. You can now define a main method directly without wrapping it in a class. Here’s how it works:

Example:

void main() {
    System.out.println("Hello, Java 25!");
}

How to Run:

  • Save the code in a file (e.g., Hello.java).
  • Run it directly using the java command:
java Hello.java
  • Java 25 will automatically recognize the main method as the program entry point.

Unnamed Classes

Unnamed classes provide a way to write anonymous, throwaway code especially suited for quick scripts, utilities, or debugging. Unlike traditional classes, unnamed classes:

  • Do not require a name.
  • Are suitable for containing small amounts of logic that you don’t intend to reuse elsewhere.

Unnamed Class Example:

// Define a main method in an unnamed class
void main() {
    System.out.println("Hello from an unnamed class!");
    Runnable task = () -> System.out.println("Running a task!");
    task.run();
}

This code can live directly in a file like Program.java. Since unnamed classes aren’t intended to have reuse or complex naming, they simplify writing quick logic.


Benefits of Classless Main and Unnamed Classes

  1. Reduced Boilerplate:
    • No need to wrap the main method in a class when running scripts.
    • Great for beginners, scripts, or prototyping.
  2. Script-Like Feel:
    • The execution of .java files directly gives Java a more “script-like” experience.
  3. Quick Experiments:
    • Faster development loop for testing code snippets without creating entire project structures.
  4. Simplified Learning Curve:
    • Removes the complexity of classes for writing basic programs, aiding new learners.

Use Cases

  1. Prototyping:
    • Quickly test small pieces of logic or APIs.
  2. One-Off Scripts:
    • Automate tasks like file processing, network requests, or data transformation without setting up a full Java project.
  3. Education:
    • Ideal for learning Java as you can explore logic first and object-oriented concepts later.
  4. Debugging:
    • Use a single file to test specific functionality while debugging.

Key Details

  • Compatibility: Make sure you’re using Java 25 or above, as earlier versions don’t support these features.
  • Execution: The java command interprets single .java files directly.
  • Limitations:
    • These features are for simplicity and quick scripts. For larger applications, traditional class structures and best practices should be followed.

By leveraging classless main methods and unnamed classes in Java 25, you can write cleaner, more concise code faster than before!