// option A
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// option B
import java.util.*;
JDK 25: jdk.java.net/25
this session focuses on Java 25
but we will also have time for 24, 23, 22
slides at slides.nipafx.dev/java-x
(hit "?" to get navigation help)
ask questions any time
Most final features in Java 25:
module imports
simplified main
flexible constructor bodies
scoped values
AOT method profiling & easier CLI
JFR improvements
compact object headers
Most final features in Java 24:
stream gatherers
class-file API
less virtual thread pinning
AOT class loading & linking
generational ZGC, only
Final features in Java 23:
Markdown in JavaDoc
Most final features in Java 22:
unnamed patterns
FFM API
multi-source-file programs
Current preview/incubating/experimental features:
primitive patterns
string templates
vector API
structured concurrency
stable values
PEM encoding
JFR CPU-time profiling
Final in Java 25 |
Module Imports |
Simplified Main |
Flexible Constructor Bodies |
Scoped Values |
Ahead-of-Time Computation |
Compact Object Headers |
… |
Which one do you prefer?
// option A
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// option B
import java.util.*;
Upsides:
more succinct
easier to manage manually
Downsides:
less clear
chance of conflicts
arbitrary slice of API
import module $mod;
imports public API of $mod
your code does not need to be in a module
import module $mod;
Imports all public top-level types in:
packages exported by $mod
packages exported by $mod
to your module
(qualified exports)
packages exported by modules that $mod
requires transitively (implied readability)
Upsides:
much more succinct
trivial to manage manually
Downsides:
less detailed
conflicts are more likely
import module java.base;
import module java.desktop;
import module java.sql;
public class Example {
public void example() {
// error: reference to Date is ambiguous
var outdated = new Date(1997, 1, 18);
// error: reference to List is ambiguous
var letters = List.of("I", "J", "N");
}
}
import module java.base;
import module java.desktop;
import module java.sql;
import java.util.Date;
import java.util.List;
public class Example {
public void example() {
var outdated = new Date(1997, 1, 18);
var letters = List.of("I", "J", "N");
}
}
Consider using module imports, when:
you’re already using star imports
you’re writing scripts, experiments, demos, etc.
Compact source files implicitly import java.base:
// complete Main.java - no explicit imports!
void main() {
List<?> dates = Stream
.of(1, 2, 23, 29)
.map(BigDecimal::new)
.map(day -> LocalDate.of(
2024,
RandomGenerator.getDefault()
.nextInt(11) + 1,
day.intValue()))
.toList();
IO.println(dates);
}
๐ JEP 511: Module Import Declarations
Final in Java 25 |
Module Imports |
Simplified Main |
Flexible Constructor Bodies |
Scoped Values |
Ahead-of-Time Computation |
Compact Object Headers |
… |
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() {
// ...
}
Use java.lang.IO
instead of System.{in|out}
:
void print(Object)
& void println(Object)
to write
String readln(String)
to read input
void main() {
var name = IO.readln("Please enter your name: ");
IO.println("Nice to meet you, " + name);
}
Compact source files implicitly import java.base:
// complete Main.java - no explicit imports!
void main() {
List<?> dates = Stream
.of(1, 2, 23, 29)
.map(BigDecimal::new)
.map(day -> LocalDate.of(
2024,
RandomGenerator.getDefault()
.nextInt(11) + 1,
day.intValue()))
.toList();
IO.println(dates);
}
It’s easy to execute that file:
java Main.java
The program can expand:
MyFirstJava
โโ Main.java
โโ Helper.java
โโ Lib
โโ library.jar
Run with:
java -cp 'Lib/*' Main.java
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 & modules
Doesn’t even have to be that order!
Final in Java 25 |
Module Imports |
Simplified Main |
Flexible Constructor Bodies |
Scoped Values |
Ahead-of-Time Computation |
Compact Object Headers |
… |
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 {
protected final String first, last;
Name(String first, String last) {
// [... checks, assignments, etc. ...]
}
Name(String fst, String mid, String lst) {
this(fst + " " + mid, lst);
}
}
With superclasses, chaining is enforced:
class ThreePartName extends Name {
protected final String middle;
ThreePartName(String fst, String mid, String lst) {
// doesn't compile without this call
super(fst, lst);
this.middle = mid;
}
}
But:
superclass should be initialized
before subclass runs any code
โ no code before super(…)
code before this(…)
would
run before super(…)
โ no code before this(…)
So: no statements before super(…)
/ this(…)
!
With:
class A
class B extends A
class C extends B
Effective constructor execution for new C()
:
Object
โ A
โ B
โ C
Name(String first, String last) {
this.first = first;
this.last = last;
IO.println(this);
}
ThreePartName(String fst, String mid, String lst) {
super(fst, lst);
this.middle = mid;
}
new ThreePartName("Jane", "Jennifer", "Doe");
// prints: "Jane null Doe" ๐
Limitation is inconvenient when you want to:
check arguments
prepare arguments
split/share arguments
class Name {
protected final String first;
protected final String last;
Name(String first, String last) {
this.first = first;
this.last = last;
}
Name(String full) {
// does the same work twice
this(full.split(" ")[0], full.split(" ")[1]);
}
}
Workarounds:
duplicate argument validation & field assignment
add an "internal" constructor to forward to
add static factory method
To enforce a uniform construction protocol:
Records require all custom constructors
to (eventually) call the canonical constructor.
โ Further limits options.
What we want to write:
class Name {
// [... fields, constructor as before ...]
Name(String full) {
String[] names = full.split(" ");
this(names[0], names[1]);
}
}
(Analogous for records.)
Flexible constructor bodies allow just that!
Constructor(...) {
// prologue
// call to this(...) or super(...)
// epilogue
}
Prologue:
can’t touch this
except to assign fields
With:
class A
class B extends A
class C extends B
Effective constructor execution for new C()
:
prologue C
โ prologue B
โ prologue A
โ Object
โ
epilogue A
โ epilogue B
โ epilogue C
Great to:
check arguments
prepare arguments
split/share arguments
assign fields
class ThreePartName extends Name {
protected final String middle;
ThreePartName(String fst, String mid, String lst) {
// can't have a middle name without a first name
requireNonNullNonEmpty(fst);
this.middle = mid;
super(fst, lst);
}
}
class ThreePartName extends Name {
protected final String middle;
ThreePartName(String fst, String mid, String lst) {
this.middle = mid;
// shorten first if middle is given
var shortFst = mid.length() == 1
? fst.substring(0, 1)
: fst;
super(shortFst, lst);
}
}
class ThreePartName extends Name {
protected final String middle;
ThreePartName(String full) {
// split "first middle last" on space (once ๐๐พ)
var names = full.split(" ");
this.middle = names[1];
super(names[0], names[2]);
}
}
Project Valhalla ponders null-restricted fields
code must never observe such a field being null
has to work with super()
subclass sets non-null fields before super()
๐ JEP 513: Flexible Constructor Bodies
๐ฅ Valhalla - Where Are We? (Brian Goetz - Aug 2024)
Final in Java 25 |
Module Imports |
Simplified Main |
Flexible Constructor Bodies |
Scoped Values |
Ahead-of-Time Computation |
Compact Object Headers |
… |
Method inputs are usually instance fields or arguments,
but sometimes:
arguments are sensitive or inconvenient
data is thread-specific
each thread needs to mutate its own instance
…
Common solution: ThreadLocal
.
static final ThreadLocal<Integer> ANSWER =
new ThreadLocal<>();
void main() {
ANSWER.set(42);
IO.println(ANSWER.get()); // "42"
new Thread(() -> {
IO.println(ANSWER.get()); // "null"
}).start();
IO.println(ANSWER.get()); // "42"
}
Challenges when using ThreadLocal
:
requires manual clean-up
data flow is two-way and possibly complex
inheriting data to child threads can be expensive
ScopedValue
addresses all that.
static final ScopedValue<Integer> ANSWER =
ScopedValue.newInstance();
void main() {
ScopedValue // โฌ VALUE
.where(ANSWER, 42)
// |<---------- SCOPE ----------->|
.run(() -> IO.println(ANSWER.get())); // "42"
// OUT OF SCOPE
ANSWER.get(); // โก๏ธ NoSuchElementException
}
Central properties:
data is only available in run
data flow is one-way
inheriting data to child threads is cheap
(because ThreadLocal
can be mutated)
This comes at the cost of less flexible data flow.
A scoped value can be rebound for a nested scope:
static final ScopedValue<Integer> ANSWER =
ScopedValue.newInstance();
void a() {
ScopedValue
.where(ANSWER, 42)
.run(this::b);
}
void b() {
IO.println(ANSWER.get()); // "42"
ScopedValue
.where(ANSWER, ANSWER.get() + 1)
.run(this::c);
IO.println(ANSWER.get()); // "42"
}
void c() {
IO.println(ANSWER.get()); // "43"
}
Because scoped values are immutable
no copies are needed within a scope.
Structured concurrency is inherently scoped.
โ Tasks/threads of a StructuredTaskScope
inherit scoped values.
(Only this inheritance is possible.)
ScopedValue.where(ANSWER, 42).run(() -> {// scope โโโ
try (var scope = StructuredTaskScope // โ
.open() { // โ
// child threads' scope โโโ โ
var subA = scope.fork(ANSWER::get); // โ โ
var subB = scope.fork(ANSWER::get); // โ โ
// โ โ
scope.join(); // โ โ
// โ โ
var result = subA.get()+subB.get(); // โ โ
IO.println(result); // "84" // โ โ
} // all child threads completed โโโ โ
IO.println(ANSWER.get()) // "42" // โ
} // โโโโโโ
๐ JEP 506: Scoped Values
Final in Java 25 |
Module Imports |
Simplified Main |
Flexible Constructor Bodies |
Scoped Values |
Ahead-of-Time Computation |
Compact Object Headers |
… |
Java has really good peak performance,
but also tends to have:
slow startup time
slow warmup time
Early work by the runtime:
class loading
callsite linkage
constant pool resolution
interpretation
profile gathering
JIT compilation (C1, C2)
Can we do this ahead of time?
But Java is highly dynamic:
class loading
class redefinition
linkage
access control
method dispatch
run-time typing (e.g. casting)
introspection
JIT compilation, decompilation
How to AOT everything?
Project Leyden introduces AOTCache:
observe JVM
capture decisions in AOTCache
(expansion of CDS Archive)
use as "initial state" during future run
fall back to live observation/optimization
if necessary and possible
# training run (โ profile)
$ java -XX:AOTMode=record
-XX:AOTConfiguration=app.aotconf
-cp app.jar com.example.App ...
# assembly phase (profile โ AOTCache)
$ java -XX:AOTMode=create
-XX:AOTConfiguration=app.aotconf
-XX:AOTCache=app.aot
-cp app.jar
# production run (AOTCache โ performance)
$ java -XX:AOTCache=app.aot
-cp app.jar com.example.App ...
Shortcut for most cases:
# training run (โ AOTCache)
$ java -XX:AOTCacheOutput=app.aot
-cp app.jar com.example.App ...
# production run (AOTCache โ performance)
$ java -XX:AOTCache=app.aot
-cp app.jar com.example.App ...
Improve startup time by making the classes of an application instantly available, in a loaded and linked state, when the HotSpot JVM starts.
Spring PetClinic benchmarks:
up to ~40% startup time reduction
AOT cache size of ~130 MB
Improve warmup time by making method-execution profiles from a previous run of an application instantly available, when the HotSpot Java Virtual Machine starts.
Benchmark of a 100_000x loop over a simple stream:
~20% run time reduction
AOT cache size increased by ~2.5%
Limitation so far:
same JDK release / architecture / OS
consistent class path for training and production
consistent module options
limited use of JVMTI agents
Otherwise, AOTCache is ignored.
class data caching with any GC (JEP 516)
code compilation (JDK-833536)
๐ JEP 483: AOT Class Loading and Linking ใ
๐ JEP 515: AOT Method Profiling ใ
๐ JEP 514: AOT CLI Ergonomics ใ
๐ฅ A Preview of What’s Coming in Project Leyden (Oct 2024)
Final in Java 25 |
Module Imports |
Simplified Main |
Flexible Constructor Bodies |
Scoped Values |
Ahead-of-Time Computation |
Compact Object Headers |
… |
Runtime needs to track information per object:
garbage collection: forwarding pointers and object ages
type system: object’s class
locking: unlocked, light-weight, or heavy-weight locks
hashing: identity hash code, once computed
This is stored in the object header.
On 64bit hardware:
95-64 .........................HHHHHHH
63-32 HHHHHHHHHHHHHHHHHHHHHHHH.AAAA.TT
31- 0 CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
A
: GC age
C
: class word (compressed)
H
: hash code
T
: tag bits
This incurs overhead:
objects tend to be small
(many workloads average 256-512 bits)
header size is noticable
(15-30% on such workloads)
โ This is worth optimizing.
The aforementioned subsystems are
complicated and performance-sensitive.
I’ve only told you the easy part of object headers:
locking operations can override upper 62 bits.
Project Lilliput achieved first milestone:
63-32 CCCCCCCCCCCCCCCCCCCCCCHHHHHHHHHH
31- 0 HHHHHHHHHHHHHHHHHHHHHVVVVAAAASTT
A
: GC age
C
: class word
H
: hash code
S
: self-forwarding
T
: tag bits
V
: Valhalla bits
These changes have several downstream effects:
locking operations no longer override
class word is now much smaller and encded differently
GC forwarding operations become more complex
In special cases, these may be detrimental to performance.
In one setting, the SPECjbb2015 benchmark uses 22% less heap space and 8% less CPU time.
In another setting, the number of garbage collections done by SPECjbb2015 is reduced by 15%, with both the G1 and Parallel collectors.
A highly parallel JSON parser benchmark runs in 10% less time.
Add flag:
-XX:+UseCompactObjectHeaders
Report observations to hotspot-dev!
This is vital to:
improve performance, specifically edge cases
decide whether this becomes default/only header encoding
Interaction with other features:
compressed class pointers must be enabled (default)
stack locking must be disabled (default)
Final in Java 25 |
Module Imports |
Simplified Main |
Flexible Constructor Bodies |
Scoped Values |
Ahead-of-Time Computation |
Compact Object Headers |
… |
Since Java 21:
Console
: Locale
overloads for
format
, printf
, readLine
, readPassword
Console
: isTerminal
Reader
: readLines
, readAllAsString
Math
, StrictMath
: more exact overloads,
e.g. unsignedMultiplyExact(int, int)
Instant
: until(Instant)
ForkJoinPool
: schedule…
and more
BodyHandlers
, BodySubscribers
: limiting
Every Java release improves performance, e.g.:
JDK 25 vs 8: +113%/+31% critical/max jOPS in SPECjbb 2015
JDK 25 vs 21: +10%/+5% critical/max jOPS in SPECjbb 2015
JDK 25 vs 21: +70-75% requests/s on Helidon
Many enhancements between Java 21 and 25:
improved security configurations
(JDK-8051959, JDK-8281658, JDK-8311596, file inclusion)
quantum-resistant cryptography
(JEP 486, JEP 496, JEP 497, JEP 510)
API for PEM texts (JEP 470, preview)
various improvements in cryptographic algorithms
What it was:
a system of checks and permissions
intended to safeguard security-relevant
code sections
embodied by SecurityManager
What you need to know:
barely used but maintenance-intensive
disallowed by default since JDK 18
removed in JDK 24
command-line flags cause errors
SecurityManager
still exists
all calls are "no-ops"
What you need to do:
observe your app with security manager disabled
(java.security.manager=disallow
on JDK 12+)
if used, move away from it
Deprecated (for removal)
finalization โฑ (JEP 421)
What it is:
finalize()
methods
a JLS/GC machinery for them
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
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)
You’re probably not using it directly,
but your dependencies may.
Find them with --sun-misc-unsafe-memory-access
:
warn
: to get run-time warnings
debug
: same with more info
deny
: throws exception
Report and help fix!
Non-record final fields can be mutated through reflection.
This causes issues for:
correctness
performance (e.g. constant folding)
A future Java version will require an option
to allow final field mutation.
Native code can undermine Java’s integrity.
App owner should opt in knowingly:
use --enable-native-access
to allow
access to restricted JNI/FFM methods
use --illegal-native-access
for other code
Three options for illegal native access:
allow
warn
(default on JDK 24)
deny
In some future release, deny
will become the only mode.
Prepare now by setting --illegal-native-access=deny
.
What it is:
a component that transforms byte code
uses java.lang.instrument
or JVM TI
launches with JVM or attaches later ("dynamic")
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
What you need to do:
run your app with -XX:-EnableDynamicAgentLoading
observe closely
investigate necessity of dynamic agents
๐ all the aforementioned JEPs
๐ฅ How to Upgrade to Java 21 and Java 25
less virtual thread pinning ใ
stream gatherers ใ
class-file API ใ
generational ZGC, only ใ
Markdown in JavaDoc ใ
unnamed patterns ใ
FFM API ใ
primitive patterns โ
string templates โ
structured concurrency โ
PEM encoding โ
Final in Java 25 |
Final in Java 24 |
Less Virtual Thread Pinning |
Stream Gatherers |
Class-File API |
GenZGC, Only |
Final in Java 23, 22 |
Preview in Java 25 |
A virtual thread:
is a regular Thread
low memory footprint (stack + bytes)
small switching cost
scheduled by the Java runtime
executes on platform thread
waits in memory
(no platform thread blocked)
a pinned VT will block the PT
caused by object monitors,
native calls, class initialization
a captured VT blocks the PT
caused by file I/O
Object monitor implementation:
was bound to OS threads
required deep refactoring
to work with VTs
fix ships with JDK 24
โ No more pinning for synchronized
.
๐ JEP 491: Synchronize Virtual Threads without Pinning
๐ฅ Java 24 Stops Pinning Virtual Threads (Almost) (Nov 2024)
Final in Java 25 |
Final in Java 24 |
Less Virtual Thread Pinning |
Stream Gatherers |
Class-File API |
GenZGC, Only |
Final in Java 23, 22 |
Preview in Java 25 |
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(IO::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
signal when processing done
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 =
(list, el, ds) -> {
list.add(el);
if (list.size() < size)
return true;
else {
var group = List.copyOf(list);
list.clear();
return ds.push(group);
}
};
BiConsumer<List<T>, Downstream<List<T>>> finisher =
(list, ds) -> {
var group = List.copyOf(list);
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(IO::println);
// [A, C]
// [F, B]
// [S]
๐ JEP 485: Stream Gatherers
๐ฅ Teaching Old Streams New Tricks (Viktor Klang)
Final in Java 25 |
Final in Java 24 |
Less Virtual Thread Pinning |
Stream Gatherers |
Class-File API |
GenZGC, Only |
Final in Java 23, 22 |
Preview in Java 25 |
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
prefetched by AOTCache
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
๐ JEP 484: Class-File API
๐ฅ A Classfile API for the JDK (Brian Goetz)
Final in Java 25 |
Final in Java 24 |
Less Virtual Thread Pinning |
Stream Gatherers |
Class-File API |
GenZGC, Only |
Final in Java 23, 22 |
Preview in Java 25 |
Compared to other GCs, ZGC:
optimizes for ultra-low pause times
can have higher memory footprint or higher CPU usage
In JDK 21, ZGC became generational.
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.
Generational mode is:
the default on JDK 23
the only mode on JDK 24+
-XX:+UseZGC
(Default GC is still G1.)
๐ JEP 439: Generational ZGC
๐ JEP 474: ZGC: Generational Mode by Default
๐ JEP 490: ZGC: Remove the Non-Generational Mode
๐ฅ Generational ZGC and Beyond (Aug 2023)
๐ฅ Java’s Highly Scalable Low-Latency GC: ZGC (Mar 2023)
Final in Java 25, 24 |
Final in Java 23 |
Markdown in JavaDoc |
Final in Java 22 |
Preview in Java 25 |
Writing simple JavaDoc is great!
Writing more complex documentation…
where does <p>
go?
do we need </p>
?
code snippets/blocks are cumbersome
lists are verbose
tables are terrible
…
I blame HTML!
Markdown is more pleasant to write:
neither <p>
nor </p>
code snippts/blocks are simple
lists are simple
tables are less terrible
embedding HTML is straightforward
Markdown is widely used and known.
Java now allows Markdown JavaDoc:
each line starts with ///
CommonMark 0.30
links to program elements use extended
reference link syntax: [text][element]
JavaDoc tags work as usual
Wouldn’t this be nice:
/**md
*
* Markdown here...
*
*/
No - reason #1:
/**md
*
* Here's a list:
*
* item #1
* item #1
*
*/
(The leading *
in JavaDoc is optional.)
No - reason #2:
/**md
*
* ```java
* /* a Java inline comment */
* ```
*
*/
(/**
can’t contain */
.)
///
:
no such issues
doesn’t require new Java syntax
(//
already "escapes" parsing)
Inline code with `backticks
`.
Code blocks with fences, e.g.:
```java public void example() { } ```
A language tag is set as a CSS class
for JS-based highlighting in the frontend.
(Add a library with javadoc --add-script …
.)
Use full reference link to add text:
/// - [the _java.base_ module][java.base/]
/// - [the `java.util` package][java.util]
/// - [the `String` class][String]
/// - [the `String#CASE_INSENSITIVE_ORDER` field][String#CASE_INSENSITIVE_ORDER]
/// - [the `String#chars()` method][String#chars()]
Output:
Markdown tables:
better than HTML tables
still uncomfortable to create manually
use something like tablesgenerator.com
Advanced tables:
for features unsupported in Markdown,
create HTML tables
JavaDoc tags work as expected:
can be used in Markdown comments
if they contain text, Markdown syntax works
/// For more information on comments,
/// see {@jls 3.7 Comments}.
///
/// @implSpec this implementation does _nothing_
public void doSomething() { }
๐ JEP 467: Markdown Documentation Comments
Final in Java 25, 24, 23 |
Final in Java 22 |
Unnamed Patterns |
FFM API |
Preview in Java 25 |
Use _
to mark a (pattern) variable as unused, e.g.:
BiConsumer<String, Double> = (s, _) -> // use `s`
Object obj = // ...
if (obj instanceof User(var name, _))
// use `name`
switch (obj) {
case User _ -> userCount++;
case Admin _ -> adminCount++;
}
That last one is very important!
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 Category categorize(Page page) {
return 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 Category categorize(Page page) {
return switch (page) {
// categorize only GitHub pages
case GitHubIssuePage is -> categorizeIssue(is);
case GitHubPrPage pr -> categorizePr(pr);
// return Category.NONE for other page types
}
}
How to handle remaining cases?
Unlike an if
-else
-if
-chain,
a pattern switch
needs to be exhaustive.
Fulfilled by:
a default
branch
explicit branches:
switching over a sealed types
a case
per subtype
Option 1:
public Category categorize(Page page) {
return switch (page) {
case GitHubIssuePage is -> categorizeIssue(is);
case GitHubPrPage pr -> categorizePr(pr);
default -> Category.NONE;
}
}
If GitHubCommitPage
is added:
public Category categorize(Page page) {
return switch (page) {
case GitHubIssuePage is -> categorizeIssue(is);
case GitHubPrPage pr -> categorizePr(pr);
// `GitHubCommitPage` gets no category!
default -> Category.NONE;
}
}
โ Adding a new subtype causes no compile error! โ
Option 2 in Java 21
(without preview features):
public Category categorize(Page page) {
return switch (page) {
case GitHubIssuePage is -> categorizeIssue(is);
case GitHubPrPage pr -> categorizePr(pr);
// duplication ๐ข
case ErrorPage err -> Category.NONE;
case ExternalPage ext -> Category.NONE;
};
}
If GitHubCommitPage
is added:
public Category categorize(Page page) {
// error:
// "the switch expression does not cover
// all possible input values"
return switch (page) {
case GitHubIssuePage is -> categorizeIssue(is);
case GitHubPrPage pr -> categorizePr(pr);
case ErrorPage err -> Category.NONE;
case ExternalPage ext -> Category.NONE;
}
}
โ Adding a new subtype causes a compile error! โ
Would be nice to combine branches:
public Category categorize(Page page) {
return switch (page) {
case GitHubIssuePage is -> categorizeIssue(is);
case GitHubPrPage pr -> categorizePr(pr);
case ErrorPage err, ExternalPage ext
-> Category.NONE;
};
}
Doesn’t make sense.
(Neither err
nor ext
would be in scope.)
Use _
to combine "default branches":
public Category categorize(Page page) {
return switch (page) {
case GitHubIssuePage is -> categorizeIssue(is);
case GitHubPrPage pr -> categorizePr(pr);
case ErrorPage _, ExternalPage _
-> Category.NONE;
};
}
โ Default behavior without default
branch. ๐ฅณ
๐ JEP 456: Unnamed Variables & Patterns
Final in Java 25, 24, 23 |
Final in Java 22 |
Unnamed Patterns |
FFM API |
Preview in Java 25 |
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: Foreign Function & Memory API
๐ฅ Project Panama - Foreign Function & Memory API (Maurizio Cimadamore)
Final in Java 25, 24, 23, 22 |
Preview in Java 25 |
Primitive Patterns |
Structured Concurrency |
PEM Encoding |
In instanceof
and switch
, patterns can:
match against reference types
deconstruct records
nest patterns
ignore parts of a pattern
In switch
:
refine the selection with guarded patterns
That (plus sealed types) are
the pattern matching basics.
This will be:
built up with more features
built out to re-balance the language
The x instanceof Y
operation:
meant: "is x
of type Y
?"
now means: "does x
match the pattern Y
?"
For primitives:
old semantics made no sense
โ no x instanceof $primitive
new semantics can make sense
Example: int number = 0;
Can number
be an instance of byte
?
No, it’s an รฌnt
.
But can its value be a byte
?
Yes!
int x = 0;
if (x instanceof byte b)
IO.println(b + " in [-128, 127]");
else
IO.println(x + " not in [-128, 127]");
What’s special about 16_777_217?
Smallest positive int
that float
can’t represent.
int x = 16_777_216;
if (x instanceof float f)
// use `f`
Boolean bool = // ...
var emoji = switch (bool) {
case null -> "";
case true -> "โ
";
case false -> "โ";
}
(Bugged in 23; fixed in 23.0.1 and 24-EA.)
๐ JEP 455: Primitive Types in Patterns, instanceof, and switch
Final in Java 25, 24, 23, 22 |
Preview in Java 25 |
Primitive Patterns |
Structured Concurrency |
PEM Encoding |
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! ๐ณ
Why not?
// (fictional syntax!)
String query =
"SELECT * FROM Person p "
+ "WHERE p.\{property} = '\{value}'";
Also comes with free SQL injection! ๐ณ
SQL injections aren’t the only concern.
These also need validation and sanitization:
HTML/XML
JSON
YAML
…
All follow format-specific rules.
string templates were removed from JDK 23
(not even a preview)
the feature needs a redesign
timing is unknown
๐
Final in Java 25, 24, 23, 22 |
Preview in Java 25 |
Primitive Patterns |
Structured Concurrency |
PEM Encoding |
Common way to organize concurrency:
have long-running ExecutorService
instances
submit new tasks wherever necessary
enqueue follow-up computations wherever convenient
wait for results wherever needed
Concurrency is "all over the place".
Makes it more difficult to:
express/identify relationships between threads
organize overarching result/error handling
analyze control flow
debug issues that span multiple threads
We have seen this before with GOTOs.
Structured programming was the solution:
prescribes single entry point
and clearly defined exit points
influenced languages and runtimes
When the flow of execution splits into multiple concurrent flows, they rejoin in the same code block.
Term coined/refined by:
Martin Sustrik in Structured Concurrency
Nathaniel J. Smith in Notes on structured concurrency […]
// implicitly short-circuits on error
try (var scope = StructuredTaskScope.open()) {
// spawns new (virtual) threads
var subtaskA = scope.fork(this::taskA);
var subtaskB = scope.fork(this::taskB);
// waits explicitly for success
// (throws error if there was one)
scope.join();
return subtaskA.get() + subtaskB.get();
} catch (ExecutionException ex) {
// TODO: handle task errors
} // waits until all tasks/threads complete
Threads are short-lived:
start when task begins
end on completion
โ Establishes parent-child/sibling relationships
and logical grouping of tasks/threads.
Structured concurrency:
defines a scope for concurrency
simplifies control flow
simplifies grouped result/error handling
makes thread relationships visible
(in thread dumps and soon๐ค debuggers)
Use Joiner
to configure result/error handling:
how are results collected?
when are subtasks cancelled?
what does join
return?
when does join
throw?
Pass to StructuredTaskScope.open(Joiner)
.
Existing joiners for heterogeneous results:
awaitAllSuccessfulOrThrow()
:
cancels/throws on first error
default behavior of open()
awaitAll()
:
never cancels/throws
try (var scope = StructuredTaskScope
.open(Joiners.awaitAll())) {
var subtask = scope.fork(this::task);
// never throws:
scope.join();
switch (subtask.state()) {
case SUCCESS -> // ...
case FAILED -> // ...
case UNAVAILABLE -> // ...
}
} catch (ExecutionException ex) {
// TODO: handle task errors
}
Existing joiners for homogeneous results:
allSuccessfulOrThrow()
:
cancels/throws on first error
returns Stream<RESULT>
anySuccessfulResultOrThrow()
cancels/throws if all fail
returns RESULT
try (var scope = StructuredTaskScope.open(
Joiners.<String>anySuccessfulResultOrThrow())) {
// no need to grab the `Subtask` instances
scope.fork(this::taskA);
scope.fork(this::taskB);
// returns the first successful result (`String`)
return scope.join();
} catch (ExecutionException ex) {
// TODO: handle task errors
}
Implement and pass to StructuredTaskScope::open
:
interface Joiner<T, R> {
boolean onFork(Subtask<? extends T> subtask);
boolean onComplete(Subtask<? extends T> subtask);
R result() throws Throwable;
}
If joiner cancels scope:
all child threads get interrupted
join
returns or throws
But StructuredTaskScope.close()
waits
until all child threds complete.
โ Handle InterruptedException
properly!
Structured concurrency being scoped allows
inheriting scoped values to child threads.
ScopedValue.where(ANSWER, 42).run(() -> {// scope โโโ
try (var scope = StructuredTaskScope // โ
.open() { // โ
// child threads' scope โโโ โ
var subA = scope.fork(ANSWER::get); // โ โ
var subB = scope.fork(ANSWER::get); // โ โ
// โ โ
scope.join(); // โ โ
// โ โ
var result = subA.get()+subB.get(); // โ โ
IO.println(result); // "84" // โ โ
} // all child threads completed โโโ โ
IO.println(ANSWER.get()) // "42" // โ
} // โโโโโโ
๐ JEP 505: Structured Concurrency
Final in Java 25, 24, 23, 22 |
Preview in Java 25 |
Primitive Patterns |
Structured Concurrency |
PEM Encoding |
Representations of cryptographic objects
(keys, certificates, certificate revocation lists):
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj
0DAQcDQgAEi/kRGOL7wCPTN4KJ
2ppeSt5UYB6ucPjjuKDtFTXbgu
OIFDdZ65O/8HTUqS/sVzRF+dg7
H3/tkQ/36KdtuADbwQ==
-----END PUBLIC KEY-----
Was introduced for exchange via email
but moved way beyond:
development platforms (e.g. GitHub)
certificate authorities
cryptographic libraries (e.g. OpenSSL)
security-sensitive applications (e.g. OpenSSL)
hardware authentication devices (e.g. YubiKeys)
your applications
โ Very helpful if Java handled them easily.
New PEM API is straightforward to use:
X509Certificate cert = // ...
PEMEncoder pe = PEMEncoder.of();
String pem = pe.encodeToString(cert);
PEMDecoder pd = PEMDecoder.of();
DEREncodable cert2 = pd.decode(pem);
Instances of PEMEncoder
and PEMDecoder
are:
immutable
thread-safe
reusable
Can encrypt/decrypt with withEncryption
:
var pd = PEMDecoder.of().withEncryption(pass);
All implementations of DEREncodable
can be encoded:
AsymmetricKey
(DH, DSA, EC, RSA, etc.)
KeyPair
PKCS8EncodedKeySpec
X509EncodedKeySpec
X509Certificate
X509CRL
EncryptedPrivateKeyInfo
PEMRecord
For decoding:
if the object type is known, call
decode(String, Class<T extends DEREncodable>)
otherwise, switch
/instanceof
over
return value of decode(String)
(Overloads for InputStream
exist.)
Unknown cryptographic objects are decoded to PEMRecord
:
String type
(header text)
String content
(Base64-encoded body)
byte[] leadingData
(data preceding the header)
๐ JEP 470: PEM Encodings of Cryptographic Objects
๐ฅ Java 25 Encodes PEM
In a few slides…
Major achievements:
pave the on-ramp
add essential lower-level APIs
improve tooling and performance
introduce AOT workflow
Get JDK 25 at jdk.java.net/25.
all other images are copyrighted