public sealed interface Page
permits GitHubIssuePage, GitHubPrPage,
ExternalPage, ErrorPage {
// ...
}
this session covers Java 18-21
this is a showcase, not a tutorial
โ go to youtube.com/@java for more
slides at slides.nipafx.dev/java-x
(hit "?" to get navigation help)
ask questions any time
Pattern Matching |
Virtual Threads |
String Templates |
New & Updated APIs |
Performance & More |
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 observability and performance
(more on that later)
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.
Pattern Matching |
A New Dynamic Dispatch |
Data-Oriented Programming |
Virtual Threads |
String Templates |
New & Updated APIs |
Performance & More |
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 ๐ฅณ
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 issue
-> categorizeIssue(issue);
case GitHubPrPage pr
-> categorizePr(pr);
case ExternalPage external
-> categorizeExternal(external);
case ErrorPage error
-> categorizeError(error);
}
}
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!
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?
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 on pattern matching:
๐ฅ Java 21 Brings Full Pattern Matching (Sep 2023)
๐งโ๐ป GitHub crawler
Pattern Matching |
A New Dynamic Dispatch |
Data-Oriented Programming |
Virtual Threads |
String Templates |
New & Updated APIs |
Performance & More |
Use Java’s strong typing to model data as data:
use types to model data, particularly:
data as data with records
alternatives with sealed types
use (static) methods to model behavior, particularly:
exhaustive switch
without default
pattern matching to destructure polymorphic data
but it’s similar (data + functions)
first priority is data, not functions
use OOP to modularize large systems
use DOP to model small, data-focused (sub)systems
More on data-oriented programming:
๐ Data Oriented Programming in Java
(Brian Goetz on InfoQ)
๐ฅ Java 21 Brings Full Pattern Matching (Sep 2023)
๐งโ๐ป GitHub crawler
Pattern Matching |
Virtual Threads |
Unlimited Threads |
Under The Hood |
String Templates |
New & Updated APIs |
Performance & More |
Imagine a hypothetical HTTP request:
interpret request
query database (blocks)
process data for response
Resource utilization:
good for 1. and 3.
really bad for 2.
How to implement that request?
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
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
There’s a conflict between:
simplicity
throughput
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
Think of them like you
think about virtual memory.
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
This laptop:
Intel i7-1165G7 (11th Gen)
8GB for JVM (32 GB total RAM)
Gentoo Linux (kernel v6.5.10)
Extremely rough measurements:
#threads | 1k | 10k | 100k | 500k | 1m | 5m |
run time | 1.0s | 1.1s | 1.3s | 3s | 6s | 20s |
Virtual threads:
aren’t "faster threads"
remove "number of threads" as bottleneck
match app’s unit of concurrency to Java’s
Virtual threads increase throughput:
when workload is not CPU-bound
when number of concurrent tasks is high
โ simplicity && throughput
For servers:
request handling threads are started by web framework
frameworks will offer (easy) configuration options
We’re getting there.
Annotate request handling method on 3.?:
@GET
@Path("api")
@RunOnVirtualThread
public String handle() {
// ...
}
(Source)
Just works on 4.0 (currently RC1).
(Source)
๐ JEP 444
๐ฅ Java 21 new feature: Virtual Threads (Sep 2023)
Pattern Matching |
Virtual Threads |
Unlimited Threads |
Under The Hood |
String Templates |
New & Updated APIs |
Performance & More |
Virtual threads:
always work correctly
may not scale perfectly
Code changes can improve scalability
(and maintainability, debuggability, observability).
Only pool expensive resources
but virtual threads are cheap.
โ Replace thread pools (for concurrency),
with virtual threads plus, e.g., semaphores.
// 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);
}
// 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();
}
}
To understand virtual thread caveats
we need to understand how they work.
(Also, it’s very interesting.)
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
Remember the hypothetical request:
interpret request
query database (blocks)
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.
Virtual threads work correctly with everything:
all blocking operations
synchronized
Thread
, currentThread
, etc.
thread interruption
thread-locals
native code
But not all scale perfectly.
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
If possible:
avoid pinning operations
remove blocking operations
from pinning code sections.
// guarantees sequential access, but pins (for now) ๐๐พ
public synchronized String accessResource() {
return access();
}
// 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();
}
}
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).
// 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);
}
// 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));
}
Most importantly:
replace thread pools with semaphores
Also helpful:
remove long-running I/O from pinned sections
replace thread-locals with scoped values
replace synchronized
with locks
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
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)
What it is:
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
already disallowed by default
enable with java.security.manager=allow
in a future release, it will be removed
What you need to do:
observe your app with default settings
(โ security manager disallowed)
if used, move away from security manager
What it is:
new Integer(42)
new Double(42)
etc.
What you need to know:
Valhalla wants to turn them into value types
those have no identity
identity-based operations need to be removed
What you need to do:
Integer.valueOf(42)
Double.valueOf(42)
etc.
๐ all the aforementioned JEPs
Pattern Matching |
Virtual Threads |
String Templates |
New & Updated APIs |
Performance & More |
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.
[Preview in Java 21 — JEP 430]
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
*
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
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
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?
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!
// 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()}"
}
""";
String templates:
simplify string concatenation
enable domain-specific processing
incentivize the "right way"
Pattern Matching |
Virtual Threads |
String Templates |
New & Updated APIs |
Sequenced Collections |
Address Resolution SPI |
Misc. Improvements |
Performance & More |
Collections with order and indexed access:
List
Collections with order without indexed access:
SortedSet
(sort order)
Deque
(insertion order)
LinkedHashSet
(insertion order)
and more
New interfaces capture the concept of order:
SequencedCollection
SequencedSet
SequencedMap
Use as parameter or return type
and enjoy new methods.
Getting the first element:
list.get(0)
sortedSet.first()
deque.getFirst()
linkedHashSet.iterator().next()
Now for all:
sequencedCollection.getFirst()
Removing the last element:
list.remove(list.size() - 1)
sortedSet.remove(sortedSet.last())
deque.removeLast()
linkedHashSet.๐คท๐พโโ๏ธ()
Now for all:
sequencedCollection.removeLast()
Reversing order:
list.listIterator()
โ ListIterator
navigableSet.descendingSet()
โ NavigableSet
(view)
deque.descendingIterator()
โ Iterator
linkedHashSet.๐คท๐พโโ๏ธ()
Now for all:
sequencedCollection.reversed()
sequencedCollection.reversed()
returns
a SequencedCollection
view:
for (E element : list.reversed())
// ...
sortedSet
.reversed().stream()
//...
deque.reversed().toArray();
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"]
void addFirst(E);
void addLast(E);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
SequencedCollection<E> reversed();
(Analoguous for maps.)
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
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!
๐ JEP 431: Sequenced Collections
๐ฅ Java 21’s New (Sequenced) Collections (Mar 2023)
Pattern Matching |
Virtual Threads |
String Templates |
New & Updated APIs |
Sequenced Collections |
Address Resolution SPI |
Misc. Improvements |
Performance & More |
The JDK has a built-in resolver for host names:
relies on OS’s native resolver
typically uses hosts
file and DNS
Desirable improvements:
better interaction with virtual threads
support for alternative protocols
support for testing/mocking
(While being backwards compatible.)
Solution: Allow plugging in a custom resolver.
Two new types:
InetAddressResolverProvider
InetAddressResolver
Old resolver implements these,
and acts as default.
// registered as a service
public abstract class InetAddressResolverProvider {
InetAddressResolver get(Configuration config);
String name();
}
public interface InetAddressResolver {
Stream<InetAddress> lookupByName(
String host, LookupPolicy lookupPolicy);
String lookupByAddress(byte[] addr);
}
// in module declaration
provides InetAddressResolverProvider
with TransparentInetAddressResolverProvider;
// class
public class TransparentInetAddressResolverProvider
extends InetAddressResolverProvider {
@Override
public InetAddressResolver get(
Configuration config) {
return new TransparentInetAddressResolver(
config.builtinResolver());
}
}
public class TransparentInetAddressResolver
implements InetAddressResolver {
private InetAddressResolver builtinResolver;
public TransparentInetAddressResolver(
InetAddressResolver builtinResolver) {
this.builtinResolver = builtinResolver;
}
// ...
}
@Override
public Stream<InetAddress> lookupByName(
String host, LookupPolicy lookupPolicy) {
return builtinResolver
.lookupByName(host, lookupPolicy);
}
@Override
public String lookupByAddress(byte[] addr) {
return builtinResolver.lookupByAddress(addr);
}
Possible resolvers:
support for QUIC, TLS, HTTPS, etc.
redirect host names to local IPs for testing
more ideas?
๐ JEP 418
Pattern Matching |
Virtual Threads |
String Templates |
New & Updated APIs |
Sequenced Collections |
Address Resolution SPI |
Misc. Improvements |
Performance & More |
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);
๐ค
Right-sizing hashing data structures:
HashMap.newHashMap(int numMappings)
HashSet.newHashSet(int numElements)
LinkedHashMap.newLinkedHashMap(int numMappings)
LinkedHashSet.newLinkedHashSet(int numElements)
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
"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)
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
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)
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
}
locale | custom | local | both |
---|---|---|---|
en_US |
|
|
|
be_BE |
|
|
|
vi_VN |
|
|
|
Analogue methods were added to DateTimeFormatterBuilder
:
DateTimeFormatterBuilder appendLocalized(
String requestedTemplate);
static String getLocalizedDateTimePattern(
String requestedTemplate,
Chronology chrono, Locale locale)
These types now implemnet AutoCloseable
:
HttpClient
ExecutorService
ForkJoinPool
Additions to Future<T>
:
T resultNow()
Throwable exceptionNow
State state()
New:
enum State {
RUNNING, SUCCESS, FAILED, CANCELLED
}
There are many more additions like this.
Find a few more in
๐ฅ Java 21 API New Features (Sep 2023)
Pattern Matching |
Virtual Threads |
String Templates |
New & Updated APIs |
Performance & More |
Generational ZGC |
Class-Data Sharing |
Usability |
On-Ramp |
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.
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
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.)
๐ JEP 439: Generational ZGC
๐ฅ Generational ZGC and Beyond (Aug 2023)
๐ฅ Java’s Highly Scalable Low-Latency GC: ZGC (Mar 2023)
Pattern Matching |
Virtual Threads |
String Templates |
New & Updated APIs |
Performance & More |
Generational ZGC |
Class-Data Sharing |
Usability |
On-Ramp |
Use CDS to shave off 10-25% of your boot times.
Recent improvements:
default CDS archives โซ (JEP 341)
dynamic CDS archives โฌ (JEP 350)
auto-generated CDS โฒ (JDK-8261455)
And more to come from Project Leyden.
Pattern Matching |
Virtual Threads |
String Templates |
New & Updated APIs |
Performance & More |
Generational ZGC |
Class-Data Sharing |
Usability |
On-Ramp |
JFR view
command ใ (JDK-8306704)
JFR scrubbing โฒ (JDK-8271585)
code snippets in Javadoc โฑ (JEP 413)
UTF-8 by default โฑ (JEP 400)
simple web server โฑ (JEP 408)
Pattern Matching |
Virtual Threads |
String Templates |
New & Updated APIs |
Performance & More |
Generational ZGC |
Class-Data Sharing |
Usability |
On-Ramp |
Java 21 makes life easier
for new (Java) developers.
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!
To write and run a simple Java program, you need:
a JDK
an editor (IDE?)
javac
(build tool? IDE?)
java
(IDE?)
some Java code
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
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!
Java 9 added jshell
:
all you need:
tools: JDK, jshell
concepts: statements & arguments
but:
not great for beginners (IMO)
no progression
More is needed.
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.
Expand single-file execution in two directions:
simplify code: reduce required Java concepts
ease progression: run multiple files with java
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]
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]
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!
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
๐ JEP 445 for a simpler main
๐ JEP 458 for launching multiple source file
๐ฅ Script Java Easily in 21 and Beyond (May 2023)
random generator API diagrams:
Nicolai Parlog
(CC-BY-NC 4.0)