Six years after its release, Java 11 remains a cornerstone of the enterprise software world. As the second Long-Term Support (LTS) release following the groundbreaking Java 8, it introduced a wealth of features that refined the language, modernized core APIs, and improved performance. While newer LTS versions like Java 17 and 21 continue to push the envelope with features from Project Loom and Project Valhalla, understanding the advancements in Java 11 is crucial for any developer working on mature codebases or planning a migration from older versions. This release marked a significant step in Java’s evolution, balancing innovation with the stability that the ecosystem demands.

This article provides a comprehensive technical overview of the most impactful news and features introduced in Java 11. We’ll explore the standardized HTTP Client, powerful new String and File methods, and subtle but important language changes. Through practical code examples and best practices, you’ll gain actionable insights into how these features can streamline your development process, enhance application performance, and align your projects with the modern Java ecosystem, which includes a vibrant community around OpenJDK, Adoptium, and various distributions like Azul Zulu and Amazon Corretto.

A New Era of HTTP Communication: The Standardized HttpClient

One of the most anticipated features of Java 11 was the standardization of a new, modern HTTP client API. For years, developers relied on the cumbersome and outdated HttpURLConnection or pulled in third-party libraries like Apache HttpClient or OkHttp for non-trivial tasks. Java 11 delivered a powerful, fluent, and asynchronous alternative right in the JDK under the java.net.http package, a feature formalized in JEP 321.

From Incubator to Standard

The new HttpClient was first introduced as an incubator module in Java 9 and 10, allowing the community to provide feedback before its finalization. Its design goals were to replace the legacy HttpURLConnection API with a simpler, more powerful interface that supports modern web protocols and programming paradigms. Key benefits include:

  • Asynchronous Operations: It provides both synchronous (blocking) and asynchronous (non-blocking) programming models, natively integrating with CompletableFuture for elegant handling of concurrent requests.
  • Modern Protocol Support: It supports HTTP/1.1, HTTP/2 out of the box, and WebSockets. HTTP/2 support offers significant performance benefits through features like multiplexing and server push.
  • Reactive Streams Integration: The API integrates with the Reactive Streams API, allowing for efficient handling of data streams, which is crucial for large request or response bodies.
  • Fluent Builder API: Creating clients and requests is intuitive and readable thanks to a modern builder pattern.

Practical Example: Making Asynchronous Requests

Let’s see how easy it is to make a non-blocking GET request to a REST API and process the response. This example fetches JSON data from a public API and prints the response body once it’s available, without blocking the main thread.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class ModernHttpClientExample {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // 1. Create an HttpClient with default settings.
        // The client can be reused for multiple requests.
        HttpClient client = HttpClient.newHttpClient();

        // 2. Build an HttpRequest using the fluent builder API.
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://jsonplaceholder.typicode.com/todos/1"))
                .header("Accept", "application/json")
                .GET() // This is the default, can be omitted.
                .build();

        // 3. Send the request asynchronously.
        // The sendAsync method returns a CompletableFuture<HttpResponse<String>>.
        // HttpResponse.BodyHandlers.ofString() specifies that the response body should be handled as a String.
        CompletableFuture<HttpResponse<String>> responseFuture =
                client.sendAsync(request, HttpResponse.BodyHandlers.ofString());

        // 4. Process the response when it's available.
        // We can chain actions to be executed upon completion.
        responseFuture.thenApply(HttpResponse::body)
                      .thenAccept(System.out::println)
                      .join(); // Wait for the future to complete for this demo.

        System.out.println("Request sent and processing started. Main thread can do other work.");
    }
}

This approach is a massive improvement, promoting better resource utilization and application responsiveness. This is a key piece of Java concurrency news from that era, setting the stage for more advanced reactive programming models.

Streamlining Common Operations: String and File API Updates

Java 11 also brought a collection of highly practical, quality-of-life improvements to some of the most commonly used classes in the JDK: String and Files. These new methods reduce boilerplate code and make everyday programming tasks more expressive and less error-prone.

Java 11 logo - Jdk Archives • The Learn Programming Academy
Java 11 logo – Jdk Archives • The Learn Programming Academy

More Power to Your Strings

The java.lang.String class received several new instance methods that address common pain points:

  • isBlank(): Returns true if the string is empty or contains only white space characters. This is more intuitive than writing str.trim().isEmpty().
  • lines(): Returns a Stream<String> of lines extracted from the string, separated by line terminators. This is a fantastic way to process multi-line text.
  • strip(), stripLeading(), stripTrailing(): These are Unicode-aware alternatives to trim(). While trim() only removes characters with codepoints less than or equal to U+0020, the strip() methods correctly handle all Unicode white space.
  • repeat(int): Repeats the string a specified number of times. A simple but welcome utility.

Code in Action: String Manipulation

Let’s combine some of these new methods to process a block of text. In this example, we’ll take a multi-line string, split it into lines, remove any blank lines, and then trim the whitespace from the remaining lines before printing them.

import java.util.stream.Collectors;

public class StringApiEnhancements {

    public static void main(String[] args) {
        String multiLineText = """
                               
                Java 11 brought many improvements.
                   This is a line with leading and trailing spaces.   
                               
                The lines() method is very useful.
                """;

        System.out.println("--- Processing with Java 11 String API ---");

        multiLineText.lines()
                     .filter(line -> !line.isBlank()) // Filter out blank lines
                     .map(String::strip)             // Use Unicode-aware trimming
                     .forEach(System.out::println);

        System.out.println("\n--- Repeating a string ---");
        String separator = "-".repeat(20);
        System.out.println(separator);
    }
}

Simplified File I/O

Reading and writing files is a fundamental task. Before Java 11, writing a string to a file or reading a file’s entire content into a string required several lines of boilerplate code. The java.nio.file.Files class introduced two new static methods to simplify this dramatically.

  • writeString(Path, CharSequence, ...): Writes a character sequence to a file.
  • readString(Path, ...): Reads all content from a file into a string.

These methods handle opening and closing the file resources for you, making the code cleaner and safer.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public class FileApiExample {

    public static void main(String[] args) {
        try {
            Path filePath = Path.of("demofile.txt");

            // Write a string to a file, overwriting if it exists
            System.out.println("Writing to file: " + filePath.toAbsolutePath());
            Files.writeString(filePath, "Hello from Java 11!\nThis is a new line.");

            // Append to the same file
            Files.writeString(filePath, "\nAppending more text.", StandardOpenOption.APPEND);

            // Read the entire file content into a string
            String content = Files.readString(filePath);
            System.out.println("\nContent read from file:");
            System.out.println(content);

            // Clean up the file
            Files.delete(filePath);
            System.out.println("\nDeleted the file.");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Language Evolution: `var` in Lambdas and Module Removals

Java 11 continued the language’s evolution with refinements that, while subtle, enable cleaner and more powerful code. It also took a significant step in slimming down the JDK by removing deprecated modules, a move that impacts dependency management for many projects.

Local-Variable Syntax for Lambda Parameters (JEP 323)

Java 10 introduced local-variable type inference with the var keyword, which was a huge win for reducing verbosity. Java 11 extended this capability to lambda expression parameters. While you could already omit the types in a lambda (e.g., (s1, s2) -> ...), you couldn’t add annotations to those implicitly typed parameters.

Using var allows you to have the conciseness of type inference while retaining the ability to add annotations. This is particularly useful with frameworks and tools that use annotations for validation, nullability analysis, or dependency injection.

Java 11 logo - Java 11 to Java 17 upgrade journey
Java 11 logo – Java 11 to Java 17 upgrade journey
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

// Assume @NonNull is an annotation from a library like Checker Framework or Lombok
import javax.annotation.Nonnull;

public class VarInLambdaExample {

    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

        // With Java 11, you can use 'var' to add annotations to lambda parameters.
        // This was not possible with '(s) -> ...' syntax.
        String result = names.stream()
                             .map((@Nonnull var name) -> name.toUpperCase())
                             .collect(Collectors.joining(", "));

        System.out.println(result); // ALICE, BOB, CHARLIE
    }
}

The Great Cleanup: Removal of Java EE and CORBA Modules

As part of an ongoing effort to modernize and slim down the Java SE Platform, Java 11 removed the Java EE and CORBA modules. These modules, which included technologies like JAXB (XML Binding), JAX-WS (Web Services), and Common Annotation, were deprecated in Java 9. Their removal means they are no longer bundled with the JDK.

This is significant for applications migrating from Java 8 that relied on these APIs. The solution is to add them back as explicit dependencies in your build configuration. This change aligns with the industry trend of decoupling these enterprise specifications from the core JDK, allowing them to evolve independently, now under the stewardship of the Eclipse Foundation as part of the Jakarta EE news.

If your project needs JAXB, you must add the following dependencies to your Maven pom.xml:

<!-- pom.xml dependencies for a project needing JAXB on Java 11+ -->
<dependencies>
    <dependency>
        <groupId>jakarta.xml.bind</groupId>
        <artifactId>jakarta.xml.bind-api</artifactId>
        <version>3.0.1</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jaxb</groupId>
        <artifactId>jaxb-runtime</artifactId>
        <version>3.0.2</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>

This is a critical consideration for any migration effort and highlights the importance of tools like Maven and Gradle in managing the modern Java ecosystem.

Under the Hood: JVM Improvements and Best Practices

Java version history - Java version history: what does
Java version history – Java version history: what does “2019 (commercial)” mean? – Stack …

Beyond the language and API features, Java 11 delivered important updates to the JVM itself, focusing on performance, security, and observability.

Performance and Security Enhancements

Java 11 introduced several key JVM features that provided more options for tuning and securing applications:

  • ZGC (Experimental): JEP 333 introduced the Z Garbage Collector, a scalable, low-latency GC designed for applications that require pause times of less than 10 milliseconds and can handle heaps ranging from hundreds of megabytes to many terabytes.
  • Epsilon GC: JEP 318 introduced a “no-op” garbage collector that handles memory allocation but does not implement any memory reclamation mechanism. It’s useful for performance testing, measuring application overhead, and for extremely short-lived, memory-conscious applications.
  • TLS 1.3 Support: JEP 332 added support for the latest version of the Transport Layer Security protocol, providing significant security and performance improvements over older versions.
  • Flight Recorder: The low-overhead data collection framework, previously a commercial feature in Oracle JDK, was open-sourced and made available in OpenJDK, providing a standard for diagnosing and profiling production applications.

Best Practices for Adopting Java 11

  1. Analyze Dependencies: Before migrating, thoroughly scan your project for dependencies on removed Java EE and CORBA modules. Use build tools to add the necessary third-party libraries.
  2. Modernize Your HTTP Code: If your application makes HTTP calls using legacy APIs, plan to refactor them to use the new HttpClient. This will improve performance and make your code more maintainable.
  3. Leverage New APIs: Encourage your team to adopt the new String and Files methods to reduce boilerplate and improve code readability. Set up static analysis rules to suggest these new methods.
  4. Choose the Right JDK Distribution: The Java ecosystem news is full of great choices. Evaluate OpenJDK builds from vendors like Adoptium (Eclipse Temurin), Oracle, Azul, Amazon, and BellSoft to find the one that best fits your support and performance needs.

Conclusion: A Vital Step in Java’s Journey

Java 11 was more than just an incremental update; it was a foundational LTS release that solidified the platform’s direction for the future. It delivered a modern, non-blocking HTTP client, cleaned up the JDK by decoupling enterprise modules, and added dozens of convenience methods that make daily coding more pleasant and productive. Its contributions to the JVM, including new garbage collectors and open-sourced tooling, have had a lasting impact on Java performance and observability.

For teams still on Java 8, migrating to a modern LTS version like 11, 17, or 21 is a strategic imperative. Java 11 serves as an excellent and stable milestone, offering a compelling blend of modern features and widespread ecosystem support. By embracing the changes it introduced, developers can build more robust, performant, and maintainable applications, ensuring they are well-positioned to take advantage of the continuous innovation happening across the Java landscape.