Java 22

Better Language, Better APIs, Better Runtime

Developer Advocate

Java Team at Oracle

Let’s get started!

Lots to talk about!

Final features in Java 22:

  • unnamed patterns

  • FFM API

  • multi-source-file programs

Lots to talk about!

New previews features in Java 22:

  • statements before super

  • stream gatherers

  • class-file API

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 performance

  • less observability

  • 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?

Java 22

Final Features
Unnamed Patterns
FFM API
Launch Multi-File Programs
New Preview Features

Avoiding default in switch.

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!

New Page Type

If GitHubCommitPage is added:

public void categorize(Page page) {
	// error:
	//     "the switch statement does not cover
	//      all possible input values"
	switch (page) {
		case GitHubIssuePage is -> categorizeIssue(is);
		case GitHubPrPage pr -> categorizePr(pr);
		case ExternalPage ext -> categorizeExternal(ext);
		case ErrorPage err -> categorizeError(err);
	}
}

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 in Java 21

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 -> { };
	};
}

This is the state-of-the-art in Java 21
(without preview features).

Avoiding Default in Java 22

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. 🥳

More

Java 22

Final Features
Unnamed Patterns
FFM API
Launch Multi-File Programs
New Preview Features

Cutting the Isthmus between Java and native code.

Foreign memory

Storing data off-heap is tough:

  • ByteBuffer is limited (2GB) and inefficient

  • Unsafe is…​ unsafe and not supported

Foreign-memory API

Panama introduces safe and performant API:

  • control (de)allocation:
    Arena, MemorySegment, SegmentAllocator

  • to access/manipulate: MemoryLayout, VarHandle

Foreign-memory API

// create `Arena` to manage off-heap memory lifetime
try (Arena offHeap = Arena.ofConfined()) {
	// [allocate off-heap memory to store pointers]
	// [do something with off-heap data]
	// [copy data back to heap]
} // off-heap memory is deallocated here

Foreign-memory API

Allocate off-heap memory to store pointers:

String[] javaStrings = { "mouse", "cat", "dog" };
// Arena offHeap = ...

MemorySegment pointers = offHeap.allocateArray(
	ValueLayout.ADDRESS, javaStrings.length);
for (int i = 0; i < javaStrings.length; i++) {
	// allocate off-heap & store a pointer
	MemorySegment cString = offHeap
		.allocateUtf8String(javaStrings[i]);
	pointers
		.setAtIndex(ValueLayout.ADDRESS, i, cString);
}

Foreign-memory API

Copy data back to heap:

// String[] javaStrings = ...
// MemorySegment pointers =

for (int i = 0; i < javaStrings.length; i++) {
	MemorySegment cString = pointers
		.getAtIndex(ValueLayout.ADDRESS, i);
	javaStrings[i] = cString.getUtf8String(0);
}

Foreign functions

JNI isn’t ideal:

  • involves several tedious artifacts (header file, impl, …​)

  • can only interoperate with languages that align
    with OS/architecture the JVM was built for

  • doesn’t reconcile Java/C type systems

Foreign-function API

Panama introduces streamlined tooling/API
based on method handles:

  • jextract: generates method handles from header file

  • classes to call foreign functions
    Linker, FunctionDescriptor, SymbolLookup

Foreign-function API

// find foreign function on the C library path
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
MethodHandle radixSort = linker
	.downcallHandle(stdlib.find("radixsort"), ...);

String[] javaStrings = { "mouse", "cat", "dog" };
try (Arena offHeap = Arena.ofConfined()) {
	// [move Java strings off heap]
	// invoke foreign function
	radixSort.invoke(
		pointers, javaStrings.length,
		MemorySegment.NULL, '\0');
	// [copy data back to heap]
}

Finally final!

Java 22 finalizes the FFM API, but there’s more to do:

  • user-friendly and performant mapping from
    native memory to Java records/interfaces

  • improving jextract and surrounding tooling

And more.

More

Java 22

Final Features
Unnamed Patterns
FFM API
Launch Multi-File Programs
New Preview Features

Keeping Java approachable.

A Mature Ecosystem

Java is very mature:

  • refined programming model

  • detailed toolchain

  • rich ecosystem

But this can make it hard to learn for new (Java) developers.

Approachable Java

Java needs to be approachable:

  • for kids

  • for students

  • for the frontend dev

  • for ML/AI folks

  • etc.

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

On-Ramp to Java

On-ramp:

  • simplified main method and class

  • single-source-file execution

  • multi-source-file execution

Simpler Code

Remove requirement of:

  • String[] args parameter

  • main being static

  • main being public

  • the class itself

// smallest viable Main.java
void main() {
	System.out.println("Hello, World!");
}

[Preview in Java 22 — JEP 463]

Single-File Execution

It’s easy to execute that file:

java Main.java

[Introduced in Java 11 — JEP 330]

Multi-File Execution

The program can expand:

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

Run with:

java -cp 'Lib/*' Main.java

[Introduced 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!

More

Simple main:

Single-source-file execution:

Multi-source-file execution:

Java 22

Final Features
New Preview Features
Statements Before Super
Stream Gatherers
Class-File API

A quality-of-life improvement.

Constructor Chaining

With multiple constructors, it’s good practice
to have one constructor that:

  • checks all arguments

  • assigns all fields

Other constructors just forward (if possible).

Constructor Chaining

class Name {

	private final String first;
	private final String last;

	Name(String first, String last) {
		// [... checks for null, etc. ...]
		this.first = first;
		this.last = last;
	}

	Name(String last) {
		this("", last);
	}

}

Inheritance

With superclasses, chaining is enforced:

class ThreePartName extends Name {

	private final String middle;

	ThreePartName(
			String first, String middle, String last) {
		// doesn't compile without this call
		super(first, last);
		this.middle = middle;
	}

}

Limitations

But:

Java allows no statements before
super(…​) / this(…​)!

Why?

No statements before super(…​) / this(…​):

  • superclass should be initialized
    before subclass runs any code
    ⇝ no code before super(…​)

  • code before this(…​) would
    run before super(…​)
    ⇝ no code before this(…​)

Limitations

This is inconvenient when you want to:

  • check arguments

  • prepare arguments

  • split/share arguments

Splitting Arguments

class Name {

	// fields and constructor as before

	Name(String full) {
		// does the same work twice
		this(
			full.split(" ")[0],
			full.split(" ")[1]);
	}

}

Splitting Arguments

class Name {

	// fields and constructor as before

	// avoids two splits but "costs"
	// duplicated argument validation
	Name(String full) {
		String[] names = full.split(" ");
		// [... checks for null, etc. ...]
		this.first = names[0];
		this.last = names[1];
	}

}

Splitting Arguments

class Name {

	// fields and constructor as before

	// avoids two splits but "costs"
	// an additional constructor
	Name(String full) {
		this(full.split(" "));
	}

	private Name(String[] names) {
		this(names[0], names[1]);
	}

}

Splitting Arguments

class Name {

	// fields and constructor as before

	// avoids two splits but "costs"
	// an additional construction protocol
	static Name fromFullName(String full) {
		String[] names = full.split(" ");
		return new Name(names[0], names[1]);
	}

}

Limitations - Record Edition

To enforce a uniform construction protocol:

Records require all custom constructors
to (eventually) call the canonical constructor.

Limitations - Record Edition

record Name(String first, String last) {

	// nope
	Name(String full) {
		String[] names = full.split(" ");
		// [... checks for null, etc. ...]
		this.first = names[0];
		this.last = names[1];
	}

}

Splitting Arguments

What we want to write:

record Name {

	Name(String full) {
		String[] names = full.split(" ");
		this(names[0], names[1]);
	}

}

(Analogous for classes.)

Statements Before …​

Java 22 previews statements
before super(…​) and this(…​).

Great to…​

Check Arguments

class ThreePartName extends Name {

	private final String middle;

	ThreePartName(
			String first, String middle, String last) {
		// can't have a middle name without a first name
		requireNonNullNonEmpty(first);
		super(first, last);
		this.middle = middle;
	}

}

Prepare Arguments

class ThreePartName extends Name {

	private final String middle;

	ThreePartName(
			String first, String middle, String last) {
		// shorten first if middle is given
		var short1st = middle.length() == 1
				? first.substring(0, 1)
				: first;
		super(first, last);
		this.middle = middle;
	}

}

Split Arguments

class ThreePartName extends Name {

	private final String middle;

	ThreePartName(String full) {
		// split "first middle last" on space (once 🙌🏾)
		var names = full.split(" ");
		super(names[0], names[2]);
		this.middle = names[1];
	}

}

More

Java 22

Final Features
New Preview Features
Statements Before Super
Stream Gatherers
Class-File API

Like collect, but intermediate.

Missing Stream Ops

Streams are great, but some
intermediate operations are missing:

  • sliding windows

  • fixed groups

  • take-while-including

  • scanning

  • increasing sequences

  • etc.

Missing Terminal Ops

Streams also don’t have all possible terminal operations.

Instead:

  • generalization for terminal ops ⇝ collectors

  • a few implementations, e.g. Collectors.toSet()

  • extension point for them: Stream::collect

Let’s do the same for intermediate ops!

Introducing Gatherers

The gatherers API consists of:

  • generalization for intermediate ops ⇝ gatherers

  • a few implementations, e.g. Gatherers.scan(…)

  • extension point for them: Stream::gather

Stream.of("A", "C", "F", "B", "S")
	.gather(scan(...))
	.forEach(System.out::println);

Gatherer Building Blocks

One required building block:

Integrator
  • accepts (state, element, downstream)

  • has the task to combine state and element

    • to update the state

    • to emit 0+ result(s) to downstream

Integrator Example

Behaves transparently:

static <T> Gatherer<T, ?, T> transparent() {
	Integrator<Void, T, T> integrator = (_, el, ds)
		-> ds.push(el);
	return Gatherer.of(integrator);
}

Integrator Example

Reimplements Stream::map:

static <T, R> Gatherer<T, ?, R> map(Function<T, R> f) {
	Integrator<Void, T, R> integrator = (_, el, ds) -> {
		R newEl = f.apply(el);
		return ds.push(newEl);
	};
	return Gatherer.of(integrator);
}

Gatherer Building Blocks

Three optional building blocks:

Initializer:

creates instance(s) of state

Finisher:
  • accepts (state, downstream)

  • emits 0+ element(s) to downstream

Combiner:

combines to states into one

Fixed-Sized Groups

Create groups of fixed size:

  • stream input: "A", "C", "F", "B", "S"

  • output of groups(2): ["A", "C"], ["F", "B"], ["S"]

We need:

  • an initializer to create empty group list

  • an integrator that emits when group is full

  • a finisher to emit incomplete group

Fixed-Sized Group Initializer

Supplier<List<T>> initializer = ArrayList::new;

Fixed-Sized Group Integrator

Integrator<List<T>, T, List<T>> integrator =
	(state, el, ds) -> {
		state.add(el);

		if (state.size() < size)
			return true;
		else {
			var group = List.copyOf(state);
			state.clear();
			return ds.push(group);
		}
	};

Fixed-Sized Group Finisher

BiConsumer<List<T>, Downstream<List<T>>> finisher =
	(state, ds) -> {
		var group = List.copyOf(state);
		ds.push(group);
	};

Fixed-Sized Group Gatherer

static <T> Gatherer<T, ?, List<T>> groups(int size) {
	Supplier<...> initializer = // ...
	Integrator<...> integrator = // ...
	BiConsumer<...> finisher = // ...

	return Gatherer.ofSequential(
		initializer, integrator, finisher);
}

Fixed-Sized Group Gatherer

Using our gatherer:

Stream.of("A", "C", "F", "B", "S")
	.gather(groups(2))
	.forEach(System.out::println);

// [A, C]
// [F, B]
// [S]

More

Java 22

Final Features
New Preview Features
Statements Before Super
Stream Gatherers
Class-File API

Unlocking easier Java updates.

Bytecode Basics

Bytecode is instruction set for JVM:

  • creating objects and arrays

  • copying variable values or references
    between stack and registers

  • invoking methods

  • computing arithmetic operations

  • etc.

Bytecode Basics

Basic lifecycle:

  • generated by javac

  • stored in .class files

  • loaded, parsed, verified by class loader

  • executed by JVM

Bytecode Beyond Basics

In real life, much more happens:

  • generated by frameworks at build time

  • turned into machine code by JIT compiler
    or Graal native image

  • prefetched by class-data sharing

  • analyzed by jdeps, SpotBugs, etc.

  • manipulated by agents and libraries

Bytecode Tools

Tooling:

  • libraries don’t manipulate bytecode themselves

  • they use a few tools

Big player is ASM
(direct or, e.g., via ByteBuddy or CGLIB).

Migration Pains

Updates:

  • bytecode has a level (e.g. 65 for Java 21)

  • tools can’t work with a higher level
    than they were built for

This can block updates!

E.g. when compiling your code with Java 25 (level 69)…​

Migration Pains

This is the reason for:

Before updating the JDK,
update all dependencies.

We want to move past that!

Class-File API

An API in Java that allows
analyzing and manipulating bytecode:

  • stable API in JDK

  • always up-to-date

When JDK is updated:

  • it may read new bytecodes

  • but that’s ok for most use cases

More

Java 22

In a few slides…​

Java 22 Finalizes

Java 22 Continues

Java 22 Previews

Java 22

Not explosive like Java 21,
but no slouch either.

Continues Java’s evolution.

Get it at jdk.java.net/22.

So long…​

37% off with
code fccparlog

bit.ly/the-jms

More

Slides at slides.nipafx.dev
⇜ Get my book!

Follow Nicolai

nipafx.dev
/nipafx

Follow Java

inside.java // dev.java
/java    //    /openjdk

Image Credits