public sealed interface Page
permits GitHubIssuePage, GitHubPrPage,
ExternalPage, ErrorPage {
// ...
}
this session focuses on Java 22
we can’t cover all details
⇝ go to youtube.com/@java for more
slides at slides.nipafx.dev/java-x
(hit "?" to get navigation help)
ask questions any time
Final features in Java 22:
unnamed patterns
FFM API
multi-source-file programs
New previews features in Java 22:
statements before super
stream gatherers
class-file API
Costs of running on old versions:
support contract for Java
waning support in libraries / frameworks
Costs of not running on new versions:
lower productivity
less performance
less observability
less access to talent
bigger upgrade costs
Resistence is futile.
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
Prepare by building on multiple JDK versions:
your baseline version
every supported version since then
latest version
EA build of next version
It’s not necessary to build …
… each commit on all versions
… the whole project on all versions
Build as much as feasible.
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.
Final Features |
Unnamed Patterns |
FFM API |
Launch Multi-File Programs |
New Preview Features |
Avoiding default
in switch
.
Features:
scrapes GitHub projects
creates Page
instances:
GitHubIssuePage
GitHubPrPage
ExternalPage
ErrorPage
further processes pages
Features:
display as interactive graph
compute graph properties
categorize pages by topic
analyze mood of interactions
process payment for analyses
etc.
How to implement features?
methods on Page
😧
visitor pattern 😫
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 types limit inheritance,
by only allowing specific subtypes.
public sealed interface Page
permits GitHubIssuePage, GitHubPrPage,
ExternalPage, ErrorPage {
// ...
}
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);
}
}
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!
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);
}
}
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
!
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).
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. 🥳
Final Features |
Unnamed Patterns |
FFM API |
Launch Multi-File Programs |
New Preview Features |
Cutting the Isthmus between Java and native code.
Storing data off-heap is tough:
ByteBuffer
is limited (2GB) and inefficient
Unsafe
is… unsafe and not supported
Panama introduces safe and performant API:
control (de)allocation:
Arena
, MemorySegment
, SegmentAllocator
to access/manipulate: MemoryLayout
, VarHandle
// 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
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);
}
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);
}
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
Panama introduces streamlined tooling/API
based on method handles:
jextract
: generates method handles from header file
classes to call foreign functions
Linker
, FunctionDescriptor
, SymbolLookup
// 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]
}
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.
📝 JEP 454
🎥 Project Panama - Foreign Function & Memory API (Maurizio Cimadamore)
Final Features |
Unnamed Patterns |
FFM API |
Launch Multi-File Programs |
New Preview Features |
Keeping Java approachable.
Java is very mature:
refined programming model
detailed toolchain
rich ecosystem
But this can make it hard to learn for new (Java) developers.
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:
simplified main
method and class
single-source-file execution
multi-source-file execution
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]
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]
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!
Final Features |
New Preview Features |
Statements Before Super |
Stream Gatherers |
Class-File API |
A quality-of-life improvement.
With multiple constructors, it’s good practice
to have one constructor that:
checks all arguments
assigns all fields
Other constructors just forward (if possible).
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);
}
}
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;
}
}
But:
Java allows no statements before
super(…)
/ this(…)
!
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(…)
This is inconvenient when you want to:
check arguments
prepare arguments
split/share arguments
class Name {
// fields and constructor as before
Name(String full) {
// does the same work twice
this(
full.split(" ")[0],
full.split(" ")[1]);
}
}
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];
}
}
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]);
}
}
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]);
}
}
To enforce a uniform construction protocol:
Records require all custom constructors
to (eventually) call the canonical constructor.
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];
}
}
What we want to write:
record Name {
Name(String full) {
String[] names = full.split(" ");
this(names[0], names[1]);
}
}
(Analogous for classes.)
Java 22 previews statements
before super(…)
and this(…)
.
Great to…
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;
}
}
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;
}
}
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];
}
}
Final Features |
New Preview Features |
Statements Before Super |
Stream Gatherers |
Class-File API |
Like collect
, but intermediate.
Streams are great, but some
intermediate operations are missing:
sliding windows
fixed groups
take-while-including
scanning
increasing sequences
etc.
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!
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);
One required building block:
accepts (state, element, downstream)
has the task to combine state
and element
to update the state
to emit 0+ result(s) to downstream
Behaves transparently:
static <T> Gatherer<T, ?, T> transparent() {
Integrator<Void, T, T> integrator = (_, el, ds)
-> ds.push(el);
return Gatherer.of(integrator);
}
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);
}
Three optional building blocks:
creates instance(s) of state
accepts (state, downstream)
emits 0+ element(s) to downstream
combines to states
into one
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
Supplier<List<T>> initializer = ArrayList::new;
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);
}
};
BiConsumer<List<T>, Downstream<List<T>>> finisher =
(state, ds) -> {
var group = List.copyOf(state);
ds.push(group);
};
static <T> Gatherer<T, ?, List<T>> groups(int size) {
Supplier<...> initializer = // ...
Integrator<...> integrator = // ...
BiConsumer<...> finisher = // ...
return Gatherer.ofSequential(
initializer, integrator, finisher);
}
Using our gatherer:
Stream.of("A", "C", "F", "B", "S")
.gather(groups(2))
.forEach(System.out::println);
// [A, C]
// [F, B]
// [S]
Final Features |
New Preview Features |
Statements Before Super |
Stream Gatherers |
Class-File API |
Unlocking easier Java updates.
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.
Basic lifecycle:
generated by javac
stored in .class
files
loaded, parsed, verified by class loader
executed by JVM
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
Tooling:
libraries don’t manipulate bytecode themselves
they use a few tools
Big player is ASM
(direct or, e.g., via ByteBuddy or CGLIB).
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)…
This is the reason for:
Before updating the JDK,
update all dependencies.
We want to move past that!
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
In a few slides…
Not explosive like Java 21,
but no slouch either.
Continues Java’s evolution.
Get it at jdk.java.net/22.
all other images are copyrighted