Top 10 Java 9 Features: Comprehensive Insights

In this article, we will explore the Top 10 Java 9 Features in great details. So, let’s get started.

Java 9 Features

Key Java 9 Features

Java 9 brought significant improvements to the Java programming language and platform. Here are some key Java 9 features:

Modular System

The Modular system allows you to organize your code into discrete modules, making your projects more maintainable and scalable.

Module Declarations:

In Java 9, you declare a module using the module keyword in a module-info.java file. A module-info.java file defines what your module exports (makes accessible to other modules) and what it requires from other modules.

// module-info.java
module com.example.mymodule {
    requires some.other.module;
    exports com.example.mypackage;
}

In this example, the module com.example.mymodule requires the module some.other.module, and it exports the package com.example.mypackage.

Modulepath:

In Java 9, you compile and run your modules using the modulepath instead of the classpath. The modulepath contains module JAR files.

Module Compilation:

To compile a module, use the -d option to specify the output directory:



javac -d mods/com.example.mymodule src/com.example.mymodule/module-info.java src/com.example.mymodule/com/example/mypackage/MyClass.java

This command compiles your module and places the output in the mods directory.

Module Execution:

To run a module, use the --module option:



java --module-path mods -m com.example.mymodule/com.example.mypackage.MyClass

Here, you’re running the com.example.mymodule module with the com.example.mypackage.MyClass class.

Benefits:

  • Encapsulation: Modules allow you to hide internal details, making it easier to maintain and understand your codebase.
  • Dependency Management: You explicitly declare module dependencies, making it clear which modules your code relies on.
  • Strong Encapsulation: Modules enforce stronger encapsulation rules, ensuring that only the specified parts of a module are accessible.
  • Reduced Classpath Hell: Modules help avoid classpath issues and JAR hell often encountered in larger applications.
  • Improved Security: With explicit dependencies, you can control what your module exposes and what it depends on, enhancing security.

The Java 9 modular system is a significant step toward more maintainable and scalable Java applications, particularly in large and complex projects.

JShell

JShell, also known as the Java Shell tool, is a dynamic and interactive resource designed for educational purposes and Java code prototyping. Its inception occurred with the release of JDK 9, and it falls under the category of Read-Evaluate-Print Loop (REPL) tools. JShell excels at assessing declarations, statements, and expressions in real-time as you input them, providing instant feedback. This versatile tool can be initiated directly from the command line.

Why Opt for JShell?

JShell offers a more streamlined approach to Java programming. Instead of the conventional, multi-step process, JShell allows you to input program elements one by one, instantly witnessing their outcomes, and making immediate adjustments.

Traditional Java program development often involves the following steps:

  1. Write the entire program.
  2. Compile it, addressing any errors encountered.
  3. Execute the program.
  4. Identify and diagnose issues.
  5. Modify the code.
  6. Repeat the entire cycle.

JShell, on the other hand, simplifies this workflow by enabling you to experiment with code and explore various options in real-time during program development. Within a JShell session, you can test individual statements, experiment with different method variations, and explore unfamiliar APIs. It’s important to note that JShell complements rather than replaces integrated development environments (IDEs). While crafting your program, you can conveniently paste code into JShell for quick evaluation and then seamlessly integrate functional code from JShell back into your program editor or IDE.

Starting JShell

To initiate JShell, simply type the ‘jshell’ command in your terminal or command prompt.

$ jshell
|  Welcome to JShell -- Version 11.0.8
|  For an introduction type: /help intro

jshell> 

Basic Arithmetic

You can perform simple calculations in JShell, just like a calculator.

jshell> int x = 5;
x ==> 5

jshell> int y = 10;
y ==> 10

jshell> x + y
$3 ==> 15

Defining Methods

You can define methods interactively in JShell.

jshell> int add(int a, int b) {
   ...>     return a + b;
   ...> }
|  created method add(int,int)

jshell> add(7, 3)
$6 ==> 10

Testing Libraries

You can import and use libraries in JShell.

jshell> import java.util.ArrayList;

jshell> ArrayList<String> list = new ArrayList<>();
list ==> []

jshell> list.add("Java");
$9 ==> true

jshell> list.add("is");
$10 ==> true

jshell> list.add("awesome!");
$11 ==> true

jshell> list
$12 ==> [Java, is, awesome!]

Class Definitions

You can define classes and use them interactively.

jshell> class Person {
   ...>     String name;
   ...>     int age;
   ...>     Person(String name, int age) {
   ...>         this.name = name;
   ...>         this.age = age;
   ...>     }
   ...> }
|  created class Person

jshell> Person person1 = new Person("Alice", 30);
person1 ==> Person[name=Alice, age=30]

Exiting JShell

Please enter /exit to exit Shell.

jshell> /exit
|  Goodbye

Improved Stream API

Java 9 introduced several improvements to the Stream API, enhancing its functionality and making it more powerful for working with sequences of data. Here are some of the key improvements with code examples:

takeWhile and dropWhile Methods

  • takeWhile: Returns elements from the beginning of the stream until a specified condition is met.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

List<Integer> result = numbers.stream()
    .takeWhile(n -> n < 6)
    .collect(Collectors.toList());

// Result: [1, 2, 3, 4, 5]
  • dropWhile: Skips elements from the beginning of the stream until a specified condition is met.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

List<Integer> result = numbers.stream()
    .dropWhile(n -> n < 6)
    .collect(Collectors.toList());

// Result: [6, 7, 8, 9, 10]

ofNullable Method

  • ofNullable allows you to create a stream containing a single element if that element is non-null, or an empty stream if the element is null.
String value = "Hello, World!";

Stream<String> stream = Stream.ofNullable(value);

// Result: Stream of "Hello, World!"

Iterate Method with Predicate

Java 9 introduced an overloaded iterate method that takes a Predicate to limit the stream elements.

Stream.iterate(1, n -> n < 10, n -> n * 2)
    .forEach(System.out::println);

// Result: 1 2 4 8

forEachOrdered Method

forEachOrdered ensures that the order of elements in the stream is maintained when performing parallel operations.

List<Integer> numbers = Arrays.asList(1, 3, 2, 4, 6, 5);

numbers.parallelStream()
    .map(n -> n * 2)
    .forEachOrdered(System.out::println);

// Result: 2 6 4 8 12 10 (Order is preserved)

New HTTP Client

Java 9 introduced a new HTTP Client API that provides improved support for working with HTTP requests and responses.

The new HTTP Client offers several advantages and improvements over the old HttpURLConnection class, which was the primary way to perform HTTP requests in previous versions of Java. Here are some key differences:

  1. Ease of Use: The new HTTP Client provides a more user-friendly and intuitive API for making HTTP requests. It reduces the amount of boilerplate code required to perform common HTTP operations.
  2. Asynchronous Support: Java 9’s HTTP Client supports asynchronous requests, making it easier to perform non-blocking HTTP operations. This is crucial for building scalable and responsive applications.
  3. HTTP/2 Support: The new HTTP Client supports the HTTP/2 protocol by default. HTTP/2 is a more efficient and faster protocol compared to HTTP/1.1, supported by the old HttpURLConnection.
  4. Request and Response Bodies: Handling request and response bodies is more straightforward with the new HTTP Client. You can easily work with different body types, including JSON, form data, and custom content.
  5. Response Handling: The response handling is more flexible, allowing you to choose between various response body handlers, such as ofString(), ofByteArray(), or custom handlers.
  6. Timeouts and Redirects: Java 9’s HTTP Client offers better control over timeouts and automatic redirects, ensuring robust behavior in various network conditions.
  7. Cookie Management: It provides built-in support for managing cookies, simplifying the handling of sessions and stateful interactions with web services.
  8. Proxy Configuration: The new HTTP Client allows you to configure proxy servers more easily, which is essential for many enterprise applications.
  9. Customization: You can customize the HTTP Client’s behavior through various builders, making it adaptable to different use cases.
  10. Modern API: The new HTTP Client aligns with modern best practices and design patterns for working with HTTP, making it more appealing to developers familiar with other programming languages.

Here’s an example of how to use the new HTTP Client API in Java 9:

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class HttpClientExample {
    public static void main(String[] args) throws IOException, InterruptedException {
        // Create an instance of HttpClient
        HttpClient httpClient = HttpClient.newHttpClient();

        // Define the URL you want to send an HTTP GET request to
        URI uri = URI.create("https://jsonplaceholder.typicode.com/posts/1");

        // Create an HTTP request
        HttpRequest request = HttpRequest.newBuilder()
                .uri(uri)
                .GET() // Use the GET method
                .build();

        // Send the request and receive the response
        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

        // Check the HTTP status code
        int statusCode = response.statusCode();
        System.out.println("HTTP Status Code: " + statusCode);

        // Print the response body
        String responseBody = response.body();
        System.out.println("Response Body:\n" + responseBody);
    }
}

Enhanced Process API

Java 9 introduced enhancements to the Process API, making it more powerful and flexible for managing and interacting with native processes.

The enhanced Process API offers several improvements and differences compared to the old Process API (available before Java 9). Here are some key differences:

  1. Richer Process Information: The new Process API provides richer information about processes. It allows you to retrieve details like the command used to start a process, its arguments, start time, total CPU duration, and more. In contrast, the old Process API had limited capabilities in terms of gathering information about processes.
  2. Interaction with Processes: With the enhanced Process API, you can more effectively interact with processes. You can retrieve information about running processes, manipulate processes, and monitor their states. The old API had limited support for such operations.
  3. Improved Filtering: The Process API in Java 9 allows you to filter processes based on criteria like command, arguments, and other attributes. This makes it easier to find and work with specific processes that meet certain conditions. The old API lacked advanced filtering capabilities.
  4. Native Process Handle: In Java 9, the ProcessHandle class represents a native process handle, which provides more control and information about a process. The old API used Process and Runtime classes for process management, which were less versatile.
  5. Asynchronous Operations: The enhanced API allows asynchronous operations like registering callbacks for process exit events. This is particularly useful for scenarios where you need to perform actions when a process exits. The old API didn’t provide built-in support for asynchronous operations.
  6. Easier Process Creation: While both APIs support process creation, the enhanced Process API offers more flexibility and control over the created processes. It simplifies the process creation and management tasks.
  7. Stream-Based Approach: The enhanced Process API is designed in a more stream-oriented fashion, making it easier to work with process information and perform operations on process streams.

Here’s an example demonstrating some of these enhancements:

import java.io.IOException;
import java.util.List;
import java.util.Optional;

public class ProcessApiExample {
    public static void main(String[] args) {
        // Run a simple process
        try {
            ProcessBuilder processBuilder = new ProcessBuilder("echo", "Hello, Java 9 Process API!");
            Process process = processBuilder.start();
            int exitCode = process.waitFor();
            System.out.println("Process exited with code: " + exitCode);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }

        // Get the current process info
        ProcessHandle currentProcess = ProcessHandle.current();
        System.out.println("Current Process ID: " + currentProcess.pid());

        // List all running processes
        ProcessHandle.allProcesses()
                .limit(10)
                .forEach(ProcessApiExample::printProcessInfo);

        // Find and manipulate a specific process
        Optional<ProcessHandle> optionalProcess = ProcessHandle.allProcesses()
                .filter(ph -> ph.info().command().orElse("").contains("java"))
                .findFirst();

        optionalProcess.ifPresent(processHandle -> {
            System.out.println("Found Java process: " + processHandle.pid());
            ProcessHandle.Info processInfo = processHandle.info();
            System.out.println("Command: " + processInfo.command().orElse("N/A"));
            System.out.println("Arguments: " + String.join(" ", processInfo.arguments().orElse(List.of())));
            System.out.println("Start Time: " + processInfo.startInstant().orElse(null));
            System.out.println("CPU Duration: " + processInfo.totalCpuDuration().orElse(null));
        });
    }

    private static void printProcessInfo(ProcessHandle processHandle) {
        ProcessHandle.Info processInfo = processHandle.info();
        System.out.println("PID: " + processHandle.pid());
        System.out.println("Command: " + processInfo.command().orElse("N/A"));
        System.out.println("Start Time: " + processInfo.startInstant().orElse(null));
        System.out.println("Total CPU Duration: " + processInfo.totalCpuDuration().orElse(null));
        System.out.println("--------------------------------------------------");
    }
}

Private Methods in Interfaces

In Java 9, one of the notable features introduced was the ability to include private methods in interfaces. Prior to Java 9, interfaces could only contain abstract method declarations. The introduction of private methods in interfaces provides more flexibility and reusability when developing interface-based APIs. Here’s an overview of this feature:

Private Methods in Interfaces:

  • Private methods in interfaces are methods that are defined with the private modifier within an interface. These methods are not visible to implementing classes and can only be used within the interface itself.
  • The primary purpose of private methods in interfaces is to break down complex or repetitive code into smaller, reusable units, improving code readability and maintainability.
  • These private methods can be utilized by other interface methods, including default and static methods, to avoid code duplication and promote code modularization.

Example of Using Private Methods in Interfaces:

public interface Calculator {
    // Public method that utilizes a private method
    default int add(int a, int b) {
        return performAddition(a, b);
    }

    // Private method for performing addition
    private int performAddition(int a, int b) {
        return a + b;
    }

    // Another public method
    default int subtract(int a, int b) {
        return a - b;
    }
}

In this example, the Calculator interface defines a private method performAddition() to handle addition. The add() method utilizes this private method for performing addition. Implementing classes are not aware of the existence of performAddition() as it is private to the interface.

Benefits of Private Methods in Interfaces:

  1. Code Reusability: Private methods allow you to reuse common logic within an interface, reducing code duplication and promoting the DRY (Don’t Repeat Yourself) principle.
  2. Code Readability: By breaking down complex operations into private methods, interfaces become more readable and maintainable.
  3. Encapsulation: Private methods are encapsulated within the interface, preventing implementing classes from accessing or modifying them.
  4. Evolution of APIs: Adding private methods to interfaces is a non-breaking change, which means existing code that implements the interface remains compatible when new private methods are added.

Try-With-Resources Enhancement

Prior to Java 9, try-with-resources has a limitation that requires resource to declare locally within its block.

Pre Java 9 Code

import java.io.*;

public class TryWithResourcesExample {
    public static void main(String[] args) {
        try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

With Java 9, we can declare and initialize the resources outside the try-with-resources block.

Java 9 try-with-resources Code

import java.io.*;

public class TryWithResourcesJava9Example {
    public static void main(String[] args) {
        // Declare and initialize the BufferedReader outside the try-with-resources block
        BufferedReader reader = new BufferedReader(new FileReader("example.txt"));
        
        try (reader) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Diamond Operator for Anonymous Inner Classes

In Java 9, the diamond operator (<>) was enhanced to allow its usage with anonymous inner classes. This feature simplifies the creation of anonymous inner classes by inferring the generic type arguments. Here’s an example:

import java.util.*;

public class DiamondOperatorExample {
    public static void main(String[] args) {
        // Java 7 and earlier: Creating an anonymous inner class for ArrayList
        List<String> list1 = new ArrayList<String>() {
            {
                add("Java");
                add("is");
                add("awesome");
            }
        };

        // Java 9: Using the diamond operator with anonymous inner class
        List<String> list2 = new ArrayList<>() {
            {
                add("Java");
                add("is");
                add("awesome");
            }
        };

        System.out.println("List 1: " + list1);
        System.out.println("List 2: " + list2);
    }
}

In this Java 9 code example, we have two lists, list1 and list2. list1 uses the older Java 7 style of creating an anonymous inner class for an ArrayList with explicit generic type arguments. In contrast, list2 uses the diamond operator (<>) to create an anonymous inner class without specifying the generic type arguments explicitly. Java 9 infers the generic type arguments from the left-hand side of the assignment, making the code cleaner and more concise.

Multi-Release JARs

In Java 9, a new feature called “Multi-Release JARs” was introduced to support the coexistence of multiple versions of Java classes within a single JAR (Java Archive) file. This feature is particularly useful when you want to maintain backward compatibility with older Java versions while taking advantage of new Java features in a single JAR file.

Here’s an explanation of Multi-Release JARs and how to use them:

What are Multi-Release JARs?

Multi-Release JARs allow you to include different versions of class files within the same JAR file, each version targeting a specific Java SE version. When your Java application runs on a Java SE version that supports Multi-Release JARs (Java 9 and later), the Java runtime environment will automatically select and load the appropriate version of a class based on the runtime’s Java version. This ensures that your application can take advantage of newer Java features on newer Java versions while remaining compatible with older Java versions.

How to Create a Multi-Release JAR:

To create a Multi-Release JAR, you need to organize your JAR file’s structure in a specific way:

  1. Root Directory (META-INF/): The root directory of your JAR file should contain a META-INF directory.
  2. META-INF/versions Directory: Inside the META-INF directory, create a subdirectory named versions. This directory will house multiple directories, each corresponding to a specific Java SE version.
  3. Version-Specific Directories: Within the versions directory, create subdirectories named after the targeted Java SE versions. For example, if you want to provide different versions for Java SE 8 and Java SE 9, you would create directories named 8 and 9.
  4. Class Files: Place version-specific class files in their respective directories. The structure should look like this:
your-jar-file.jar
└─ META-INF/
   └─ versions/
      ├─ 8/
      │  └─ com/
      │     └─ example/
      │        └─ YourClass.class (for Java 8)
      ├─ 9/
         └─ com/
            └─ example/
               └─ YourClass.class (for Java 9)

How Multi-Release JARs Work:

When your application runs on a Java 9 or later JVM, it will automatically select the version-specific class files from the appropriate directory (e.g., 8 or 9) based on the runtime’s Java version. If it’s running on Java 8 or an older version, it will ignore the version-specific directories and use the default class files.

This allows you to maintain a single JAR file that can run on multiple Java versions, utilizing newer language features when available without breaking compatibility with older versions.

Use Cases:

  • Providing performance improvements or bug fixes for newer Java versions.
  • Taking advantage of new language features without sacrificing compatibility.
  • Supporting a transition period when users might be on different Java versions.

Multi-Release JARs are a powerful feature for library developers and anyone creating Java applications that need to run across a range of Java versions while leveraging the capabilities of each version.

Stack-Walking API

In Java 9, the Stack-Walking API was introduced as a powerful tool for inspecting and traversing the call stack of the current thread. It allows you to programmatically access information about method calls and stack frames, which can be valuable for debugging, profiling, and monitoring applications. Here, we’ll explore the Stack-Walking API with some code examples.

Simple Stack Walking:

Here’s a basic example of how to use the Stack-Walking API to walk through the current thread’s call stack:

import java.util.List;

public class StackWalkingExample {
    public static void main(String[] args) {
        StackWalker walker = StackWalker.getInstance();
        walker.forEach(frame -> {
            System.out.println("Class: " + frame.getClassName());
            System.out.println("Method: " + frame.getMethodName());
            System.out.println("Line Number: " + frame.getLineNumber());
        });
    }
}

In this example, we obtain a StackWalker instance and use the forEach method to iterate through the frames of the call stack. For each frame, we print the class name, method name, and line number.

Filtering and Limiting:

You can filter and limit the frames you want to inspect. Here’s an example of filtering frames to include only those from a specific package:

import java.util.List;
import java.util.stream.Collectors;

public class StackWalkingFilterExample {
    public static void main(String[] args) {
        StackWalker walker = StackWalker.getInstance();
        List<StackWalker.StackFrame> frames = walker.walk(frames -> frames
                .filter(frame -> frame.getClassName().startsWith("com.example"))
                .collect(Collectors.toList()));

        frames.forEach(frame -> {
            System.out.println("Class: " + frame.getClassName());
            System.out.println("Method: " + frame.getMethodName());
            System.out.println("Line Number: " + frame.getLineNumber());
        });
    }
}

In this example, we filter frames to include only those whose class names start with “com.example.”

Accessing Caller Frame:

You can also access the caller’s frame using the getCallerFrame method:

public class StackWalkingCallerExample {
    public static void main(String[] args) {
        StackWalker walker = StackWalker.getInstance();
        walker.forEach(frame -> {
            StackWalker.StackFrame callerFrame = frame.getCallerFrame();
            if (callerFrame != null) {
                System.out.println("Caller Class: " + callerFrame.getClassName());
                System.out.println("Caller Method: " + callerFrame.getMethodName());
            }
        });
    }
}

This example prints information about the caller frame of each frame in the call stack.

The Stack-Walking API provides fine-grained control over stack traversal and is particularly useful for diagnosing issues, profiling code, and implementing custom debugging tools. It enhances the capabilities of Java developers to understand and interact with the runtime call stack.

Conclusion: Java 9 Features

In this article, we’ve delved into the top Java 9 features in great detail. Java 9 brought significant enhancements to the language and platform. Let’s summarize the key takeaways:

  1. Modular System: Java 9 introduced a modular system that allows developers to organize code into discrete modules. This improves project maintainability and scalability by encapsulating internal details and managing dependencies more effectively.
  2. JShell: JShell, the Java Shell tool, provides a dynamic and interactive environment for learning and prototyping Java code. It simplifies the development process by allowing you to experiment with code in real-time.
  3. Improved Stream API: Java 9 enhanced the Stream API with features like takeWhile and dropWhile for better sequence data manipulation. It introduced methods like ofNullable and improved forEachOrdered for stream processing.
  4. New HTTP Client: Java 9 introduced a modern and versatile HTTP Client API, which simplifies working with HTTP requests and responses. It supports asynchronous operations, HTTP/2, better response handling, and more.
  5. Enhanced Process API: The Process API in Java 9 provides richer information about native processes, including interaction, filtering, and asynchronous operations. It offers improved capabilities compared to the old Process API.
  6. Private Methods in Interfaces: Java 9 allows private methods in interfaces, promoting code reuse, readability, and encapsulation within interface-based APIs.
  7. Try-With-Resources Enhancement: Java 9 allows resources to be declared and initialized outside the try-with-resources block, making code more concise and readable.
  8. Diamond Operator for Anonymous Classes: The diamond operator (<>) can be used with anonymous inner classes in Java 9, simplifying their creation.
  9. Multi-Release JARs: Java 9 introduced Multi-Release JARs, enabling the coexistence of multiple Java class versions within a single JAR, making it easier to maintain compatibility across Java versions.
  10. Stack-Walking API: The Stack-Walking API provides powerful tools for inspecting and traversing the call stack of a thread, aiding in debugging, profiling, and monitoring applications.

These Java 9 features offer developers improved code organization, more interactive development tools, enhanced APIs, and better control over processes and resources.

Leave a Reply

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