Skip to main content

Lancing JDK 19

Topic: JDK Release                                                                                                  Level: Basic

Introducing JDK 19

Java 19 is the 10th release since the transition to the 6-month release cadence of new JDK features, comprising of 7 JDK Enhancement Proposals.

With the ever-releasing new JDK features catching up on the latest version could be a daunting task. However it is a gift in disguise, with the shorter releases, 

  • Applications can effortlessly migrate to the successor versions by incorporating fewer minimal additions/deprecations in the features rather than having big bang migration housing innumerable changes.
  • The steep learning curve and adaptation curve on the features introduced can be significantly lessened as the release cycles are periodic and incremental.
  • Nurtures prompt experimentation and feedback for improvement and development of the released features (incubate and preview).
Focusing on LTS (Long Term Support) release candidates, the JDK 19 is a non-LTS version and JDK 17 is the latest and greatest LTS and the next one per schedule is the JDK 21 (due September 2023)
java_19_illustration
Image source: oracle.com

Constitutes of JDK 19

  1. (JEP 428) Structured Concurrency (Incubator)
    • Improve the maintainability, reliability, and observability of your multithreaded code.
  2. (JEP 405) Record Patterns (Preview)
    • Discover how to extend pattern matching to express more sophisticated, composable data queries.
  3. (JEP 424) Foreign Function and Memory API (Preview)
    • Enable interoperability with code and data outside the Java runtime.
  4. (JEP 425) Virtual Threads (Preview)
    • Reduce the effort of writing, maintaining, and observing high-throughput, concurrent applications.
  5. (JEP 427) Pattern Matching for Switch (Third Preview)
    • Expand the expressiveness and applicability of switch expressions and statements by allowing patterns to appear in case labels.
  6. (JEP 426) Vector API (Fourth Incubator)
    • Express vector computations that reliably compile at runtime to optimal vector instructions on supported CPU architectures, thus achieving performance superior to equivalent scalar computations.
  7. (JEP 422) Linux/RISC-V Support
    • With the increasing availability of RISC-V hardware, access the JDK to Linux/RISC-V to expand your hardware reach.
The Java ecosystem compromises the below projects such that each of the JDK Enhancement Proposals (JEP) can be organised in their respective project umbrella,
  • Project Amber - Continuously improve developer productivity through evolutions of the Java language
    • 405: Record Patterns (Preview)
    • 427: Pattern Matching for Switch (Third Preview)
  • Project Leyden - Improve start-up time to achieve peak performance
  • Project Loom - Massively scale lightweight threads, making concurrency simple
    • 425: Virtual Threads (Preview)
    • 428: Structured Concurrency (Incubator)
  • Project Panama - High performance with easier creation of I/O intensive apps through Java-native platform changes (library)
    • 424: Foreign Function and Memory API (Preview)
    • 426: Vector API (Fourth Incubator)
    • 422: Linux/RISC-V Port
  • Project Valhalla - Higher memory density, better performance of ML and big data apps through the introduction of value types
  • Project ZGC - Create a scalable low latency garbage collector capable of handling large heaps

On to the Lancing Details,

405: Record Patterns (Preview)

Introduced from JDK 16 onwards, the following code snippet compiles

//Old code
if (o instanceof String) {
    String s = (String)o;
    ... use s ...
}
//New code
if (o instanceof String s) {
    ... use s ...
}

In the snippet heed from the //New code, that when the parameter passed 'o' is found to be an instance belonging to String then the value of 'o' will be assigned to the pattern variable 's', so as to avoid the cast that if to follow when consuming (ref. above // Old code)

From the JDK 17 and 18 ahead, the type pattern ideology has been applied to the switch case labels.

Using Pattern matching and record classes,

record Point(int x, int y) {}
static void printSum(Object o) {
    if (o instanceof Point p) { /* pattern variable p is used to extract the values x() and y() */
        int x = p.x();
        int y = p.y();
        System.out.println(x+y);
        }
}
//New code
record Point(int x, int y) {}
void printSum(Object o) {
    if (o instanceof Point(int x, int y)) {
    System.out.println(x+y);
    }
}
In the above code, when the parameter passed 'o' is of a Point record type then rather than assigning the pattern variable 'p' for extracting the values, declare the record pattern such that variables are extracted and initialized via the record accessor methods x() and y().

//Advanced example,
record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}

static void printUpperLeftColoredPoint(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
         System.out.println(ul.c());
    }
}
In the above code, Rectangle is a record pattern containing ColoredPoint records which are extracted and initialized via the Rectangle record defined accessor methods, ie. Record.ColoredPoint().

The ColoredPoint is in turn a record of its own containing Point record and Enum, here the records are nested as such the notion of extracting and initializing the record variables applies as below defined code,


static void printColorOfUpperLeftPoint(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint(Point p, Color c),
                               ColoredPoint lr)) {
        System.out.println(c);
    }
}

static void printXCoordOfUpperLeftPointWithPatterns(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint(Point(var x, var y), var c),
                               var lr)) {
        System.out.println("Upper-left corner: " + x);
    }
}

The record Point performs type inference and var is used which gets resolved on runtime.

427: Pattern Matching for Switch (Third Preview)

Introduced in JDK 17,

static String formatter(Object o) {
    String formatted = "unknown";
    if (o instanceof Integer i) {
        formatted = String.format("int %d", i);
    } else if (o instanceof Long l) {
        formatted = String.format("long %d", l);
    } else if (o instanceof Double d) {
        formatted = String.format("double %f", d);
    } else if (o instanceof String s) {
        formatted = String.format("String %s", s);
    }
    return formatted;
}

Constructing the case labels of pattern type transforms the if-else construct as,

static String formatterPatternSwitch(Object o) {
    return switch (o) {
        case Integer i -> String.format("int %d", i);
        case Long l    -> String.format("long %d", l);
        case Double d  -> String.format("double %f", d);
        case String s  -> String.format("String %s", s);
        default        -> o.toString();
    };
}

Integrate the null test into the switch by allowing a new null case label:

static void testFooBar(String s) {
    switch (s) {
        case null         -> System.out.println("Oops");
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}

The Guarded Pattern by evaluating a condition and executing only if the same holds true,

static void test(Object o) {
    switch (o) {
        case String s when s.length() == 1 -> ...
        case String s                      -> ...
        ...
    }
}

The first clause matches if the Object 'o' is both a String and of length 1. The second case matches if the Object 'o' is a String of any length.

The type of the selector expression be either an integral primitive type (excluding long) or any reference type, an enum type, a record type, and an array type, along with a null case label and a default:

record Point(int i, int j) {}
enum Color { RED, GREEN, BLUE; }
static void typeTester(Object o) {
    switch (o) {
        case null     -> System.out.println("null");
        case String s -> System.out.println("String");
        case Color c  -> System.out.println("Color: " + c.toString());
        case Point p  -> System.out.println("Record class: " + p.toString());
        case int[] ia -> System.out.println("Array of ints of length" + ia.length);
        default       -> System.out.println("Something else");
    }
}

Dominance of Pattern labels

The String pattern label is unreachable in the sense that there is no value of the selector expression that would cause it to be chosen. By analogy to unreachable code, this is treated as a programmer error and results in a compile-time error.

static void error(Object o) {
    switch (o) {
        case CharSequence cs ->
            System.out.println("A sequence of length " + cs.length());
        case String s ->    // Error - pattern is dominated by previous pattern
            System.out.println("A string: " + s);
        default -> {
            break;
        }
    }
}

A switch expression requires that all possible values of the selector expression be handled in the switch block; in other words, it is exhaustive.

static int coverage(Object o) {
    return switch (o) {         // Error - still not exhaustive
        case String s  -> s.length();
        case Integer i -> i;
    };
}

Scope of pattern variable declarations

  1. The scope of a pattern variable declaration which occurs in a switch label includes any when clause of that label.
  2. The scope of a pattern variable declaration which occurs in a case label of a switch rule includes the expression, block, or throw statement that appears to the right of the arrow.
  3. The scope of a pattern variable declaration which occurs in a case label of a switch labeled statement group includes the block statements of the statement group. Falling through a case label that declares a pattern variable is forbidden.
static void test(Object o) {
    if ((o instanceof String s) && s.length() > 3) {
        System.out.println(s);
    } else {
        System.out.println("Not a string");
    }
}

In the above code, the pattern variable 's' extends to all the condition evaluations facilitating the functions/operations of that type.

static void test(Object o) {
    switch (o) {
        case Character c:
            if (c.charValue() == 7) {
                System.out.print("Ding ");
            }
            if (c.charValue() == 9) {
                System.out.print("Tab ");
            }
            System.out.println("Character");
        default:
            System.out.println();
    }
}

The above code follows the scope rule (2).

425: Virtual Threads (Preview)

Short-comings in the current thread creation and processing are,

  • In the current threading model, the number of available threads is limited because the JDK implements threads as wrappers around operating system (OS) threads. Consuming all the threads leads to CPU/Memory resource exhaustion.
  • Asynchronous processing by thread pooling requires redesigning of code with callbacks and as requests are executed on diverse threads debugging, capturing stack traces would be difficult so as to comprehend the flow.

A virtual thread is an instance of java.lang.Thread that requires an OS thread to do CPU work
but doesn't hold the OS thread while waiting for other resources.
A Java runtime can give the illusion of plentiful threads by mapping a large number of virtual threads to a small number of OS threads. Virtual thread consumes an OS thread only while it performs calculations on the CPU

When code running in a virtual thread calls a blocking I/O operation in the java.* API, the runtime performs a non-blocking OS call and automatically suspends the virtual thread until it can be resumed later. 

Virtual threads are cheap and plentiful, and thus should never be pooled: A new virtual thread should be created for every application task. Most virtual threads will thus be short-lived and have shallow call stacks, performing as little as a single HTTP client call or a single JDBC query. Platform threads, by contrast, are heavyweight and expensive, and thus often must be pooled. They tend to be long-lived, have deep call stacks, and be shared among many tasks.

The program first obtains an ExecutorService that will create a new virtual thread for each submitted task. It then submits 10,000 tasks and waits for all of them to be complete:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
}  // executor.close() is called implicitly, and waits

Virtual threads are a preview API, disabled by default

  • Compile the program with javac --release 19 --enable-preview Main.java and run it with java --enable-preview Main; or,
  • When using the source code launcher, run the program with java --source 19 --enable-preview Main.java; or,
  • When using jshell, start it with jshell --enable-preview.

428: Structured Concurrency (Incubator)

Structured concurrency treats multiple tasks running in different threads as a single unit of work, streamlining error handling and cancellation, improving reliability, and enhancing observability.

The ExecutorService immediately returns a Future for each subtask, and executes each subtask in its own thread. The handle() method awaits the subtasks' results via blocking calls to their futures' get() methods, so the task is said to join its subtasks.

Response handle() throws ExecutionException, InterruptedException {
    Future<String>  user  = esvc.submit(() -> findUser()); //subtask
    Future<Integer> order = esvc.submit(() -> fetchOrder()); //subtask
    String theUser  = user.get();   // Join findUser
    int    theOrder = order.get();  // Join fetchOrder
    return new Response(theUser, theOrder);
}

  1. If findUser() throws an exception then handle() will throw an exception when calling user.get() but fetchOrder() will continue to run in its own thread. This is a thread leak which, at best, wastes resources; at worst, the fetchOrder() thread will interfere with other tasks.
  2. If the thread executing handle() is interrupted, the interruption will not propagate to the subtasks. Both the findUser() and fetchOrder() threads will leak, continuing to run even after handle() has failed.
  3. If findUser() takes a long time to execute, but fetchOrder() fails in the meantime, then handle() will wait unnecessarily for findUser() by blocking on user.get() rather than cancelling it. Only after findUser() completes and user.get() returns will order.get() throw an exception, causing handle() to fail.

Subtasks are executed in their own threads by forking them individually and then joining them as a unit and, possibly, cancelling them as a unit. The subtasks' successful results or exceptions are aggregated and handled by the parent task. StructuredTaskScope confines the lifetimes of the subtasks, or forks, to a clear lexical scope in which all of a task's interactions with its subtasks — forking, joining, cancelling, handling errors, and composing results — takes place.

Response handle() throws ExecutionException, InterruptedException {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Future<String>  user  = scope.fork(() -> findUser());
        Future<Integer> order = scope.fork(() -> fetchOrder());
scope.join();           // Join both forks
        scope.throwIfFailed();  // ... and propagate errors
// Here, both forks have succeeded, so compose their results
        return new Response(user.resultNow(), order.resultNow());
    }
}

Virtual threads deliver an abundance of threads. Structured concurrency ensures that they are correctly and robustly coordinated, and enables observability tools to display threads as they are understood by the developer.

424: Foreign Function and Memory API (Preview)

To interoperate with data outside the java runtime, facilitating in invoking foreign functions and safely accessing foreign memory, calling native libraries and operators on native data without JNI

426: Vector API (Fourth Incubator)

The Vector API aims to improve the situation by providing a way to write complex vector algorithms in Java, using the existing HotSpot auto-vectorizer but with a user model which makes vectorization far more predictable and robust. Hand-coded vector loops can express high-performance algorithms, such as vectorized hashCode or specialized array comparisons, which an auto-vectorizer may never optimize. Numerous domains can benefit from this explicit vector API including machine learning, linear algebra, cryptography, finance, and code within the JDK itself.

422: Linux/RISC-V Port

Platform hardware that uses an operating system based on the Reduced Instruction Set Computer (RISC) -V architecture to simplify the individual instructions given to the computer to accomplish tasks, will now have the ability for JDK features exposed via a Port. The feature aims at integrating port into JDK main-line repository.

The port will support,

  • The template interpreter,
  • The C1 (client) JIT compiler,
  • The C2 (server) JIT compiler, and
  • All current mainline GCs, including ZGC and Shenandoah.

References: 

https://openjdk.org/projects/jdk/19/
https://jdk.java.net/19/release-notes


Disclaimer: 
This is a personal blog. Any views or opinions represented in this blog are personal and belong solely to the blog owner and do not represent those of people, institutions or organizations that the owner may or may not be associated with in professional or personal capacity, unless explicitly stated. Any views or opinions are not intended to malign any religion, ethnic group, club, organization, company, or individual. All content provided on this blog is for informational purposes only. The owner of this blog makes no representations as to the accuracy or completeness of any information on this site or found by following any link on this site. The owner will not be liable for any errors or omissions in this information nor for the availability of this information. The owner will not be liable for any losses, injuries, or damages from the display or use of this information.
Downloadable Files and ImagesAny downloadable file, including but not limited to pdfs, docs, jpegs, pngs, is provided at the user’s own risk. The owner will not be liable for any losses, injuries, or damages resulting from a corrupted or damaged file.
  • Comments are welcome. However, the blog owner reserves the right to edit or delete any comments submitted to this blog without notice due to :
  • Comments deemed to be spam or questionable spam.
  • Comments including profanity.
  • Comments containing language or concepts that could be deemed offensive.
  • Comments containing hate speech, credible threats, or direct attacks on an individual or group.
The blog owner is not responsible for the content in the comments. This blog disclaimer is subject to change at any time.

Comments

Popular posts from this blog

Tech Conversant Weekly Jul 03 - Jul 15

Topic: General                                                                                                                                              Level: All Welcome to the world of cutting-edge technology! Every bi-week, we bring you the latest and most incredible advancements in the tech industry that are sure to leave you feeling inspired and empowered. Stay ahead of the game and be the first to know about the newest innovations shaping our world. Discover new ways to improve your daily life, become more efficient, and enjoy new experiences. This time, we've...

Tech Conversant Weekly Jun 19 - Jul 01

Topic: General                                                                                                                                              Level: All Welcome to the world of cutting-edge technology! Every bi-week, we bring you the latest and most incredible advancements in the tech industry that are sure to leave you feeling inspired and empowered. Stay ahead of the game and be the first to know about the newest innovations shaping our world. Discover new ways to improve your daily life, become more efficient, and enjoy new experiences. This time, we've...

Microservices - Design Patterns

Topic: Software Design                                                                                                        Level: Intermediate Microservices - What? Microservice is a software design methodology, delegated to perform an isolated decoupled single functionality (following the Single-Responsibility Principle from object-oriented SOLID design principles).  Moreover, microservices by design, are decoupled making it easy to develop, test, maintain, deploy, configure, monitor and scale modules independently. Microservices - Why? Having one microservice would not be helpful without it being able to interact with other microservices, to aid in bringing an end-to-end b...