public class PageTreeFactory {
public static Page loadPageTree(/*...*/) {
// [...]
}
}
this session covers Java 18-21
it’s 3h but it still can’t cover everything!
go to youtube.com/@java for more
slides at slides.nipafx.dev/java-x
(hit "?" to get navigation help)
ask questions any time or tweet at @nipafx
Pattern Matching |
Virtual Threads |
String Templates |
New & Updated APIs |
Performance & Security |
On-Ramp |
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 & Security |
On-Ramp |
Features:
scrapes GitHub projects
creates Page
instances:
GitHubIssuePage
GitHubPrPage
ExternalPage
ErrorPage
further processes pages
The scraping is implemented in:
public class PageTreeFactory {
public static Page loadPageTree(/*...*/) {
// [...]
}
}
(Code on github.com/nipafx/loom-lab.)
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 ๐ซ
type checks ๐ฑ
public void categorize(Page page) {
if (page instanceof GitHubIssuePage) {
GitHubIssuePage issue = (GitHubIssuePage) page;
categorizeIssuePage(issue);
} else if (page instanceof GitHubPrPage) {
// ... etc. for all types
}
}
Ignore the ๐ฑ and let’s work on this.
[Finalized in Java 16 — JEP 394]
They combine:
type check
variable declaration
cast/assignment
โ Standardizes and eases a common pattern.
public void categorize(Page page) {
if (page instanceof GitHubIssuePage issue)
categorizeIssuePage(issue);
else if (page instanceof GitHubPrPage pr)
// ... etc. for all types
}
Generally, patterns consist of three parts:
a boolean check
variable declaration(s)
extraction(s)/assignment(s)
[Finalized in Java 16 — JEP 395]
Transparent carriers for immutable data.
compiler understands internals
couples API to internals
reduces verbosity a lot
Transparent carriers for immutable data.
record ExternalPage(URI url, String content) { }
ExternalPage
is final
private final fields: URI url
and String content
constructor: ExternalPage(URI url, String content)
accessors: URI url()
and String content()
equals()
, hashCode()
, toString()
that use the two fields
All method/constructor bodies can be customized.
[Finalized in Java 21 — JEP 440]
check whether variable is of correct type
declare one variable per component
assign component values to variables
if (page instanceof
ExternalPage(var url, var content)) {
// use `url` and `content`
}
โ Standardizes and eases a common pattern.
[Finalized in Java 21 — JEP 441]
public void categorize(Page page) {
switch (page) {
case GitHubIssuePage issue
-> categorizeIssuePage(issue);
case ExternalPage(var url, var content)
-> categorizeExternalUrl(url);
// ... etc. for all types
}
}
But:
error: the switch expression does not cover
all possible input values
Unlike an if
-else
-if
-chain,
a pattern switch
needs to be exhaustive:
public void categorize(Page page) {
switch (page) {
case GitHubIssuePage issue ->
categorizeIssuePage(issue);
// ... etc. for all types
default ->
throw new IllegalArgumentException();
}
}
That touches the ๐ฑ nerve.
[Finalized in Java 17 — JEP 409]
Sealed types limit inheritance,
by only allowing specific subtypes.
communicates intention to developers
allows compiler to check exhaustiveness
public sealed interface Page
permits GitHubIssuePage, GitHubPrPage,
ExternalPage, ErrorPage {
// ...
}
โ class MyPage implements Page
doesn’t compile
If all subtypes of a sealed types are covered,
the switch is exhaustive (without default
) โฆ
public void categorize(Page page) {
switch (page) {
case GitHubIssuePage issue -> // ...
case GitHubPrPage pr -> // ...
case ExternalPage external -> // ...
case ErrorPage error -> // ...
}
}
โฆ and the compiler is happy!
(But still watching.)
Why is switching over the type scary?
Because it may not be future proof!
But this one is!
Let’s add GitHubCommitPage implements Page
.
โ Follow the compile errors!
Starting point:
record GitHubCommitPage(/*โฆ*/) implements Page {
// ...
}
Compile error because supertype is sealed.
โ Go to the sealed supertype.
Next stop: the sealed supertype
โ Permit the new subtype!
public sealed interface Page
permits GitHubIssuePage, GitHubPrPage,
GitHubCommitPage,
ExternalPage, ErrorPage {
// ...
}
Next stop: all switches that are no longer exhaustive.
public void categorize(Page page) {
switch (page) {
case GitHubIssuePage issue -> // ...
case GitHubPrPage pr -> // ...
case ExternalPage external -> // ...
case ErrorPage error -> // ...
// missing case
}
}
Bingo!
(But only works without default branch.)
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
Without methods becoming part of the API.
Via pattern matching (new):
makes "sealed" part of type
straight-forward
Design patterns make up gaps in the language.
Good example is the strategy pattern:
used to be "a thing" in Java
you use it everytime you pass a lambda
But do you still think of it a design pattern?
(I don’t.)
Pattern matching does the same for the visitor pattern.
Applications for records, switch
, and pattern matching:
ad-hoc data structures
complex return types
complex domains
Often local, throw-away types used in one class or package:
record PageWithLinks(Page page, Set<URI> links) {
PageWithLinks {
requireNonNull(page);
requireNonNull(links);
links = new HashSet<>(links);
}
}
Return values that are deconstructed immediately:
Match<User> findUser(String userName) { ... }
// types
sealed interface Match<T> { }
record None<T>() implements Match<T> { }
record Exact<T>(T entity) implements Match<T> { }
record Fuzzies<T>(Collection<T> entities)
implements Match<T> { }
Return values that are deconstructed immediately:
// calling the method
switch (findUser("John Doe")) {
case None<> none -> // ...
case Exact<> exact -> // ...
case Fuzzies<> fuzzies -> // ...
}
Long-living objects that are part
of the program’s domain.
For example Page
.
Pattern matching will probably see
further improvements, e.g.:
unnamed patterns (JEP 443, preview in Java 21)
primitive types in patterns (JEP 455)
deconstruction on assignment (no JEP, but it’s coming)
with
expression
(design document from Aug 2020)
[Preview in Java 21 — JEP 443]
Use _
to ignore components:
public void categorize(Page page) {
switch (page) {
case GitHubIssuePage(_, _, int issue, _) -> // ...
case GitHubPrPage(_, _, int pr, _) -> // ...
case ExternalPage(var url, _) -> // ...
case ErrorPage(var url, _) -> // ...
}
}
โ Focus on what’s essential.
Use _
to define default behavior:
public void categorizeGitHub(Page page) {
switch (page) {
case GitHubIssuePage(_, _, int issue, _) -> // ...
case GitHubPrPage(_, _, int pr, _) -> // ...
case ErrorPage _, ExternalPage _ -> { };
};
}
โ Default behavior without default
branch.
When keeping functionality separate from types:
seal the supertype
switch over sealed types
enumerate all subtypes
avoid default branches!
More on pattern matching:
๐ฅ Java21 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 & Security |
On-Ramp |
Paradigms often come with an
"Everything is a …" sentence.
The goal of any programming paradigm is to manage complexity.
complexity comes in many forms
not all paradigms handle all forms equally well
โ "It depends"
Everything is an object
combines state and behavior
hinges on encapsulation
polymorphism through inheritance
Works best when defining/upholding boundaries.
Great use cases for OOP:
boundaries between libraries and clients
in large programs to enable modular reasoning
Consider a data-oriented approach for:
smaller (sub)systems
focused on data
Guiding principles:
model the data, the whole data,
and nothing but the data
data is immutable
validate at the boundary
make illegal states unrepresentable
From Brian Goetz' seminal article:
Data Oriented Programming in Java
The app we just looked at:
is small
focusses on data (Page
)
separates operations from types
โ Perfect for data-oriented programming!
Model the data, the whole data,
and nothing but the data.
There are four kinds of pages:
error page
external page
GitHub issue page
GitHub PR page
โ Use four records to model them!
public record ErrorPage(
URI url, Exception ex) { }
public record ExternalPage(
URI url, String content) { }
public record GitHubIssuePage(
URI url, String content,
int issueNumber, Set<Page> links) { }
public record GitHubPrPage(
URI url, String content,
int prNumber, Set<Page> links) { }
Model the data, the whole data,
and nothing but the data.
There are additional relations between them:
a page (load) is either successful or not
a successful page is either external or GitHub
a GitHub page is either for a PR or an issue
โ Use sealed types to model the alternatives!
public sealed interface Page
permits ErrorPage, SuccessfulPage {
URI url();
}
public sealed interface SuccessfulPage
extends Page permits ExternalPage, GitHubPage {
String content();
}
public sealed interface GitHubPage
extends SuccessfulPage
permits GitHubIssuePage, GitHubPrPage {
Set<Page> links();
default Stream<Page> subtree() { ... }
}
records are product types
sealed types are sum types
This simple combination of mechanisms — aggregation and choice — is deceptively powerful
Make illegal states unrepresentable.
Many are already, e.g.:
with error
and with content
with issueNumber
and prNumber
with isseNumber
or prNumber
but no links
Validate at the boundary.
โ Reject other illegal states in constructors.
record ExternalPage(URI url, String content) {
// compact constructor
ExternalPage {
Objects.requireNonNull(url);
Objects.requireNonNull(content);
if (content.isBlank())
throw new IllegalArgumentException();
}
}
Data is immutable.
Records are shallowly immutable,
but field types may not be.
โ Fix that during construction.
// compact constructor
GitHubPrPage {
// [...]
links = Set.copyOf(links);
}
page "type" is explicit in Java’s type
only legal combination of data are possible
API is self-documenting
code is trivial to test
But where did the operations go?
Model the data, the whole data,
and nothing but the data.
โ Methods should be limited to derived quantities.
public record GitHubIssuePage(
URI url, String content,
int issueNumber, Set<Page> links) {
public String toPrettyString() {
return "๐ ISSUE #" + issueNumber;
}
}
Other operations must be defined elsewhere:
methods in other subsystems
use pattern matching over sealed types
for polymorphic operations
avoid default branch
use record patterns to access data
โ This is just pattern matching.
If toPrettyString
were defined outside of Page
:
private static String toPrettyString(Page page) {
return switch (page) {
case ErrorPage(var url, _)
-> "๐ฅ ERROR: " + url.getHost();
case ExternalPage(var url, _)
-> "๐ค EXTERNAL: " + url.getHost();
case GitHubIssuePage(_, _, int issueNumber, _)
-> "๐ ISSUE #" + issueNumber;
case GitHubPrPage(_, _, int prNumber, _)
-> "๐ PR #" + prNumber;
};
}
immutable data structures
methods (functions?) that operate on them
Isn’t this just functional programming?!
Kind of.
Functional programming:
Everything is a function
โ Focus on creating and composing functions.
Data-oriented programming:
Model data as data.
โ Focus on correctly modeling the data.
OOP is not dead (again):
valuable for complex entities or rich libraries
use whenever encapsulation is needed
still a good default on high level
DOP — consider when:
mainly handling outside data
working with simple or ad-hoc data
data and behavior should be separated
Use Java’s strong typing to model data as data:
use classes to represent data, particularly:
data as data with records
alternatives with sealed classes
use methods (separately) to model behavior, particularly:
exhaustive switch
without default
pattern matching to destructure polymorphic data
model the data, the whole data,
and nothing but the data
data is immutable
validate at the boundary
make illegal states unrepresentable
More on data-oriented programming:
๐ Data Oriented Programming in Java
(Brian Goetz on InfoQ)
๐ฅ Java21 Brings Full Pattern Matching (Sep 2023)
๐งโ๐ป GitHub crawler
Pattern Matching |
Virtual Threads |
Unlimited Threads |
Under The Hood |
String Templates |
New & Updated APIs |
Performance & Security |
On-Ramp |
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
There are other conflicts:
design vs performance (โ Valhalla)
explicitness vs succinctness (โ Amber)
flexibility vs safety (โ Panama)
optimization vs specification (โ Leyden)
A virtual thread:
is a regular Thread
low memory footprint ([k]bytes)
small switching cost
scheduled by the Java runtime
requires no OS thread when waiting
Virtual memory:
maps large virtual address space
to limited physical memory
gives illusion of plentiful memory
Virtual threads:
map large number of virtual threads
to a small number of OS threads
give the illusion of plentiful threads
Programs rarely care about virtual vs physical memory.
Programs need rarely care about virtual vs platform thread.
Instead:
write straightforward (blocking) code
runtime shares available OS threads
reduces the cost of blocking to near zero
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
Virtual threads:
remove "number of threads" as bottleneck
match app’s unit of concurrency to Java’s
โ simplicity && throughput
Virtual threads aren’t "faster threads":
same number of CPU cycles
each task takes the same time (same latency)
So why bother?
Parallelism | Concurrency | |
---|---|---|
Task origin | solution | problem |
Control | developer | environment |
Resource use | coordinated | competitive |
Metric | latency | throughput |
Abstraction | CPU cores | tasks |
# of threads | # of cores | # of tasks |
When workload is not CPU-bound:
start waiting as early as possible
for as many tasks as possible
โ Virtual threads increase throughput:
when workload is not CPU-bound
when number of concurrent tasks is high
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)
Go forth and multiply (your threads)
Pattern Matching |
Virtual Threads |
Unlimited Threads |
Under The Hood |
String Templates |
New & Updated APIs |
Performance & Security |
On-Ramp |
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 & Security |
On-Ramp |
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 & Security |
On-Ramp |
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 & Security |
On-Ramp |
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 & Security |
On-Ramp |
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 & Security |
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 & Security |
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
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
Don’t miss Brian Goetz' keynote: Wed, 10:20!
๐ JEP 445 for a simpler main
๐ JEP draft 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)