Java 21 Deep Dive

Better Language, Better Scalability, Better APIs, Better Tools

Let’s get started!

Lots to talk about!

Part I:

  • pattern matching

  • virtual threads

  • string templates

Lots to talk about!

Part II:

  • on-ramping to Java

  • sequenced collections

  • misc API improvements

  • GC, CDS, UTF-8

Lots to talk about!

Part III:

  • Source and Classfile Tools

  • Development and Deployment Tools

  • Security Tools

  • Monitoring Tools

Why upgrade?

Costs of running on old versions:

  • support contract for Java

  • waning support in libraries / frameworks

Why upgrade?

Costs of not running on new versions:

  • lower productivity

  • less observability and performance
    (more on that later)

  • less access to talent

  • bigger upgrade costs

Why upgrade?

Resistence is futile.

How to upgrade?

Preparations:

  • stick to supported APIs

  • stick to standardized behavior

  • stick to well-maintained projects

  • keep dependencies and tools up to date

  • stay ahead of removals (jdeprscan)

  • build on many JDK versions

How to upgrade?

Prepare by building on multiple JDK versions:

  • your baseline version

  • every supported version since then

  • latest version

  • EA build of next version

How to upgrade?

It’s not necessary to build …

  • … each commit on all versions

  • … the whole project on all versions

Build as much as feasible.

What about LTS?

Within OpenJDK, there is no LTS.

⇝ has no impact on features, reliability, etc.

It’s a vendor-centric concept
to offer continuous fixes
(usually for money).

You’re paying not to get new features.

What about LTS?

inside java newscast 52

Java 21 💣💥

Part I

Pattern Matching
A New Dynamic Dispatch
Data-Oriented Programming
Virtual Threads
String Templates

A simple app

Features:

  • scrapes GitHub projects

  • creates Page instances:

    • GitHubIssuePage

    • GitHubPrPage

    • ExternalPage

    • ErrorPage

  • further processes pages

A simple app

Features:

  • display as interactive graph

  • compute graph properties

  • categorize pages by topic

  • analyze mood of interactions

  • process payment for analyses

  • etc.

A simple architecture?

How to implement features?

  • methods on Page 😧

  • visitor pattern 😫

  • pattern matching 🥳

Pattern Matching

Ingredients:

Pattern Matching

Approach:

  • make Page sealed

  • implement features as methods outside of Page

  • accept Page parameters and switch over it

  • avoid default branch for maintainability

Sealed Page

Sealed types limit inheritance,
by only allowing specific subtypes.

public sealed interface Page
	permits GitHubIssuePage, GitHubPrPage,
			ExternalPage, ErrorPage {
	// ...
}

Switch over Page

public void categorize(Page page) {
	switch (page) {
		case GitHubIssuePage is -> categorizeIssue(is);
		case GitHubPrPage pr -> categorizePr(pr);
		case ExternalPage ext -> categorizeExternal(ext);
		case ErrorPage err -> categorizeError(err);
	}
}

Maintainability

Unlike an if-else-if-chain,
a pattern switch needs to be exhaustive.

Fulfilled by:

  • switching over a sealed types

  • a case per subtype

  • avoiding the default branch

⇝ Adding a new subtype causes compile error!

Avoiding Default

Sometimes you have "defaulty" behavior:

public void categorize(Page page) {
	switch (page) {
		case GitHubIssuePage is -> categorizeIssue(is);
		case GitHubPrPage pr -> categorizePr(pr);
		default -> { }
	}
}

But we need to avoid default!

Avoiding Default

Write explicit branches:

public void categorize(Page page) {
	switch (page) {
		case GitHubIssuePage is -> categorizeIssue(is);
		case GitHubPrPage pr -> categorizePr(pr);
		// duplication 😢
		case ErrorPage err -> { };
		case ExternalPage ext -> { };
	};
}

Avoiding Default

Use _ to combine "default branches":

public void categorize(Page page) {
	switch (page) {
		case GitHubIssuePage is -> categorizeIssue(is);
		case GitHubPrPage pr -> categorizePr(pr);
		case ErrorPage _, ExternalPage _ -> { };
	};
}

⇝ Default behavior without default branch.

Dynamic dispatch

Dynamic dispatch selects the invoked method by type.

As language feature:

  • via inheritance

  • makes method part of API

What if methods shouldn’t be part of the API?

Dynamic dispatch

Without methods becoming part of the API.

Via visitor pattern:

  • makes "visitation" part of API

  • cumbersome and indirect

Via pattern matching (new):

  • makes "sealed" part of type

  • straight-forward

More

Java 21 💣💥

Part I

Pattern Matching
A New Dynamic Dispatch
Data-Oriented Programming
Virtual Threads
String Templates

Programming paradigms

Paradigms often come with an
"Everything is a …​" sentence.

The goal of any programming paradigm is to manage complexity.

  • complexity comes in many forms

  • not all paradigms handle all forms equally well

⇝ "It depends"

Object-oriented programming

Everything is an object

  • combines state and behavior

  • hinges on encapsulation

  • polymorphism through inheritance

Works best when defining/upholding boundaries.

Mixed programming

Great use cases for OOP:

  • boundaries between libraries and clients

  • in large programs to enable modular reasoning

Consider a data-oriented approach for:

  • smaller (sub)systems

  • focused on data

Data-oriented programming

Guiding principles:

  • model the data, the whole data,
    and nothing but the data

  • data is immutable

  • validate at the boundary

  • make illegal states unrepresentable

From Brian Goetz' seminal article:
Data Oriented Programming in Java

Crawling GitHub

The app we just looked at:

  • is small

  • focusses on data (Page)

  • separates operations from types

⇝ Perfect for data-oriented programming!

Applying DOP

Model the data, the whole data,
and nothing but the data.

There are four kinds of pages:

  • error page

  • external page

  • GitHub issue page

  • GitHub PR page

⇝ Use four records to model them!

Modeling the data

public record ErrorPage(
	URI url, Exception ex) { }

public record ExternalPage(
	URI url, String content) { }

public record GitHubIssuePage(
	URI url, String content,
	int issueNumber, Set<Page> links) { }

public record GitHubPrPage(
	URI url, String content,
	int prNumber, Set<Page> links) { }

Applying DOP

Model the data, the whole data,
and nothing but the data.

There are additional relations between them:

  • a page (load) is either successful or not

  • a successful page is either external or GitHub

  • a GitHub page is either for a PR or an issue

⇝ Use sealed types to model the alternatives!

Modeling alternatives

public sealed interface Page
		permits ErrorPage, SuccessfulPage {
	URI url();
}

public sealed interface SuccessfulPage
		extends Page permits ExternalPage, GitHubPage {
	String content();
}

public sealed interface GitHubPage
		extends SuccessfulPage
		permits GitHubIssuePage, GitHubPrPage {
	Set<Page> links();
	default Stream<Page> subtree() { ... }
}

Algebraic data types

  • records are product types

  • sealed types are sum types

This simple combination of mechanisms — aggregation and choice — is deceptively powerful

Applying DOP

Make illegal states unrepresentable.

Many are already, e.g.:

  • with error and with content

  • with issueNumber and prNumber

  • with isseNumber or prNumber but no links

Validation

Validate at the boundary.

⇝ Reject other illegal states in constructors.

record ExternalPage(URI url, String content) {
	// compact constructor
	ExternalPage {
		Objects.requireNonNull(url);
		Objects.requireNonNull(content);
		if (content.isBlank())
			throw new IllegalArgumentException();
	}
}

Applying DOP

Data is immutable.

Records are shallowly immutable,
but field types may not be.

⇝ Fix that during construction.

// compact constructor
GitHubPrPage {
	// [...]
	links = Set.copyOf(links);
}

Where are we?

  • page "type" is explicit in Java’s type

  • only legal combination of data are possible

  • API is self-documenting

  • code is trivial to test

But where did the operations go?

Operations on data

Model the data, the whole data,
and nothing but the data.

⇝ Methods should be limited to derived quantities.

public record GitHubIssuePage(
		URI url, String content,
		int issueNumber, Set<Page> links) {

	public String toPrettyString() {
		return "🐈 ISSUE #" + issueNumber;
	}

}

Operations on data

Other operations must be defined elsewhere:

  • methods in other subsystems

  • use pattern matching over sealed types
    for polymorphic operations

  • avoid default branch

  • use record patterns to access data

⇝ This is just pattern matching.

Operations on data

If toPrettyString is defined outside of Page:

private static String toPrettyString(Page page) {
	return switch (page) {
		case ErrorPage(var url, _)
			-> "💥 ERROR: " + url.getHost();
		case ExternalPage(var url, _)
			-> "💤 EXTERNAL: " + url.getHost();
		case GitHubIssuePage(_, _, int issueNumber, _)
			-> "🐈 ISSUE #" + issueNumber;
		case GitHubPrPage(_, _, int prNumber, _)
			-> "🐙 PR #" + prNumber;
	};
}

Functional programming?!

  • immutable data structures

  • methods (functions?) that operate on them

Isn’t this just functional programming?!

Kind of.

DOP vs FP

Functional programming:

Everything is a function

⇝ Focus on creating and composing functions.


Data-oriented programming:

Model data as data.

⇝ Focus on correctly modeling the data.

DOP vs OOP

OOP is not dead (again):

  • valuable for complex entities or rich libraries

  • use whenever encapsulation is needed

  • still a good default on high level

DOP —  consider when:

  • mainly handling outside data

  • working with simple or ad-hoc data

  • data and behavior should be separated

Data-oriented programming

Use Java’s strong typing to model data as data:

  • use classes to represent data, particularly:

    • data as data with records

    • alternatives with sealed classes

  • use methods (separately) to model behavior, particularly:

    • exhaustive switch without default

    • pattern matching to destructure polymorphic data

Guiding principles

  • model the data, the whole data,
    and nothing but the data

  • data is immutable

  • validate at the boundary

  • make illegal states unrepresentable

More

More on data-oriented programming:

Java 21 💣💥

Part I

Pattern Matching
Virtual Threads
Unlimited Threads
Under The Hood
String Templates

A simple web request

Imagine a hypothetical HTTP request:

  1. interpret request

  2. query database (blocks)

  3. process data for response

Resource utilization:

  • good for 1. and 3.

  • really bad for 2.

How to implement that request?

Synchronous

Align application’s unit of concurrency (request)
with Java’s unit of concurrency (thread):

  • use thread per request

  • simple to write, debug, profile

  • blocks threads on certain calls

  • limited number of platform threads
    ⇝ bad resource utilization
    ⇝ low throughput

Asynchronous

Only use threads for actual computations:

  • use non-blocking APIs (futures / reactive streams)

  • harder to write, challenging to debug/profile

  • incompatible with synchronous code

  • shares platform threads
    ⇝ great resource utilization
    ⇝ high throughput

Conflict!

There’s a conflict between:

  • simplicity

  • throughput

Nota bene

There are other conflicts:

  • design vs performance (⇝ Valhalla)

  • explicitness vs succinctness (⇝ Amber)

  • flexibility vs safety (⇝ Panama)

  • dynamism vs performance (⇝ Leyden)

Enter virtual threads!

A virtual thread:

  • is a regular Thread

  • low memory footprint ([k]bytes)

  • small switching cost

  • scheduled by the Java runtime

  • requires no OS thread when waiting

Virtual things

Virtual memory:

  • maps large virtual address space
    to limited physical memory

  • gives illusion of plentiful memory

Virtual threads:

  • map large number of virtual threads
    to a small number of OS threads

  • give the illusion of plentiful threads

Virtual things

Programs rarely care about virtual vs physical memory.

Programs need rarely care about virtual vs platform thread.

Instead:

  • write straightforward (blocking) code

  • runtime shares available OS threads

  • reduces the cost of blocking to near zero

Example

try (var executor = Executors
		.newVirtualThreadPerTaskExecutor()) {
	for (int i = 0; i < 1_000_000; i++) {
		var number = i;
		executor.submit(() -> {
			Thread.sleep(Duration.ofSeconds(1));
			return number;
		});
	}
} // executor.close() is called implicitly, and waits

Effects

Virtual threads:

  • remove "number of threads" as bottleneck

  • match app’s unit of concurrency to Java’s

simplicity && throughput

Performance

Virtual threads aren’t "faster threads":

  • same number of CPU cycles

  • each task takes the same time (same latency)

So why bother?

Parallelism vs concurrency

ParallelismConcurrency

Task origin

solution

problem

Control

developer

environment

Resource use

coordinated

competitive

Metric

latency

throughput

Abstraction

CPU cores

tasks

# of threads

# of cores

# of tasks

Performance

When workload is not CPU-bound:

  • start waiting as early as possible

  • for as many tasks as possible

⇝ Virtual threads increase throughput:

  • when workload is not CPU-bound

  • when number of concurrent tasks is high

Server how-to

For servers:

  • request handling threads are started by web framework

  • frameworks will offer (easy) configuration options

We’re getting there.

Spring Boot

Set property on 3.2 (Nov 2023):

spring.threads.virtual.enabled=true

Quarkus

Annotate request handling method on 3.?:

@GET
@Path("api")
@RunOnVirtualThread
public String handle() {
	// ...
}

Helidon

Just works on 4.0 (currently RC1).

More

Virtual threads

Go forth and multiply (your threads)

Java 21 💣💥

Part I

Pattern Matching
Virtual Threads
Unlimited Threads
Under The Hood
String Templates

Preparing your code

Virtual threads:

  • always work correctly

  • may not scale perfectly

Code changes can improve scalability
(and maintainability, debuggability, observability).

Avoid thread pools

Only pool expensive resources
but virtual threads are cheap.

⇝ Replace thread pools (for concurrency),
with virtual threads plus, e.g., semaphores.

With thread pools

// limits concurrent queries but pools 👎🏾
private static final ExecutorService DB_POOL =
	Executors.newFixedThreadPool(16);

public <T> Future<T> queryDatabase(Callable<T> query) {
	return DB_POOL.submit(query);
}

With semaphore

// limits concurrent queries without pool 👍🏾
private static final Semaphore DB_SEMAPHORE =
	new Semaphore(16);

public <T> T queryDatabase(Callable<T> query)
		throws Exception {
	DB_SEMAPHORE.acquire();
	try {
		return query.call();
	} finally {
		DB_SEMAPHORE.release();
	}
}

Caveats

To understand virtual thread caveats
we need to understand how they work.

(Also, it’s very interesting.)

Under the hood

The Java runtime manages virtual threads:

  • runs them on a pool of carrier threads

  • on blocking call:

    • internally calls non-blocking operation

    • unmounts from carrier thread!

  • when call returns:

    • mounts to (other) carrier thread

    • continues

The simple web request

Remember the hypothetical request:

  1. interpret request

  2. query database (blocks)

  3. process data for response

In a virtual thread:

  • runtime submits task to carrier thread pool

  • when 2. blocks, virtual thread unmounts

  • runtime hands carrier thread back to pool

  • when 2. unblocks, runtime resubmits task

  • virtual thread mounts and continues with 3.

Compatibility

Virtual threads work correctly with everything:

  • all blocking operations

  • synchronized

  • Thread, currentThread, etc.

  • thread interruption

  • thread-locals

  • native code

But not all scale perfectly.

Caveat #1: capture

Despite lots of internal rework (e.g. JEPs 353, 373)
not all blocking operations unmount.

Some capture platform thread:

  • Object::wait

  • file I/O (⇝ io_uring)

⇝ Compensated by temporarily growing carrier pool.

⚠️ Problematic when capturing operations dominate.

Caveat #2: pinning

Some operations pin (operations don’t unmount):

  • native method call (JNI)

  • foreign function call (FFM)

  • synchronized block (for now)

⇝ No compensation

⚠️ Problematic when:

  • pinning is frequent

  • contains blocking operations

Avoid long-running pins

If possible:

  • avoid pinning operations

  • remove blocking operations
    from pinning code sections.

With synchronization

// guarantees sequential access, but pins (for now) 👎🏾
public synchronized String accessResource() {
	return access();
}

With lock

// guarantees sequential access without pinning 👍🏾
private static final ReentrantLock LOCK =
	new ReentrantLock();

public String accessResource() {
	// lock guarantees sequential access
	LOCK.lock();
	try {
		return access();
	} finally {
		LOCK.unlock();
	}
}

Caveat #3: thread-locals

Thread-locals can hinder scalability:

  • can be inherited

  • to keep them thread-local,
    values are copied

  • can occupy lots of memory

(There are also API shortcomings.)

⇝ Refactor to scoped values (JEP 446).

With thread-local

// copies value for each inheriting thread 👎🏾
static final ThreadLocal<Principal> PRINCIPAL =
	new ThreadLocal<>();

public void serve(Request request, Response response) {
	var level = request.isAdmin() ? ADMIN : GUEST;
	var principal = new Principal(level);
	PRINCIPAL.set(principal);
	Application.handle(request, response);
}

With scoped value

// immutable, so no copies needed 👍🏾
static final ScopedValue<Principal> PRINCIPAL =
	new ScopedValue<>();

public void serve(Request request, Response response) {
	var level = request.isAdmin() ? ADMIN : GUEST;
	var principal = new Principal(level);
	ScopedValue
		.where(PRINCIPAL, principal)
		.run(() -> Application
			.handle(request, response));
}

Preparing your code

Most importantly:

  1. replace thread pools with semaphores

Also helpful:

  1. remove long-running I/O from pinned sections

  2. replace thread-locals with scoped values

  3. replace synchronized with locks

Java 21 💣💥

Part I

Pattern Matching
Virtual Threads
String Templates

String composition

Composing strings in Java is cumbersome:

String property = "last_name";
String value = "Doe";

// concatenation
String query =
	"SELECT * FROM Person p WHERE p."
		+ property + " = '" + value + "'";
// formatting
String query =
	"SELECT * FROM Person p WHERE p.%s = '%s'"
		.formatted(property, value);

Comes with free SQL injection! 😳

String interpolation

Why not?

// (fictional syntax!)
String query =
	"SELECT * FROM Person p "
		+ "WHERE p.\{property} = '\{value}'";

Also comes with free SQL injection! 😳

String interpolation

SQL injections aren’t the only concern.

These also need validation and sanitization:

  • HTML/XML

  • JSON

  • YAML

  • …​

All follow format-specific rules.

String templates

[Preview in Java 21 and 22 — JEP 430, JEP 459]

String query = STR."""
	SELECT * FROM Person p
	WHERE p.\{property} = '\{value}'
	""";

Template expression ingredients:

  • template with embedded expressions
    ~> StringTemplate

  • template processor (e.g. STR):
    transforms StringTemplate into String*

Template procesor STR

String form = STR."""
	Desc     Unit   Qty   Amount
	\{desc} $\{price} \{qty} $\{price * qty}

	Subtotal  $\{price * qty}
	Tax       $\{price * qty * tax}
	Total     $\{price * qty * (1.0 + tax)}
	""";
Desc     Unit   Qty   Amount
hammer   $7.88  3     $23.64

Subtotal  $23.64
Tax       $3.546
Total     $27.186

Template processor FMT

String form = FMT."""
	Desc        Unit      Qty   Amount
	%-10s\{desc} $%5.2f\{price} %5d\{qty} $%5.2f\{price * qty}

	Subtotal  $%5.2f\{price * qty}
	Tax       $%5.2f\{price * qty * tax}
	Total     $%5.2f\{price * qty * (1.0 + tax)}
	""";
Desc        Unit      Qty   Amount
hammer      $ 7.88      3   $23.64

Subtotal  $23.64
Tax       $ 3.55
Total     $27.19

Why strings?

Often, strings are just exchange format, e.g.:

  • start with: String + values

  • validate / sanitize (i.e. parse)

  • dumb down to: String 🤔

  • parse to: JSONObject, Statement, …

Why the detour?

Custom templating

STR is a singleton instance of
a Processor implementation:

public interface Processor<RESULT, EX> {
	RESULT process(StringTemplate s) throws EX;
}

RESULT can be of any type!

Custom templating

// prevents SQL injections
Statement query = SQL."""
	SELECT * FROM Person p
	WHERE p.\{property} = '\{value}'
	""";

// validates & escapes JSON
JSONObject doc = JSON."""
	{
		"name": "\{name}",
		"year": "\{bday.getYear()}"
	}
	""";

Summary

String templates:

  • simplify string concatenation

  • enable domain-specific processing

  • incentivize the "right way"

Break

We’ll see you back here in 15 minutes with Part II:

  • on-ramping to Java

  • sequenced collections

  • misc API improvements

  • GC, CDS, UTF-8

Java 21 💣💥

Part II

On-Ramp
Sequenced Collections
Misc. API Improvements
Generational ZGC
Class-Data Sharing
UTF-8 by Default

Starting (with) Java

Java 21 makes life easier
for new (Java) developers.

Why do we care?

We all know Java, IDEs, build tools, etc.

  • do we all?

  • what about your kids?

  • what about students?

  • what about the frontend dev?

  • what about ML/AI folks?

Java needs to be approachable!
Java needs an on-ramp for new (Java) developers!

Running Java

To write and run a simple Java program, you need:

  • a JDK

  • an editor (IDE?)

  • javac (build tool? IDE?)

  • java (IDE?)

  • some Java code

Writing Java

Minimal Java code:

public class Main {
	public static void main(String[] args) {
		System.out.println("Hello, World!");
	}
}
  • visibility

  • classes & methods

  • static vs instance

  • returns & parameters

  • statements & arguments

Approachability

That’s a lot of tools and concepts!

Java is great for large-scale development:

  • detailed toolchain

  • refined programming model

This make it less approachable.

Let’s change that!

jshell

Java 9 added jshell:

  • all you need:

    • tools: JDK, jshell

    • concepts: statements & arguments

  • but:

    • not great for beginners (IMO)

    • no progression

More is needed.

Single-file execution

Java 11 added single-file execution (JEP 330):

java Prog.java
  • removed: javac

  • but: no progression

Much better for beginners,
but just a section of an on-ramp.

On-Ramp

Expand single-file execution in two directions:

  • simplify code: reduce required Java concepts

  • ease progression: run multiple files with java

Simpler code

Remove requirement of:

  • String[] args parameter

  • main being static

  • main being public

  • the class itself

// all the code in Prog.java
void main() {
	System.out.println("Hello, World!");
}

[Preview in Java 21 — JEP 445]

Running multiple files

Say you have a folder:

MyFirstJava
 ├─ Prog.java
 ├─ Helper.java
 └─ Lib
     └─ library.jar

Run with:

java -cp 'Lib/*' Prog.java

[Preview in Java 22 — JEP 458]

Progression

Natural progression:

  • start with main()

  • need arguments? ⇝ add String[] args

  • need to organize code? ⇝ add methods

  • need shared state? ⇝ add fields

  • need more functionality? ⇝ explore JDK APIs

  • even more? ⇝ explore simple libraries

  • need more structure? ⇝ split into multiple files

  • even more ⇝ use visibility & packages

Doesn’t even have to be that order!

Summary

Java’s strengths for large-scale development
make it less approachable:

  • detailed toolchain

  • refined programming model

There are new features that:

  • make it easier to start

  • allow gradual progression

  • entice the future dev generation

More

Java 21 💣💥

Part II

On-Ramp
Sequenced Collections
Misc. API Improvements
Generational ZGC
Class-Data Sharing
UTF-8 by Default

Order

Collections with order and indexed access:

  • List

Collections with order without indexed access:

  • SortedSet (sort order)

  • Deque (insertion order)

  • LinkedHashSet (insertion order)

  • and more

Sequence

New interfaces capture the concept of order:

  • SequencedCollection

  • SequencedSet

  • SequencedMap

Use as parameter or return type
and enjoy new methods.

Get first

Getting the first element:

  • list.get(0)

  • sortedSet.first()

  • deque.getFirst()

  • linkedHashSet.iterator().next()

Now for all:

sequencedCollection.getFirst()

Remove last

Removing the last element:

  • list.remove(list.size() - 1)

  • sortedSet.remove(sortedSet.last())

  • deque.removeLast()

  • linkedHashSet.🤷🏾‍♂️()

Now for all:

sequencedCollection.removeLast()

Reverse order

Reversing order:

  • list.listIterator()ListIterator

  • navigableSet.descendingSet()NavigableSet (view)

  • deque.descendingIterator()Iterator

  • linkedHashSet.🤷🏾‍♂️()

Now for all:

sequencedCollection.reversed()

Reversed collection

sequencedCollection.reversed() returns
a SequencedCollection view:

for (E element : list.reversed())
	// ...

sortedSet
	.reversed().stream()
	//...

deque.reversed().toArray();

Reversed view

sequencedCollection.reversed() returns
a SequencedCollection view:

var letters = new ArrayList<>(List.of("A", "B", "C"));
	// ⇝ letters = ["A", "B", "C"]
letters.reversed().removeFirst();
	// ⇝ letters = ["A", "B"]

New operations

void addFirst(E);
void addLast(E);
E getFirst();
E getLast();
E removeFirst();
E removeLast();

SequencedCollection<E> reversed();

(Analoguous for maps.)

New ops vs sort order

What happens when addFirst|Last is used
on a sorted data structure?

SortedSet<String> letters = new TreeSet<>(
	List.of("B", "A", "C"));
	// ⇝ letters = ["A", "B", "C"]
letters.addLast("D");

Options:

  • works always ⇝ breaks SortedSet contract

  • works if value fits ⇝ hard to predict

  • works never ⇝ UnsupportedOperationException

Using types

Use the most general type that:

  • has the API you need/support

  • plays the role you need/support

For collections, that’s often: Collection
(less often: List, Set).

⇝ Consider new types!

More

Java 21 💣💥

Part II

On-Ramp
Sequenced Collections
Misc. API Improvements
Generational ZGC
Class-Data Sharing
UTF-8 by Default

Collections

How do you create an ArrayList that
can store 50 elements without resizing?

new ArrayList<>(50);

How do you create a HashMap that
can store 50 pairs without resizing?

new HashMap<>(64, 0.8f);
new HashMap<>(128);

🤔

Collections

Right-sizing hashing data structures:

  • HashMap.newHashMap(int numMappings)

  • HashSet.newHashSet(int numElements)

  • LinkedHashMap.newLinkedHashMap(int numMappings)

  • LinkedHashSet.newLinkedHashSet(int numElements)

Math

Lots of new methods on Math:

  • for int/long division with different modes for:

    • rounding

    • overflow handling

  • for ceiling modulus (5 ⌈%⌉ 3 = -1)

  • for clamping

Strings and builders

"String".indexOf(
	String str, int beginIndex, int endIndex)

On StringBuilder and StringBuffer:

  • repeat(int codePoint, int count)

  • repeat(CharSequence cs, int count)

💣💥

On Character (all static):

  • isEmoji(int codePoint)

  • isEmojiPresentation(int codePoint)

  • isEmojiModifier(int codePoint)

  • isEmojiModifierBase(int codePoint)

  • isEmojiComponent(int codePoint)

Localizing DateTimes

Options for formatting dates/times with DateTimeFormatter:

  • with a fixed pattern: ofPattern

  • with a localized style: ofLocalizedDate
    (FULL, LONG, MEDIUM, SHORT)

What about a localized result with custom elements?

DateTimeFormatter.ofLocalizedPattern

Localized patterns

DateTimeFormatter.ofLocalizedPattern:

  • you include what you want to show up
    (e.g. year + month with "yMM")

  • result will depend on locale
    (e.g. "10/2023" in USA)

Pattern comparison:

var now = ZonedDateTime.now();
for (var locale : List.of(
		Locale.of("en", "US"),
		Locale.of("be", "BE"),
		Locale.of("vi", "VN"))) {
	Locale.setDefault(locale);

	var custom = DateTimeFormatter
		.ofPattern("y-MM-dd");
	var local = DateTimeFormatter
		.ofLocalizedDate(FormatStyle.SHORT);
	var customLocal = DateTimeFormatter
		.ofLocalizedPattern("yMM");

	// pretty print
}

Pattern comparison

localecustomlocalboth

en_US

2023-10-02

10/2/23

10/2023

be_BE

2023-10-02

2.10.23

10.2023

vi_VN

2023-10-02

02/10/2023

tháng 10, 2023

Localized patterns

Analogue methods were added to DateTimeFormatterBuilder:

DateTimeFormatterBuilder appendLocalized(
	String requestedTemplate);

static String getLocalizedDateTimePattern(
	String requestedTemplate,
	Chronology chrono, Locale locale)

More AutoCloseable

These types now implemnet AutoCloseable:

  • HttpClient

  • ExecutorService

  • ForkJoinPool

A better future

Additions to Future<T>:

  • T resultNow()

  • Throwable exceptionNow

  • State state()

New:

enum State {
	RUNNING, SUCCESS, FAILED, CANCELLED
}

More

There are many more additions like this.

Find a few more in
🎥 Java 21 API New Features (Sep 2023)

Java 21 💣💥

Part II

On-Ramp
Sequenced Collections
Misc. API Improvements
Generational ZGC
Class-Data Sharing
UTF-8 by Default

Generational ZGC

Compared to other GCs, ZGC:

  • optimizes for ultra-low pause times

  • can have higher memory footprint or higher CPU usage

In Java 21, ZGC becomes generational.

Generational Hypothesis

  • most objects die young

  • those who don’t, grow (very) old

GCs can make use of this by tracking
young and old generations.

ZGC didn’t do this, but can do it now with:
-XX:+UseZGC -XX:+ZGenerational

Some Benchmarks

A Cassandra 4 benchmark of ZGC vs GenZGC showed:

  • 4x throughput with a fixed heap or

  • 1/4x heap size with stable throughput

(Probably not representative but very promising.)

More

Java 21 💣💥

Part II

On-Ramp
Sequenced Collections
Misc. API Improvements
Generational ZGC
Class-Data Sharing
UTF-8 by Default

Class-Data Sharing

Use CDS to shave off 10-25% of your boot times.

Recent improvements:

And more to come from Project Leyden.

More

Java 21 💣💥

Part II

On-Ramp
Sequenced Collections
Misc. API Improvements
Generational ZGC
Class-Data Sharing
UTF-8 by Default

UTF-8 by Default

[Since Java 18 — JEP 400]

All Java APIs use UTF-8 as default charset.

This change concerns projects that:

  • run on Windows

  • use non UTF-8 encoded files

  • do not pass a charset to APIs

In that case, you need to update code!

More

Deprecations

Cleaning house

Already removed:

Cleaning house

Deprecations (for removal):

Agents

What it is:

  • a component that transforms byte code

  • uses java.lang.instrument or JVM TI

  • launches with JVM or attaches later ("dynamic")

Dynamic agents

What you need to know:

  • all mechanisms for agents remain intact

  • nothing changed yet

  • in the future, dynamic attach will be
    disabled by default

  • enable with -XX:+EnableDynamicAgentLoading

Dynamic agents

What you need to do:

  • run your app with -XX:-EnableDynamicAgentLoading

  • observe closely

  • investigate necessity of dynamic agents

Finalization

What it is:

  • finalize() methods

  • a JLS/GC machinery for them

Finalization

What you need to know:

  • you can disable with --finalization=disabled

  • in a future release, disabled will be the default

  • in a later release, finalization will be removed

Finalization

What you need to do:

  • search for finalize() in your code and
    replace with try-with-resources or Cleaner API

  • search for finalize() in your dependencies and
    help remove them

  • run your app with --finalization=disabled and
    closely monitor resource behavior (e.g. file handles)

Security Manager

What it is:

  • a system of checks and permissions

  • intended to safeguard security-relevant
    code sections

  • embodied by SecurityManager

Security Manager

What you need to know:

  • barely used but maintenance-intensive

  • already disallowed by default

  • enable with java.security.manager=allow

  • in a future release, it will be removed

Security Manager

What you need to do:

  • observe your app with default settings
    (⇝ security manager disallowed)

  • if used, move away from security manager

Primitive constructors

What it is:

  • new Integer(42)

  • new Double(42)

  • etc.

Primitive constructors

What you need to know:

  • Valhalla wants to turn them into value types

  • those have no identity

  • identity-based operations need to be removed

Primitive constructors

What you need to do:

  • Integer.valueOf(42)

  • Double.valueOf(42)

  • etc.

More

Break

We’ll see you back here in 15 minutes with Part III:

  • Source and Classfile Tools

  • Development and Deployment Tools

  • Security Tools

  • Monitoring Tools

Java 21 💣💥

Source and Classfile Tools
Development and Deployment Tools
Security Tools
Monitoring Tools

Fundamental tools of the JDK

The tools you use to create and build Java programs.

Standard compiler optimizations

  • Literal constants are folded.

  • String concatenation is folded.

  • Constant fields are inlined.

  • Dead code branches are eliminated.

These do not require any flag and are part
of the compiler specification before JDK 17.

String concatenation strategy

Before JDK 19

public class Example {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("foo");
        System.err.println("" + sb + sb.append("bar"));
    }
}
// prints foobarfoobar

String concatenation strategy

After JDK 19, string concatenation evaluates each argument and eagerly converts it to a string, in left-to-right order.

public class Example {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("foo");
        System.err.println("" + sb + sb.append("bar"));
    }
}
// prints foofoobar

[Indy String Concat Changes Order of Operations — JDK-8273914]

Warnings on lossy conversions

Evaluation error when compiling:

public class Example {
    public static void main(String[] args) {
        long value = 5L;
		long b = value + 0.1 * 3L;
    }
}
// incompatible types: possible lossy conversion
// from double to long

Warnings on lossy conversions

Before JDK 20, no error is produced when compiling:

public class Example {
    public static void main(String[] args) {
        long b = 5L;
		b += 0.1 * 3L;
    }
}

Warnings on lossy conversions

After JDK 20, get a warning when compiling:

javac Example.java -Xlint:all

Example.java:4: warning:
[lossy-conversions] implicit cast from double
to long in compound assignment is possibly lossy
     b += 0.1 * 3L;
               ^
 1 warning

[Warn compound assignment is possibly lossy  — JDK-8244681]

Warnings about possible this escapes

DO NOT call overridable methods from a constructor!

public class LintExample {
	public LintExample() {
		System.out.println(this.hashCode());
	}

	public static void main(String[] args) {
		new LintExample();
	}
}

Warnings about possible this escapes

Before JDK 21, LintExample.java compiles successfully

javac LintExample.java -Xlint:all

Warnings about possible this escapes

The compiler has a new -Xlint:this-escape key:

javac LintExample.java -Xlint:[all|this-escape]

LintExample.java 3: warning:
[this-escape] possible 'this' escape before subclass
is fully initialized
    System.out.println(this.hashCode());                                      									^
 1 warning

[New lint check key ㉑ — JDK-8015831]

Generate great API documentation

Goals for API documentation

  • Helps with product maintenance.

  • Technical users can understand your APIs goals.

  • Can increase awareness/adoption of your software.

  • Third-party developers can start quickly by trying out API examples.

Words may come easy, yet examples require extra care.

Inserting fragments of source code in documentation

  • Wrap code examples inside <pre> and {@code …​}

  • Automatically escape special characters with {@code …​}

  • Little control over indentation

  • No code highlighting

Simplify inclusion of code examples

  • JEP 413 introduced {@snippet …​} tag in JDK 18.

  • A better presentation of the code examples via regions.

  • Control code via @highlight, @replace, @link tags and regions.

Simplify inclusion of code examples

  • JEP 413 introduced {@snippet …​} tag in JDK 18.

  • A better presentation of the code examples via regions.

  • Control code via @highlight,@replace,@link tags and regions.

  • The tag accepts separate files that hold snippet content.

$ javadoc # other options..
    --snippet-path ./src/xml User.java

Benefit of interactive documentation

Configure the --add-script <file> option of javadoc

cat interact.js
alert("Get ready to move your fingers!");
javadoc -d docs # other options..........
	--add-script interact.js User.java

Use the option to add multiple scripts in your generated documentation.

More

Java 21 💣💥

Source and Classfile Tools
Development and Deployment Tools
Security Tools
Monitoring Tools

Fast prototyping Java code with jshell

  • Quickly try, debug and learn Java and its APIs.

  • Experiment with Java by bypassing the compile stage.

  • Get immediate feedback on your code.

  • Exposes a programmatic API

Highlighting deprecated elements, variables and keywords

jshell> var check = new Boolean("true");
|  Warning:
|  Boolean(java.lang.String) in java.lang.Boolean
|  has been deprecated and marked for removal
|  var check = new Boolean("true");
|              ^-----------------^
check ==> true

[jshell outlines deprecated elements ⑲ — JDK-8274148]

JDK tool access in jshell

  • jshell supports loading scripts.

  • Scripts can be local files or one of the predefined scripts.

  • Scripts may hold any valid code snippets or jshell commands.

Predefined jshell scripts

ScriptDescription

DEFAULT

Loads the default entries, which are commonly used as imports.

JAVASE

Imports all Java SE packages.

PRINTING

Defines print,println, and printf as jshell methods for use within the tool.

TOOLING ㉑

Defines javac, jar, and other methods for running JDK tools within jshell.

Loading TOOLING script

  • When you start jshell

jshell TOOLING
  • Inside a jshell session

jshell> /open TOOLING

[JDK tools in jshell ㉑ — JDK-8306560]

Using the TOOLING script

  • Check available tools

    jshell> tools()
  • Execute a tool command in a jshell session

    jshell> run("javac", "-version")
    jshell> javac("-version")
    
    javac 21

What tool would you use for local prototyping, testing or debugging a client-server setup?

[Introducing jwebserver ⑱ — JEP 408]

Use cases for jwebserver

Web development testing, to simulate locally a client-server setup.

jwebserver

Binding to loopback by default.
For all interfaces use "-b 0.0.0.0" or "-b ::".
Serving /cwd and subdirectories on 127.0.0.1 port 8000
URL: http://127.0.0.1:8000/

Use cases for jwebserver

Web-service prototyping or application testing.

jwebserver -d api -p 9000

Binding to loopback by default.
For all interfaces use "-b 0.0.0.0" or "-b ::".
Serving /api and subdirectories on 127.0.0.1 port 9000
URL http://127.0.0.1:9000/
127.0.0.1 - - [01/Feb/2024:14:27:26 +0100]
"GET /activity.json HTTP/1.1" 200 -

Use cases for jwebserver

Search a directory on a remote server from your local machine.

jwebserver -b 0.0.0.0

Serving /work and subdirectories
on 0.0.0.0 (all interfaces) port 8000
URL http://192.168.178.41:8000/

Working with the Simple Web Server API

The SimpleFileServer class supports the creation of:

  • file server

  • file handler

  • an output filter

[Working with the Simple Web Server — inside.java article]

Distributing modular applications

Let’s assume the following project structure:

src/main/java
├── module-info.java
└── org
    └── example
        └── HelloWorldFX.java

cat src/main/java/module-info.java
module hellofx {
	requires javafx.controls;
	exports org.example;
}

Make performant module files

jmod creates module files that encapsulate a set of
compiled Java classes, resources and other related files.

jmod create --class-path mods/helloworldfx \
    --main-class org.example.HelloWorldFX \
    --module-version 1.0.0 \
    --compress zip-9 hellofx.jmod

Specify a module’s compression level

Use --compress command line option:

  • accepts values between zip-[0-9]

  • zip-0 provides no compression,

  • zip-9 provides the best compression

  • the default compression value is zip-6

[jmod --compress option — JDK-8293499]

Why does that matter?

You can further create an optimal application image.

jlink --launcher hello=hellofx/example.HelloWorldFX \
    --module-path $PATH_TO_JMODS:/hellofx.jmod \
    --add-modules hellofx \
    --output my-app

Packaging Java Applications

Intro to jpackage

  • Packages self-contained Java applications.

  • Prior JDK19, installing a jpackaged app was system-wide.

  • For modular applications, jpackage will automatically run jlink and generate a runtime with the modules needed.

Using jpackage

jpackage --input target/ --name JDemoApp \
    --type app-image --main-jar JDemoApp.jar \
    --main-class JDemoApp
JDemoApp.app/
  Contents/
    Info.plist
    MacOS/               // Application launchers
      JDemoApp
    Resources/           // Icons, etc.
    app/
      JDemoApp.cfg     // Config info, done by jpackage
      JDemoApp.jar     // copied from --input directory
    runtime/           // Java runtime image

Installation of a jpackaged application after JDK 19

Application launcher will search for .cfg in user-specific folders.

Linux
    ~/.local/${PACKAGE_NAME}
    ~/.${PACKAGE_NAME}
macOS
    ~/Library/Application Support/${PACKAGE_NAME}
Windows
    %LocalAppData%\%PACKAGE_NAME%
    %AppData%\%PACKAGE_NAME%

# ${PACKAGE_NAME} and %PACKAGE_NAME%
# refer to jpackaged application name.

Installation of a jpackaged application after JDK 19

Application launcher will look up the .cfg file:

  • In user-specific directories,

  • From the installation directory if .cfg file is not found,

  • From the application image directory if the application launcher is executed from application image.

More

Java 21 💣💥

Source and Classfile Tools
Development and Deployment Tools
Security Tools
Monitoring Tools

New version option

You can check the version of keytool and jarsigner.

keytool -version & jarsigner -version

[1] 83711
jarsigner 21
keytool 21

[Add -version option to keytool and jarsigner ⑱ — JDK-8272163]

Updated Options for keytool

As of JDK 21, keytool warns you when using weak password-based encryption algorithms via:

  • -genseckey option

  • -importpass option

[keytool warns about weak PBE algorithms ㉑ — JDK-8286907]

Detecting weak password-based encryption algorithms

keytool -genseckey -alias secret -keypass changeit \
    -keyalg RC4 -keysize 128 -keystore example.p12 \
    -storepass changeit -storetype PKCS12 -v
Generated 128-bit ARCFOUR secret key
[Storing example.p12]
Warning:
The generated secret key uses the ARCFOUR algorithm
which is considered a security risk.

Updated Options for jarsigner

  • Removed in JDK 21: -altsigner and -altsignerpath

  • As of JDK 19, specify the classpath for providers
    via –providerPath.

Loading external keystore implementations

jarsigner -keystore keystore -storetype CUSTOMKS \
  -providerPath /path/to/test.myks \
  -providerClass my.custom.AnotherProvider \
  signed.jar mykey

[Add -providerPath option to jarsigner ⑲ — JDK-8281175]

More

Java 21 💣💥

Source and Classfile Tools
Development and Deployment Tools
Security Tools
Monitoring Tools

Monitoring Java applications

TechnologyGoal

JDK Flight Recorder (JFR)

Collects diagnostic and profiling data about a running Java application.

JFR Event Streaming API

API for the continuous consumption of JFR data on disk.

JDK Mission Control (JMC)

A set of tools for managing, monitoring, profiling, and troubleshooting Java applications.

New JFR view command

  • Start JFR a recording via -XX:StartFlightRecording or jcmd.

java -XX:StartFlightRecording -jar imaging.jar
  • Use the PID or jar name in the command

Note
Use jps JDK tool to list all running Java processes.

[JFR view command ㉑ — JDK-8306704]

JFR event for finalization

jdk.FinalizerStatistics

  • Identifies classes at runtime that use finalizers.

  • Enabled by default in default.jfc and profile.jfc.

  • No event is sent if java --finalization=disabled.

[A finalization JFR event — JDK-8266936]

JFR event for finalization

Disable via

jfr configure jdk.FinalizerStatistics#enabled=false
# or on launch
java -XX:StartFlightRecording:settings=none,
+jdk.FinalizerStatistics#enabled=false

Recording initial security properties with JFR

jdk.InitialSecurityProperty cryptographic event

  • Enabled by default in default.jfc and profile.jfc.

  • Captures details of initial security properties when loaded via the java.security.Security class.

Recording initial security properties with JFR

Disable via

jfr configure jdk.InitialSecurityProperty#enabled=false
# or on launch
java -XX:StartFlightRecording:settings=none,
+jdk.InitialSecurityProperty#enabled=false

Recording details about security provider instance requests

jdk.SecurityProviderService cryptographic event

  • Disabled by default in default.jfc and profile.jfc.

  • Records info on java.security.Provider.getService(…​) calls.

Recording details about security provider instance requests

Enable via

jfr configure jdk.SecurityProviderService#enabled=true
# or on launch
java -XX:StartFlightRecording:settings=none,
+jdk.SecurityProviderService#enabled=true

Performance analysis and monitoring

  • Rendering large graphs is possible with smart pruning.

  • Aggregate Flame graph and Stack trace via Samples.

  • Dependency view presents aggregation of events using hierarchical edge bundling.

More

High quality from the source

Dev.java
Inside.java
youtube.com/java

Image Credits