The Java community is buzzing once again, this time due to the release of Java 21. We believe that Java 21 boasts several groundbreaking and uniquely valuable features and I want to highlight these features in this post.
Both seasoned Java developers and newcomers will find this post informative and educational.
Java 21's release is driven by a dual mission: not only to deliver a multitude of performance, security, and stability enhancements but also to realign the language with the evolving requirements of modern applications. Java adapts to the changing landscape and the expectations surrounding it to maintain its agility and relevance.
Some noteworthy features, highly relevant to contemporary programming and application needs, include:
Thread-local variables have two main purposes. First, they are connected to a specific thread and can only be accessed by that thread or any threads created by it. Second, they remove the need for synchronization. These variables are typically used to share information or context between threads that come from the same parent. Without thread-local variables, you would have to pass extra object parameters to functions to carry information about the state, session, or context.
However, using thread-local variables brings some problems as applications become more complex and handle multiple requests at the same time:
Additionally, the introduction of virtual threads has made these issues more pronounced, especially because virtual threads allow you to create many threads that share the same operating system thread, making their usage more complex.
Here is an example to clarify:
Imagine we have a graphics library with a general <span class="pink" >drawShape()</span> function that paints closed figures by handling each of their sides. There are specific versions of this method optimized for drawing certain closed figures like a rhombus (painting one side and then repeating for the opposite side) or a square (painting just one side and repeating this process three times).
In this code snippet, there's a concept called <span class="pink" >ScopedValue</span>, and we're using it to manage the figure type during drawing:
In this code, the <span class="pink" >shape</span> variable holds the figure type, and it changes within different scopes as we draw different figures. Scoped values can be inherited by the method they call, and they allow for communicating different values to their own callees. This feature of inheritance and rebinding makes <span class="pink" >ScopedValue</span> a valuable programming construct.
When to use <span class="teal" >ThreadLocal</span>:Now, you might be wondering when to use <span class="pink" >ThreadLocal</span>. It's more suitable for inherently mutable classes like <span class="pink" >Java.util.DateM</span>, <span class="pink" >StringBuilder</span>, <span class="pink" >StringBuffer</span>, etc. These classes can't be shared between threads without synchronization. So, using ThreadLocal to provide each thread with its own mutable object, which persists throughout the thread's lifetime, is a practical approach. It removes the need for writing synchronization code and is safer and more efficient.
The above code example shows how <span class="pink" >ScopedValue</span> can manage the type of figure being drawn, and <span class="pink" >ThreadLocal</span> is preferable in situations involving inherently mutable classes for thread safety.
Java21 introduces Key Encapsulation Mechanisms (KEMs), as the preferred approach over Authenticated Key Exchanges (AKE) within Public Key Encryption (PKE) schemes.
In the older key exchange algorithm, the sender encrypts the message using their private key, and the receiver decrypts it with the sender's publicly available key. However, this approach is no longer considered secure, as quantum computers can compromise most key exchange algorithms, rendering AKEs vulnerable.
In the KEM scheme, in addition to the public-private key pair, the sender and receiver mutually agree upon an additional "secret value." The sender then transmits this "secret value" to the receiver in an encrypted form. Only the receiver possesses the capability to decrypt this "secret value" and put it to use.
Here's a breakdown of the KEM process: The sender initially leverages the public key and an encryption function to invoke the key encapsulation, which subsequently yields a secret key (referred to as K) and a key encapsulation message (termed ciphertext in ISO 18033-2). The sender then dispatches this key encapsulation message to the receiver. On the receiver's side, they acquire this encapsulated data and apply a key decapsulation function employing their private key along with the received key encapsulation message. This process unveils the secret key K, which the receiver employs to decrypt further messages.
In essence, an additional "secret value" is exchanged between the two parties alongside the PKEs, bolstering the security of the process. Previously, support for KEMs was primarily provided by third-party providers such as BouncyCastle. However, Java now offers a standardized mechanism for KEMs as well.
In the following example, we employ the Diffie-Hellman-based KEM, which uses the receiver's Diffie-Hellman or elliptic curve Diffie-Hellman public key to create encapsulation, thus enforcing the Key Encapsulation Mechanisms in the ensuing data exchange.
Here is a code snippet to explain the above steps
1. The receiver generates a keypair and hands over his public key to the sender
2. The sender generates an encapsulation, using the receiver's public key
Note that on the sender side, we use DHKEM, an advanced version of the Diffie-Hellman algorithm. Earlier, we used X25519( or id-kema-dhkem-x25519) to generate a keypair, per IETF spec.
3. The receiver receives the encapsulation, and uses their private key to decapsulate the received packet, in order to know the "secret value".
4. All further communication between the receiver and the sender will use the "secret value"
You can also first use KEM's to only transmit the "secret value", and later data exchanges can work on the older PKE schemes, assuming that every decryption at the receiver's end will also use the "secret value" for additional privacy.
Here is an example where the sender and receiver can implement a secure file exchange:
The file named <span class="pink" >receivedFileDecrypted</span> has the information that was shared by the sender.
Even if someone is eavesdropping or knows the private keys of either sender or receiver, they have no way to break this encryption if the "Secret key" has already been shared using the KEM mechanisms.
While some programming constructs are valuable in organizing large projects and team collaborations, they may seem overly complex for beginners or small programs. For instance, consider the iconic "Hello, World!" program:
For beginners, concepts like access control (public/private), the static modifier, void return type, and enclosing everything within a class named "HelloWorld" can be confusing. Often, explanations like "we'll discuss it later, for now, just accept it" further mystify newcomers.
Java21 introduces a new feature that allows you to write small programs without the need to define a class, specify parameters and data types, or access control details. For example, the classic "Hello, World!" program can be simplified as follows:
In this simplified version, class declarations are implicit, and method parameters and access control modifiers don't need explicit declarations. There's no requirement for encapsulation, namespaces, modularization, or scoping. What truly matters are variables, control flow, and subroutines. Internally, Java21 still encapsulates the code within an implicit class/package, but beginners don't need to worry about it. This concept is known as "unnamed classes" and "instance-based main() methods."
This simplification makes programming enjoyable for novice programmers, and as they gain experience, they can gradually transition to more complex structures.
Experienced programmers can also benefit from this feature when writing small utilities, performing bridging, or testing. Keep in mind that other named classes can't access this unnamed class, and the unnamed class is final and can't extend/implement other classes/interfaces. However, it provides a clear example of how to use the fundamental building blocks to execute a simple Java program.
To enable this feature, compile the program using:
And run it with:
You can save the code in a source file named "MyFirst.java," and then launch this unnamed class using the source-code launcher:
Pretty cool, right? 😎
BTW, if you are using Java 17 or below, do check out Direct Invoke from Unlogged, you can call any Java method directly from your IDE.
Java was initially designed to be platform-independent, object-oriented, and efficient for multi-threaded server applications. As technology advanced, modern applications evolved to handle high-throughput workloads, demanding low latency and scalability. This evolution called for a more advanced garbage collector to efficiently manage system resources, conserving CPU cycles and memory.
Generational ZGC, part of the Z Garbage Collector, was first made available for production use in JDK 15 and has now been promoted from "Targeted" to "Completed" status in JDK 21. Generational ZGC operates on the principle that most objects have short lifespans, known as the weak generational hypothesis. Young objects have brief lifespans, while older objects endure longer. This theory informs the practical implementation, optimizing the collection of younger objects for more memory and older objects with fewer resources.
Generational ZGC divides the heap into two logical generations: the young generation for recently allocated objects and the old generation for long-lived objects. Both generations are collected independently, focusing on quickly collecting younger objects. Generational ZGC follows several design principles to ensure low latency and high scalability:
To use Generational ZGC, add the -XX:+ZGenerational option to the -XX:+UseZGC command-line option. Generational ZGC will become the default in future releases, with non-generational ZGC eventually being removed.
Additionally, this release introduces other features such as Structured Concurrency (in preview), preparing to disallow the dynamic loading of agents (proposal), and Vector API (still in incubation). These features enhance Java's capabilities and performance.
Please note that the text provides an overview of the key points without delving into all the technical details.
We have not gone into much detail about some other features like:-
I am preparing some content around these points, stay tuned for my next blog post.