module java.sql {
requires transitive java.logging
requires transitive java.xml
uses java.sql.Driver
exports java.sql
exports javax.sql
exports javax.transaction.xa
}
this talk covers Java 19
and whatever else we have time for
this is a showcase, not a tutorial
slides at slides.nipafx.dev/java-x
(hit "?" to get navigation help)
they cover Java 9 to 19+
(without module system)
Module System |
Language Changes |
New and Updated APIs |
New JVM Features |
Performance Improvements |
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)
worse 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 each release (including EA)
If you’re on…
Java 8: upgrade ASAP to 11
Java 11: prepare upgrade to 17
Java 17: consider upgrading to 19
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.
JARs have:
no name the JVM cares about
no explicit dependencies
no well-defined API
no concept of versions
Some consequences:
NoClassDefFoundError
no encapsulation across JARs
version conflicts
Modules are like JARs but have:
proper names
explicit dependencies
a well-defined API
no concept of versions 😭
Important goals:
reliable configuration
strong encapsulation
A file module-info.java
:
module java.sql {
requires transitive java.logging
requires transitive java.xml
uses java.sql.Driver
exports java.sql
exports javax.sql
exports javax.transaction.xa
}
module java.sql {
requires transitive java.logging
requires transitive java.xml
}
Module system enforces:
all required modules are present
no ambiguity
no static dependency cycles
no split packages
module java.sql {
exports java.sql
exports javax.sql
exports javax.transaction.xa
}
Say you want to access java.sql.ResultSet
.
Module system only grants access if:
ResultSet
is public
java.sql
is exported by java.sql
your module reads java.sql
The module system as a service registry:
module java.sql {
uses java.sql.Driver
}
module mysql.driver {
provides java.sql.Driver
with com.mysql.MySQLDriver;
}
Code in java.sql can do this:
List<Driver> drivers = new ArrayList<>();
ServiceLoader
.load(Driver.class)
.forEach(drivers::add);
Create your own runtime images:
with the JDK modules your app needs
with all modules (JDK, app, deps)
Let’s look at the latter!
# create image
$ jlink
--output awesome-app
--module-path $JAVA_HOME/jmods:mods
--add-modules com.awesome.app
--launcher awesome=com.awesome.app
# [ship image]
# use image
awesome-app/bin/awesome
automatic service binding
(with --bind-services
)
various optimizations
(size and launch performance)
plugin API (not yet public)
cross OS image generation
finer grained dependencies and exports
open packages and modules (for reflection)
unnamed and automatic modules (for migration)
layers (for containers)
I’ve written a book!
⇝ bit.ly/the-jms
creating, building,
running modules
migration to Java 9+
modularization
services and
advanced features
reflection and layers
custom runtime images
Pattern Matching Things... |
More Cool Things... |
All The Small Things... |
Pattern Matching in Switch |
Sealed Classes ⑰ |
Type Pattern Matching ⑯ |
Switch Expressions ⑭ |
More Cool Things... |
All The Small Things... |
Standing on three pillars.
Note:
This is a preview language feature!
must be enabled with --enable-preview
(on javac
and java
).
in IntelliJ, set the module’s language level to
19 (Preview) - …
in Eclipse, go to Compiler Settings
and check Enable preview features
Patterns and switch
evolution
are coming together:
// tired
if (staff instanceof Employee employee)
employee.paySalary();
else if (staff instanceof Freelancer freelancer)
freelancer.payBill();
// wired
switch (staff) {
case Employee emp -> emp.paySalary();
case Freelancer free -> free.payBill();
default -> { }
}
When using patterns in switch:
switched-over variable becomes target
pattern goes after case
// target
switch (staff) {
// |- pattern -|
case Employee emp -> emp.paySalary();
// |- pattern ---|
case Freelancer free -> free.payBill();
default -> { }
}
Things to discuss:
null
handling
when
clauses
completeness
Explicit with null
label:
switch (staff) {
case Employee emp -> emp.paySalary();
case Freelancer free -> free.payBill();
case null -> { }
default -> { }
}
Default branch does not handle null
but branches can be merged:
switch (staff) {
case Employee emp -> emp.paySalary();
case Freelancer free -> free.payBill();
case null, default -> { }
}
String result = switch (object) {
case String s when s.length() > 1 -> "string";
case String s -> "character";
default -> "";
}
Like switch expressions, pattern switches need to be complete:
// compile error
switch (staff) {
case Employee emp -> emp.paySalary();
case Freelancer free -> free.payBill();
}
Or is it?!
Third pillar comes into play:
sealed interface Staff
permits Employee, Freelancer { }
switch (staff) {
case Employee emp -> emp.paySalary();
case Freelancer free -> free.payBill();
// no default branch needed!
}
This is big!
Best way to add functionality:
as methods to interfaces / classes 👍🏾
visitor pattern 😕
Third option: patterns over sealed classes!
leaves types untouched
simple to branch based
on types and conditions
compile errors for new types
Pattens also help on boundary.
Consider parsed JSON:
mostly subtypes of JsonNode
if that’s sealed, switch over:
StringJsonNode
ArrayJsonNode
etc.
merges pattern matching, switch evolution
and sealed types
makes it easy and safe to branch
based on types and conditions
improves null
handling
Pattern Matching in Switch |
Sealed Classes ⑰ |
Type Pattern Matching ⑯ |
Switch Expressions ⑭ |
More Cool Things... |
All The Small Things... |
Between final
and open classes
Many systems have central abstractions, e.g.:
staff/customers
delivery mechanisms
shapes
Commonly, polymorphism is used
to reuse code and attach functionality.
If many subsystems operate on abstractions,
there’s the risk of feature creep.
Alternatively, subsystems can
implement their own handling.
Challenge is that subtypes
are effectively unknown, e.g.:
what subtypes of Staff
exist?
what subtypes of Shape
exist?
OO-solutions are cumbersome.
(e.g. visitor pattern)
In many cases, a type’s variations
are finite and known, e.g.:
Employee
, Freelancer
extend Staff
Circle
, Rectangle
extend Shape
If subsystems rely on that,
their code becomes simpler (instanceof
).
But less maintainable?
⇝ Only because compiler can’t help!
There’s three options how a class can be extended:
by no classes (final
class)
by package classes
(package-visible constructor)
by all classes (public
class)
(For interfaces, there’s no choice at all.)
In all cases:
Implementations are unknown to the compiler.
With sealed
types, we can express
limited extensibility:
only specific types can extend sealed type
those are known to developer and compiler
mark class/interface as sealed
use permits
to list types
sealed interface Staff
permits Employee, Freelancer { }
final class Employee implements Staff { }
final class Freelancer implements Staff { }
// compile error
final class Consultant implements Staff { }
Goal is to combine sealed types,
switch expressions, and type patterns.
But we’re not there yet - for now:
sealed classes limit extensibility
(between final
and non-final
)
prevent extension by users
express intention to maintainers
There are a few details to discuss:
for the sealed type
for the permitted types
for both of those types
Sealed types can extend/inherit as usual:
sealed class Staff
extends Person
implements Comparable<Staff>
permits Employee, Freelancer {
// ...
}
Permitted types must use exactly one of these modifiers:
final
for no inheritance
sealed
for limited inheritance
non-sealed
for unlimited inheritance
With sealed
and non-sealed
, a type
can admit further implementations.
sealed interface Staff
permits Employee, Freelancer { }
non-sealed class Employee implements Staff { }
sealed class Freelancer implements Staff
permits Consultant { }
final class Consultant extends Freelancer { }
But what about completeness?!
⇝ type pyramid has "complete peak"
Permitted types must directly extend sealed type:
sealed interface Staff
// compile error
permits Freelancer, Consultant { }
non-sealed class Freelancer implements Staff { }
class Consultant extends Freelancer { }
This keeps type pyramid layered.
Remember, records are implicitly final
.
They make good permitted types.
Permitted types must be "close":
same package for non-modular JAR
same module for modular JAR
Sealed and each permitted type must be
visible/accesible to one another.
If all types are in same source file,
permits
can be omitted:
public class Employment {
sealed interface Staff { }
final class Employee implements Staff { }
final class Freelancer implements Staff { }
}
Sealed types make inheritance:
more flexible between open and final
analyzable to the compiler
Limited inheritance is
one pillar for pattern matching.
Pattern Matching in Switch |
Sealed Classes ⑰ |
Type Pattern Matching ⑯ |
Switch Expressions ⑭ |
More Cool Things... |
All The Small Things... |
Type check and cast in one operation.
instanceof
instanceof
is cumbersome:
public void pay(Staff staff) {
if (staff instanceof Employee)
((Employee) staff).paySalary();
else if (staff instanceof Freelancer)
((Freelancer) staff).payBill();
}
Three things are happening:
type test
type conversaion
variable declaration (implicit)
public void pay(Staff staff) {
if (staff instanceof Employee employee)
employee.paySalary();
else if (staff instanceof Freelancer freelancer)
freelancer.payBill();
}
staff instanceof Employee employee
:
does all three things in one operation
employee
is scoped to true
-branch
A pattern is:
a test/predicate
that is applied to a target
pattern variables
that are extracted from the target
if the test passes
// |--------- pattern --------|
// target |----- test ------| variable
staff instanceof Employee employee
We will see more patterns in the future.
Pattern variable is in scope
where compiler can prove pattern is true
:
public void inverted(Object object) {
if (!(object instanceof String string))
throw new IllegalArgumentException();
// after inverted test
System.out.println(string.length());
}
public void scoped(Object object) {
// later in same expression
if (object instanceof String string
&& string.length() > 50)
System.out.println("Long string");
if (object instanceof String string
// compiler error because || means
// it's not necessarily a string
|| string.length() > 50)
System.out.println("Maybe string");
}
Just like instanceof
,
type patterns reject null
:
public void nullChecked(Object object) {
if (object instanceof String string)
// never NPEs because `string` is not null
System.out.println(string.length());
}
Upcasting makes little sense,
so it’s considered an implementation error:
public void upcast(String string) {
// compile error
if (string instanceof CharSequence sequence)
System.out.println("Duh");
}
General recommendation:
Consider classic OOP design
before type patterns.
public void pay(Staff staff) {
if (staff instanceof Employee employee)
employee.paySalary();
else if (staff instanceof Freelancer freelancer)
freelancer.payBill();
}
public void pay(Staff staff) {
// method on interface `Staff`
staff.pay();
}
But that doesn’t always work best:
handling primitives
no control over types
OOP solutions can be cumbersome
(visitor pattern)
Another really neat application:
@Override
public final boolean equals(Object o) {
return o instanceof Type other
&& someField.equals(other.someField)
&& anotherField.equals(other.anotherField);
}
More use cases in later versions.
$TARGET instanceof $TYPE $VAR
:
checks whether $TARGET
is of type $TYPE
creates variable $TYPE $VAR = $TARGET
in scope wherever instanceof $TYPE
is true
first of many patterns
don’t overuse it - polymorphism still exists
one pillar of full pattern matching support
Pattern Matching in Switch |
Sealed Classes ⑰ |
Type Pattern Matching ⑯ |
Switch Expressions ⑭ |
More Cool Things... |
All The Small Things... |
More powerful switch
.
Say you’re facing the dreaded ternary Boolean …
public enum TernaryBoolean {
TRUE,
FALSE,
FILE_NOT_FOUND
}
... and want to convert it to a regular Boolean
.
Before Java 14, you might have done this:
boolean result;
switch (ternaryBool) {
case TRUE: result = true; break;
case FALSE: result = false; break;
case FILE_NOT_FOUND:
var ex = new UncheckedIOException(
"This is ridiculous!",
new FileNotFoundException());
throw ex;
default:
var ex2 = new IllegalArgumentException(
"Seriously?! 😠");
throw ex2;
}
Lots of room for improvements:
default fall-through is annoying
result
handling is roundabout
lacking compiler support is error-prone
This is better:
public boolean convert(TernaryBoolean ternaryBool) {
switch (ternaryBool) {
case TRUE: return true;
case FALSE: return false;
case FILE_NOT_FOUND:
throw new UncheckedIOException(
"This is ridiculous!",
new FileNotFoundException());
default:
throw new IllegalArgumentException(
"Seriously?! 😠");
}
}
Better:
return
prevents fall-through
results are created on the spot
But:
default
is not really necessary…
…but prevents compile error
on missing branches
creating a method is not always
possible or convenient
Enter switch
expressions:
boolean result = switch(ternaryBool) {
case TRUE -> true;
case FALSE -> false;
case FILE_NOT_FOUND ->
throw new UncheckedIOException(
"This is ridiculous!",
new FileNotFoundException());
};
Two things to note:
switch
"has a result"
⇝ it’s an expression, not a statement
lambda-style arrow syntax
Statement:
if (condition)
result = doThis();
else
result = doThat();
Expression:
result = condition
? doThis()
: doThat();
Statement:
imperative construct
guides computation, but has no result
Expression:
is computed to a result
For switch
:
if used with an assignment,
switch
becomes an expression
if used "stand-alone", it’s
treated as a statement
This results in different behavior
(more on that later).
You can use :
and ->
with
expressions and statements, e.g.:
boolean result = switch(ternaryBool) {
case TRUE: yield true;
case FALSE: yield false;
case FILE_NOT_FOUND:
throw new UncheckedIOException(
"This is ridiculous!",
new FileNotFoundException());
};
switch
is used as an expression
yield result
returns result
Whether you use arrow or colon
results in different behavior
(more on that later).
general improvements
multiple case labels
specifics of arrow form
no fall-through
statement blocks
specifics of expressions
poly expression
returning early
completeness
Statements and expressions,
in colon and arrow form
can use multiple case labels:
String result = switch (ternaryBool) {
case TRUE, FALSE -> "sane";
// `default, case FILE_NOT_FOUND -> ...`
// does not work (neither does other way
// around), but that makes sense because
// using only `default` suffices
default -> "insane";
};
Whether used as statement or expression,
the arrow form has no fall-through:
switch (ternaryBool) {
case TRUE, FALSE ->
System.out.println("Bool was sane");
// in colon-form, if `ternaryBool` is `TRUE`
// or `FALSE`, we would see both messages;
// in arrow-form, only one branch is executed
default ->
System.out.println("Bool was insane");
}
Whether used as statement or expression,
the arrow form can use statement blocks:
boolean result = switch (ternaryBoolean) {
case TRUE -> {
System.out.println("Bool true");
yield true;
}
case FALSE -> {
System.out.println("Bool false");
yield false;
}
// cases `FILE_NOT_FOUND` and `default`
};
Natural way to create scope:
boolean result = switch (ternaryBoolean) {
// cases `TRUE` and `FALSE`
case FILE_NOT_FOUND -> {
var ex = new UncheckedIOException(
"This is ridiculous!",
new FileNotFoundException());
throw ex;
}
default -> {
var ex = new IllegalArgumentException(
"Seriously?! 🤬");
throw ex;
}
};
A poly expression
has no definitive type
can be one of several types
Lambdas are poly expressions:
Function<String, String> fun = s -> s + " ";
UnaryOperator<String> op = s -> s + " ";
Whether in colon or arrow form,
a switch
expression is a poly expression.
How it’s type is determined,
depends on the target type:
// target type known: String
String result = switch (ternaryBool) { ... }
// target type unknown
var result = switch (ternaryBool) { ... }
If target type is known, all branches must conform to it:
String result = switch (ternaryBool) {
case TRUE, FALSE -> "sane";
default -> "insane";
};
If target type is unknown, the compiler infers a type:
// compiler infers super type of `String` and
// `IllegalArgumentException` ~> `Serializable`
var serializableMessage = switch (bool) {
case TRUE, FALSE -> "sane";
default -> new IllegalArgumentException("insane");
};
Whether in colon or arrow form,
you can’t return early from a switch
expression:
public String sanity(Bool ternaryBool) {
String result = switch (ternaryBool) {
// compile error:
// "return outside
// of enclosing switch expression"
case TRUE, FALSE -> { return "sane"; }
default -> { return "This is ridiculous!"; }
};
}
Whether in colon or arrow form,
a switch
expression checks completeness:
// compile error:
// "the switch expression does not cover
// all possible input values"
boolean result = switch (ternaryBool) {
case TRUE -> true;
// no case for `FALSE`
case FILE_NOT_FOUND ->
throw new UncheckedIOException(
"This is ridiculous!",
new FileNotFoundException());
};
No compile error for missing default
:
// compiles without `default` branch because
// all cases for `ternaryBool` are covered
boolean result = switch (ternaryBool) {
case TRUE -> true;
case FALSE -> false;
case FILE_NOT_FOUND ->
throw new UncheckedIOException(
"This is ridiculous!",
new FileNotFoundException());
};
Compiler adds in default
branch.
switch
Definitive Guide To Switch Expressions
More powerful and usable switch is
one pillar of pattern matching.
Pattern Matching Things... |
Records ⑯ |
Text Blocks ⑮ |
Local-Variable Type Inference ⑩ |
All The Small Things... |
Simple classes ~> simple code
Typical Java Bean:
public class Range {
// part I 😀
private final int low;
private final int high;
public Range(int low, int high) {
this.low = low;
this.high = high;
}
}
public class Range {
// part II 🙄
public int getLow() {
return low;
}
public int getHigh() {
return high;
}
}
public class Range {
// part III 🤨
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Range range = (Range) o;
return low == range.low &&
high == range.high;
}
}
public class Range {
// part IV 🥴
@Override
public int hashCode() {
return Objects.hash(low, high);
}
}
public class Range {
// part V 😭
@Override
public String toString() {
return "[" + low + "; " + high + "]";
}
}
Range.java
is simple:
declares type Range
declares two components, low
and high
Takes 44 lines!
verbose
room for error
unexpressive
// these are "components"
public record Range(int low, int high) {
// compiler generates:
// * (final) fields
// * canonical constructor
// * accessors low(), high()
// * equals, hashCode, toString
}
The API for a record models the state, the whole state, and nothing but the state.
The deal:
give up encapsulation
couple API to internal state
get API for free
The benefits:
no boilerplate for plain "data carriers"
no room for error
makes Java more expressive
On to the details!
Records are limited classes:
no inheritance
can’t use extends
are final
component fields are final
no additional fields
Records can be customized:
override constructor
add constructors and
static factory methods
override accessors
add other methods
override Object
methods
implement interfaces
make serializable
public Range(int low, int high) {
if (high < low)
throw new IllegalArgumentException();
this.low = low;
this.high = high;
}
Compact canonical constructor:
// executed before fields are assigned
public Range {
if (high < low)
throw new IllegalArgumentException();
}
// arguments can be reassigned
public Range {
if (high < low)
high = low;
}
implicit constructor has same visibility as record
explicit constructors can’t reduce visibility
can’t assign fields in compact form
(happens automatically after its execution)
Additional constructors work as usual:
public Range(int high) {
this(0, high);
}
(Compact canonical constructor can’t delegate.)
Additional static factories work as usual:
public static Range open(int low, int high) {
return new Range(low, high + 1);
}
Can’t reduce constructor visibility, though.
@Deprecated
// use static factory method instead
public Range { }
Accessors can be overridden:
@Override
public low() {
return Math.max(0, low);
}
Not a good example!
The API for a record models the state, the whole state, and nothing but the state.
public record Range(int low, int high)
implements Comparable<Range> {
@Override
public int compareTo(Range other) {
return this.low == other.low
? this.high - other.high
: this.low - other.low;
}
}
public record Range(int low, int high)
implements Serializable { }
has default serialVersionUID
0
uses FileOutputStream
and
FileInputStream
as usual
deserializaton calls constructor 🙌
framework support is growing
(e.g. Jackson, Apache Johnzon)
use records to replace data carriers
it’s not anti-boilerplate pixie dust
⇝ use only when "the deal" makes sense
beware of limitations
beware of class-building facilites
observe ecosystem for adoption
Pattern Matching Things... |
Records ⑯ |
Text Blocks ⑮ |
Local-Variable Type Inference ⑩ |
All The Small Things... |
Multiline strings. Finally.
Text blocks are straightforward:
String haikuBlock = """
worker bees can leave
even drones can fly away
the queen is their slave""";
System.out.println(haiku);
// > worker bees can leave
// > even drones can fly away
// > the queen is their slave
line breaks are normalized to \n
intentional indentation remains
accidental indentation is removed
can be used in same place
as "string literals"
start with """
and new line
end with """
on the last line of content
on its own line
Position of closing """
decides
whether string ends with "\n"
.
Compare to:
String haikuLiteral = ""
+ "worker bees can leave\n"
+ " even drones can fly away\n"
+ " the queen is their slave";
haikuBlock.equals(haikuLiteral)
thanks to string interning even
haikuBlock == haikuLiteral
⇝ No way to discern source at run time!
Line ending depends on configuration.
Source file properties influence semantics?
Text block lines always end with \n
!
Escape sequences are translated afterwards:
String windows = """
Windows\r
line\r
endings\r
"""
Compiler discerns:
accidental indentation
(from code style; gets removed)
essential indentation
(within the string; remains)
How?
closing """
are on their own line
⇝ their indentation is accidental
otherwise, line with smallest indentation
⇝ its indentation is accidental
Accidental vs intentional indentation
(separated with |
):
String haikuBlock = """
|worker bees can leave
| even drones can fly away
| the queen is their slave""";
String haikuBlock = """
| worker bees can leave
| even drones can fly away
| the queen is their slave
""";
To manually manage indentation:
String::stripIndent
String::indent
Text blocks are not raw:
escape sequences work (e.g. \r
)
escape sequences are necessary
But: "
is not special!
String phrase = """
{
greeting: "hello",
audience: "text blocks",
}
""";
⇝ Way fewer escapes in HTML/JSON/SQL/etc.
Pattern Matching Things... |
Records ⑯ |
Text Blocks ⑮ |
Local-Variable Type Inference ⑩ |
All The Small Things... |
Type inference with var
.
Less typing, but still strongly typed.
We’re used to duplicating
type information:
URL nipafx = new URL("https://nipafx.dev");
URLConnection connection = nipafx.openConnection();
Reader reader = new BufferedReader(
new InputStreamReader(
connection.getInputStream()));
Not so bad?
What about this?
No no = new No();
AmountIncrease<BigDecimal> more =
new BigDecimalAmountIncrease();
HorizontalConnection<LinePosition, LinePosition>
jumping =
new HorizontalLinePositionConnection();
Variable variable = new Constant(5);
List<String> names = List.of("Max", "Maria");
Can’t somebody else do that?
Compiler knows the types!
Enter var
:
var nipafx = new URL("https://nipafx.dev");
var connection = nipafx.openConnection();
var reader = new BufferedReader(
new InputStreamReader(
connection.getInputStream()));
How much information is used for inference?
type inference can be
arbitrarily complex/powerful
critical resource is not
compiler but developer
code should be readable
(without compiler/IDE)
⇝ Better to keep it simple!
// inferred as `int`
var id = 123;
if (id < 100) {
// very long branch
} else {
// oh boy, much more code...
}
// now we add this line:
id = "124";
What type should id
be?
Where does the error show up?
var
Hence, var
only works in limited scopes:
compiler infers type from right-hand side
⇝ rhs has to exist and define a type
only works for local variables, for
, try
⇝ no var
on fields or in method signatures
also on lambda parameters ⑪
⇝ annotate inferred type on lambda parameters
var
Two more:
not a keyword, but a reserved type name
⇝ variables/fields can be named var
compiler writes type into bytecode
⇝ no run-time component
This is about readability!
less redundancy
more intermediate variables
more focus on variable names
aligned variable names
var no = new No();
var more = new BigDecimalAmountIncrease();
var jumping = new HorizontalLinePositionConnection();
var variable = new Constant(5);
var names = List.of("Max", "Maria");
Still think omitting types is always bad?
Ever wrote a lambda without declaring types?
rhetoricalQuestion.answer(yes -> "see my point?");
Principles from the official style guidelines:
Reading code is more important than writing it.
Code should be clear from local reasoning.
Code readability shouldn’t depend on IDEs.
Explicit types are a tradeoff.
Guidelines:
Choose variable names that provide useful info.
Minimize the scope of local variables.
Consider var
when the initializer provides sufficient information to the reader.
Use var
to break up chained or nested expressions.
Don’t worry too much about "programming to the interface".
Take care when using var
with diamonds or generics.
Take care when using var
with literals.
Choose variable names that provide useful info.
/* ✘ */ var u = UserRepository.findUser(id);
/* ✔ */ var user = UserRepository.findUser(id);
/* 👍*/ var userToLogIn = UserRepository.findUser(id);
Minimize the scope of local variables.
// ✘
var id = 123;
if (id < 100) {
// very long branch
} else {
// oh boy, much more code...
}
LOGGER.info("... " + id);
// ✔ replace branches with method calls
Consider var
when the initializer provides
sufficient information to the reader.
/* ✘ */ var user = Repository.find(id);
/* ✔ */ var user = UserRepository.findUser(id);
/* 👍*/ var user = new User(id);
Use var
to break up chained or nested expressions.
// ✘
return Canvas
.activeCanvas()
.drawings()
.filter(Drawing::isLine)
.map(drawing -> (HorizontalConnection) drawing)
// now we have lines
.filter(line -> length(line) == 7)
.map(this::generateSquare)
// now we have squares
.map(this::createRandomColoredSquare)
.map(this::createRandomBorderedSquare)
.collect(toList());
Use var
to break up chained or nested expressions.
// ✔
var lines = Canvas
.activeCanvas()
.drawings()
.filter(Drawing::isLine)
.map(drawing -> (HorizontalConnection) drawing)
var squares = lines
.filter(line -> length(line) == 7)
.map(this::generateSquare);
return squares
.map(this::createRandomColoredSquare)
.map(this::createRandomBorderedSquare)
.collect(toList());
Don’t worry too much about
"programming to the interface".
// inferred as `ArrayList` (not `List`),
// but that's ok
var users = new ArrayList<User>();
Careful when refactoring:
extracting methods that use var
-ed variables
puts concrete types into method signatures
look out and replace with most general type
Take care when using var
with diamonds or generics.
// ✘ infers `ArrayList<Object>`
var users = new ArrayList<>();
// ✔ infers `ArrayList<User>`
var users = new ArrayList<User>();
Take care when using var
with literals.
// ✘ when used with `var`, these
// variables become `int`
byte b = 42;
short s = 42;
long l = 42;
var
First Contact With var
In Java 10
💻 tiny.cc/java-var /
▶ tiny.cc/java-var-yt
cheat sheet (⇜ print when getting started!)
var
and …
Pattern Matching Things... |
More Cool Things... |
Private Interface Methods ⑨ |
Try-With-Resources ⑨ |
Diamond Operator ⑨ |
SafeVarargs ⑨ |
Deprecation Warnings ⑨⑯ |
Enabling reuse between default methods.
public interface InJava8 {
default boolean evenSum(int... numbers) {
return sum(numbers) % 2 == 0;
}
default boolean oddSum(int... numbers) {
return sum(numbers) % 2 == 1;
}
default int sum(int[] numbers) {
return IntStream.of(numbers).sum();
}
}
public interface InJava9 {
private int sum(int[] numbers) {
return IntStream.of(numbers).sum();
}
}
Just like private methods in abstract classes:
must be implemented
can not be overriden
can only be called in same source file
Pattern Matching Things... |
More Cool Things... |
Private Interface Methods ⑨ |
Try-With-Resources ⑨ |
Diamond Operator ⑨ |
SafeVarargs ⑨ |
Deprecation Warnings ⑨⑯ |
Making try
-with-resources blocks cleaner.
void doSomethingWith(Connection connection)
throws Exception {
try(Connection c = connection) {
c.doSomething();
}
}
Why is c
necessary?
c
necessary?target of close()
must be obvious
⇝ resource should not be reassigned
easiest if resource is final
easiest if resource must be assigned
and can be made implicitly final
try(Connection c = connection)
But since Java 8 we have effectively final!
This works in Java 9:
void doSomethingWith(Connection connection)
throws Exception {
try(connection) {
connection.doSomething();
}
}
compiler knows that connection
is not reassigned
developers know what effectively final means
Pattern Matching Things... |
More Cool Things... |
Private Interface Methods ⑨ |
Try-With-Resources ⑨ |
Diamond Operator ⑨ |
SafeVarargs ⑨ |
Deprecation Warnings ⑨⑯ |
A little more type inference.
Maybe the best example:
List<String> strings = new ArrayList<>();
used at a constructor call
tells Java to infer the parametric type
Diamond did not work with anonymous classes:
<T> Box<T> createBox(T content) {
// we have to put the `T` here :(
return new Box<T>(content) { };
}
Reason are non-denotable types:
might be inferred by compiler
for anonymous classes
can not be expressed by JVM
Java 9 infers denotable types:
<T> Box<T> createBox(T content) {
return new Box<>(content) { };
}
Gives compile error if type is non-denotable:
Box<?> createCrazyBox(Object content) {
List<?> innerList = Arrays.asList(content);
// compile error
return new Box<>(innerList) { };
}
Pattern Matching Things... |
More Cool Things... |
Private Interface Methods ⑨ |
Try-With-Resources ⑨ |
Diamond Operator ⑨ |
SafeVarargs ⑨ |
Deprecation Warnings ⑨⑯ |
One less warning you couldn’t do anything about.
Innocent looking code…
private <T> Optional<T> firstNonNull(T... args) {
return stream(args)
.filter(Objects::nonNull)
.findFirst();
}
Compiler warns (on call site, too):
Possible heap pollution from
parameterized vararg type
For generic varargs argument T… args
,
you must not depend on it being a T[]
!
private <T> T[] replaceTwoNulls(
T value, T first, T second) {
return replaceAllNulls(value, first, second);
}
private <T> T[] replaceAllNulls(T value, T... args) {
// loop over `args`, replacing `null` with `value`
return args;
}
Compiler is aware of the problem and warns you.
If you think, everything’s under control:
@SafeVarargs
private <T> Optional<T> firstNonNull(T... args) {
return // [...]
}
Or not… In Java 8 this is a compile error!
Invalid SafeVarargs annotation. Instance
method <T>firstNonNull(T...) is not final.
The @SafeVarargs
annotation:
tells the caller that all is fine
only makes sense on methods
that can not be overriden
Which methods can’t be overriden?
⇝ final
methods
What about private
methods?
⇝ Damn! 😭
Looong story, here’s the point:
In Java 9 @SafeVarargs
can be applied to private methods.
Pattern Matching Things... |
More Cool Things... |
Private Interface Methods ⑨ |
Try-With-Resources ⑨ |
Diamond Operator ⑨ |
SafeVarargs ⑨ |
Deprecation Warnings ⑨⑯ |
Some come, some go.
Project Valhalla will bring primitive classes:
code like a class, work like an int
have no identity
allow no identity-based operations
Value-based classes are their precursors.
What is identity-based?
constructor calls
mutability
synchronization
serialization
These need to be prevented
for primitive and value-based classes.
Java 16 designates primitive wrapper classes
(Integer
, Long
, Float
, Double
, etc)
as value-based classes.
Warning on both lines:
// use Integer::valueOf instead
Integer answer = new Integer(42);
// don't synchronize on values
synchronize(answer) { /*... */ }
constructors are deprecated for removal
synchronization yields warning
Should this code emit a warning?
// LineNumberInputStream is deprecated
import java.io.LineNumberInputStream;
public class DeprecatedImports {
LineNumberInputStream stream;
}
// LineNumberInputStream is deprecated
import java.io.LineNumberInputStream;
@Deprecated
public class DeprecatedImports {
LineNumberInputStream stream;
}
Java 9 no longer emits warnings
for importing deprecated members.
Warning free:
import java.io.LineNumberInputStream;
@Deprecated
public class DeprecatedImports {
LineNumberInputStream stream;
}
Random Numbers ⑰ |
Socket Channels ⑯ |
String ⑪⑫ |
Stream ⑨⑩⑫⑯ |
Optional ⑨⑩⑪ |
OS Processes ⑨ |
Completable Future ⑫ |
Existing APIs are continuously improved.
Random Numbers ⑰ |
Socket Channels ⑯ |
String ⑪⑫ |
Stream ⑨⑩⑫⑯ |
Optional ⑨⑩⑪ |
OS Processes ⑨ |
Completable Future ⑫ |
Better maintainability and future-proof algorithm selection.
The API before Java 17 was muddied:
limited algorithms
uneven, e.g. nextLong(long bound)
weird inheritance hierarchy
(The implementations weren’t great, either.)
New central abstraction:
RandomGenerator
.
API is basically Random
plus missing methods
no requirements for thread-safety
or cryptographic security
Subtypes specify how one generator
can create another that is:
statistically independent
individually uniform
(Or some approximation thereof.)
Existing classes were updated:
implement RandomGenerator
or suitable subtype
better implementation with less repetition
Current list of pseudorandom number generators (PNRGs):
All interfaces have static factory method of
:
var name = "...";
var rg = RandomGenerator.of(name);
var jg = JumpableGenerator.of(name);
var lg = LeapableGenerator.of(name);
// ...
If algorithm of that name and type…
exists ~> it is returned
doesn’t exist ~> IllegalArgumentException
Advances in PRNG development and analysis require flexibility:
JDKs can contain more algorithms than listed above
new algorithms can be added over time
algorithms can be deprecated at any time
algorithms can be removed in major versions
⇝ Selection by name is not robust.
If you have no specific requirements:
var generator = RandomGenerator.getDefault();
With requirements, use RandomGeneratorFactory
.
Steps to select an algorithm by properties:
call static method all()
to get
Stream<RandomGeneratorFactory>
use methods to filter by properties, e.g.:
isStreamable()
, isSplittable()
, etc
isStatistical()
, isStochastic()
isHardware()
period()
, stateBits()
call create()
to create the generator
var generator = RandomGeneratorFactory.all()
.filter(RandomGeneratorFactory::isJumpable)
.filter(factory -> factory.stateBits() > 128)
.findAny()
.map(RandomGeneratorFactory::create)
// if you need a `JumpableGenerator`:
// .map(JumpableGenerator.class::cast)
.orElseThrow();
Summary:
interface RandomGenerator
as central abstraction
more detailed interfaces specify how to
create one generator from another
existing classes implement suitable interfaces
list of algorithms is extended and more flexible
use RandomGeneratorFactory
to select
algorithms based on properties
Random Numbers ⑰ |
Socket Channels ⑯ |
String ⑪⑫ |
Stream ⑨⑩⑫⑯ |
Optional ⑨⑩⑪ |
OS Processes ⑨ |
Completable Future ⑫ |
Now with Unix domain sockets!
NIO API around SocketChannel
/ServerSocketChannel
:
communication via network sockets
can be non-blocking
allows multiplexing via selector
New in Java 16: Unix domain sockets.
Unix domain sockets:
work with filesystem paths
only for connections on same host
no TCP/IP stack
Server connecting to Unix domain socket:
Path socketFile = Path
.of(System.getProperty("user.home"))
.resolve("server.socket");
UnixDomainSocketAddress address =
UnixDomainSocketAddress.of(socketFile);
ServerSocketChannel serverChannel = ServerSocketChannel
.open(StandardProtocolFamily.UNIX);
serverChannel.bind(address);
// start sending/receiving messages
Client connecting to Unix domain socket:
Path file = Path
.of(System.getProperty("user.home"))
.resolve("server.socket");
UnixDomainSocketAddress address =
UnixDomainSocketAddress.of(file);
SocketChannel channel = SocketChannel
.open(StandardProtocolFamily.INET6);
channel.connect(address);
// start sending/receiving messages
Unix domain socket are safer and faster:
same host ⇝ no inbound connections
filesystem paths
⇝ detailed, well-understood, OS-enforced permissions
no TCP/IP stack ⇝ faster setup, higher throughput
Random Numbers ⑰ |
Socket Channels ⑯ |
String ⑪⑫ |
Stream ⑨⑩⑫⑯ |
Optional ⑨⑩⑪ |
OS Processes ⑨ |
Completable Future ⑫ |
Small improvements to String
.
Getting rid of white space:
String strip();
String stripLeading();
String stripTrailing();
Only at beginning and end of string:
" foo bar ".strip().equals("foo bar");
Wait, what about trim()
?
trim()
defines white space as:
any character whose codepoint
is less than or equal to'U+0020'
(the space character)
strip()
relies on Character::isWhitespace
,
which covers many more cases
Is a string only white space?
boolean isBlank();
Functionally equivalent to:
string.isBlank() == string.strip().isEmpty();
As soon as Java APIs get new method,
scour StackOverflow for easy karma!
Formerly accepted answer:
😍
Ta-da!
Processing a string’s lines:
Stream<String> lines();
splits a string on "\n"
, "\r"
, "\r\n"
lines do not include terminator
more performant than split("\R")
lazy!
Use String::indent
to add or remove
leading white space:
String oneTwo = " one\n two\n";
oneTwo.indent(0).equals(" one\n two\n");
oneTwo.indent(1).equals(" one\n two\n");
oneTwo.indent(-1).equals("one\n two\n");
oneTwo.indent(-2).equals("one\ntwo\n");
Would have been nice to pass resulting indentation,
not change in indentation.
String::indent
normalizes line endings
so each line ends in \n
:
"1\n2".indent(0).equals("1\n2\n");
"1\r\n2".indent(0).equals("1\n2\n");
"1\r2\n".indent(0).equals("1\n2\n");
"1\n2\n".indent(0).equals("1\n2\n");
New method on String
:
public <R> R transform(Function<String, R> f) {
return f.apply(this);
}
Use to chain calls instead of nesting them:
User newUser = parse(clean(input));
User newUser = input
.transform(this::clean)
.transform(this::parse);
Makes more sense at end of long call chain
(stream pipeline?) to chain more calls.
Maybe other classes get transform
, too!
Great for "chain-friendly" APIs like Stream
, Optional
:
// in a museum...
tourists.stream()
.map(this::letEnter)
.transform(this::groupsOfFive)
.forEach(this::giveTour)
Stream<TouristGroup> groupsOfFive(
Stream<Tourist> tourists) {
// this is not trivial,
// but at least possible
}
⇝ Practice with String::transform
!
Random Numbers ⑰ |
Socket Channels ⑯ |
String ⑪⑫ |
Stream ⑨⑩⑫⑯ |
Optional ⑨⑩⑪ |
OS Processes ⑨ |
Completable Future ⑫ |
Small improvements to Java 8 streams.
Create a stream of zero or one elements:
long zero = Stream.ofNullable(null).count();
long one = Stream.ofNullable("42").count();
To use for
even less…
iterate(
T seed,
Predicate<T> hasNext,
UnaryOperator<T> next);
Example:
Stream
.iterate(1, i -> i<=10, i -> 2*i)
.forEach(System.out::println);
// output: 1 2 4 8
Counter Example:
Enumeration<Integer> en = // ...
Stream.iterate(
en.nextElement(),
el -> en.hasMoreElements(),
el -> en.nextElement())
.forEach(System.out::println);
first nextElement()
then hasMoreElements()
⇝ fail
Stream as long as a condition is true:
Stream<T> takeWhile(Predicate<T> predicate);
Example:
Stream.of("a-", "b-", "c-", "", "e-")
.takeWhile(s -> !s.isEmpty())
.forEach(System.out::print);
// output: a-b-c-
Ignore as long as a condition is true:
Stream<T> dropWhile(Predicate<T> predicate);
Example:
Stream.of("a-", "b-", "c-", "de-", "f-")
.dropWhile(s -> s.length() <= 2)
.forEach(System.out::print);
// output: de-f-
Create unmodifiable collections
(in the sense of List::of
et al)
with Collectors
:
Collector<T, ?, List<T>> toUnmodifiableList();
Collector<T, ?, Set<T>> toUnmodifiableSet();
Collector<T, ?, Map<K,U>> toUnmodifiableMap(
Function<T, K> keyMapper,
Function<T, U> valueMapper);
// plus overload with merge function
Collect stream elements in two collectors
and combine their results:
// on Collectors
Collector<T, ?, R> teeing(
Collector<T, ?, R1> downstream1,
Collector<T, ?, R2> downstream2,
BiFunction<R1, R2, R> merger);
Example:
Statistics stats = Stream
.of(1, 2, 4, 5)
.collect(teeing(
// Collector<Integer, ?, Integer>
summingInt(i -> i),
// Collector<Integer, ?, Double>
averagingInt(i -> i),
// BiFunction<Integer, Double, Statistics>
Statistics::of));
// stats = Statistics {sum=12, average=3.0}
Stream::flatMap
is great, but:
sometimes you can’t easily
map to a Stream
creating small/empty streams
can harm performance
For these niche (!) cases,
there’s Stream::mapMulti
.
<R> Stream<R> mapMulti(
BiConsumer<T, Consumer<R>> mapper)
BiConsumer
is called for each element:
gets the element T
gets a Consumer<R>
can pass arbitrarily many R
-s
to the consumer
they show up downstream
So like flatMap
, but imperative.
Stream.of(1, 2, 3, 4)
// changes nothing, just passes on elements
.mapMulti((el, ds) -> ds.accept(el));
Stream
.of(Optional.of("0"), Optional.empty())
// unpacks Optionals
.mapMulti((el, ds) -> el.ifPresent(ds));
Stream
.of(Optional.of("0"), Optional.empty())
.mapMulti(Optional::ifPresent);
Unfortunately, mapMulti
confuses
parametric type inference:
List<String> strings = Stream
.of(Optional.of("0"), Optional.empty())
// without <String>, collect returns List<Object>
.<String> mapMulti(Optional::ifPresent)
.collect(toList());
How often have you written
.collect(Collectors.toList())
?
Answer: too damn often!
But no more:
List<String> strings = Stream
.of("A", "B", "C")
// some stream stuff
.toList()
Like collection factories,
the returned lists are:
immutable/unmodifiable
Unlike them:
they can contain null
Random Numbers ⑰ |
Socket Channels ⑯ |
String ⑪⑫ |
Stream ⑨⑩⑫⑯ |
Optional ⑨⑩⑪ |
OS Processes ⑨ |
Completable Future ⑫ |
Small improvements to Java 8 Optional
.
Choose a non-empty Optional
:
Optional<T> or(Supplier<Optional<T>> supplier);
public interface Search {
Optional<Customer> inMemory(String id);
Optional<Customer> onDisk(String id);
Optional<Customer> remotely(String id);
default Optional<Customer> anywhere(String id) {
return inMemory(id)
.or(() -> onDisk(id))
.or(() -> remotely(id));
}
}
Like ifPresent
but do something if empty:
void ifPresentOrElse(
Consumer<T> action,
Runnable emptyAction);
Example:
void logLogin(String id) {
findCustomer(id)
.ifPresentOrElse(
this::logCustomerLogin,
() -> logUnknownLogin(id));
}
Turns an Optional
into a Stream
of zero or one elements:
Stream<T> stream();
private Optional<Customer> findCustomer(String id) {
// ...
}
Stream<Customer> findCustomers(List<String> ids) {
return ids.stream()
.map(this::findCustomer)
// now we have a Stream<Optional<Customer>>
.filter(Optional::isPresent)
.map(Optional::get)
}
private Optional<Customer> findCustomer(String id) {
// ...
}
Stream<Customer> findCustomers(List<String> ids) {
return ids.stream()
.map(this::findCustomer)
// now we have a Stream<Optional<Customer>>
// we can now filter-map in one step
.flatMap(Optional::stream)
}
List<Order> getOrders(Customer c)
is expensive:
List<Order> findOrdersForCustomer(String id) {
return findCustomer(id)
.map(this::getOrders) // eager
.orElse(new ArrayList<>());
}
Stream<Order> findOrdersForCustomer(String id) {
return findCustomer(id)
.stream()
.map(this::getOrders) // lazy
.flatMap(List::stream);
}
Optional::get
invites misuse
by calling it reflexively.
Maybe get
wasn’t the best name?
New:
T orElseThrow()
Works exactly as get
,
but more self-documenting.
Name in line with other accessors:
T orElse(T other)
T orElseGet(Supplier<T> supplier)
T orElseThrow()
throws NoSuchElementException
T orElseThrow(
Supplier<EX> exceptionSupplier)
throws EX
No more !foo.isPresent()
:
boolean isEmpty()
Does exactly what
you think it does.
Random Numbers ⑰ |
Socket Channels ⑯ |
String ⑪⑫ |
Stream ⑨⑩⑫⑯ |
Optional ⑨⑩⑪ |
OS Processes ⑨ |
Completable Future ⑫ |
Improving interaction with OS processes.
ls /home/nipa/tmp | grep pdf
Path dir = Paths.get("/home/nipa/tmp");
ProcessBuilder ls = new ProcessBuilder()
.command("ls")
.directory(dir.toFile());
ProcessBuilder grepPdf = new ProcessBuilder()
.command("grep", "pdf")
.redirectOutput(Redirect.INHERIT);
List<Process> lsThenGrep = ProcessBuilder
.startPipeline(List.of(ls, grepPdf));
Process
Cool new methods on Process
:
boolean supportsNormalTermination();
long pid();
CompletableFuture<Process> onExit();
Stream<ProcessHandle> children();
Stream<ProcessHandle> descendants();
ProcessHandle toHandle();
ProcessHandle
New functionality actually comes from ProcessHandle
.
Interesting static
methods:
Stream<ProcessHandle> allProcesses();
Optional<ProcessHandle> of(long pid);
ProcessHandle current();
ProcessHandle
can return Info
:
command, arguments
start time
CPU time
Random Numbers ⑰ |
Socket Channels ⑯ |
String ⑪⑫ |
Stream ⑨⑩⑫⑯ |
Optional ⑨⑩⑪ |
OS Processes ⑨ |
Completable Future ⑫ |
Asynchronous error recovery.
// start an asynchronous computation
public static CompletableFuture<T> supplyAsync(
Supplier<T>);
// attach further steps
public CompletableFuture<U> thenApply(Function<T, U>);
public CompletableFuture<U> thenCompose(
Function<T, CompletableFuture<U>);
public CompletableFuture<Void> thenAccept(Consumer<T>);
// wait for the computation to finish and get result
public T join();
Example:
public void loadWebPage() {
String url = "https://nipafx.dev";
CompletableFuture<WebPage> future = CompletableFuture
.supplyAsync(() -> webRequest(url))
.thenApply(html -> new WebPage(url, html));
// ... do other stuff
WebPage page = future.join();
}
private String webRequest(String url) {
// make request to URL and return HTML
// (this can take a while)
}
A pipeline or stage completes when
the underlying computation terminates.
it completes normally if
the computation yields a result
it completes exceptionally if
the computation results in an exception
Two methods to recover errors:
// turn the error into a result
CompletableFuture<T> exceptionally(Function<Throwable, T>);
// turn the result or error into a new result
CompletableFuture<U> handle(BiFunction<T, Throwable, U>);
They turn exceptional completion of the previous stage
into normal completion of the new stage.
Example:
loadUser(id)
.thenCompose(this::loadUserHistory)
.thenCompose(this::createRecommendations)
.exceptionally(ex -> {
log.warn("Recommendation error", ex)
return createDefaultRecommendations();
})
.thenAccept(this::respondWithRecommendations);
Error recovery accepts functions
that produce CompletableFuture
:
exceptionallyCompose(
Function<Throwable, CompletionStage<T>>)
Which threads actually compute the stages?
supplyAsync(Supplier<T>)
is executed
in the common fork/join pool
for other stages it’s undefined:
could be the same thread as the previous stage
could be another thread in the pool
could be the thread calling thenAccept
et al.
How to force async computation?
All "composing" methods
have an …Async
companion, e.g.:
thenApplyAsync(Function<T, U>);
thenAcceptAsync(Consumer<T>);
They submit each stage as a separate task
to the common fork/join pool.
Error recovery can be asynchronous:
CompletableFuture<T> exceptionallyAsync(
Function<Throwable, T>)
CompletableFuture<T> exceptionallyComposeAsync(
Function<Throwable, CompletableFuture<T>>)
There are overloads that accept Executor
.
Two great sources on
Java API changes
between versions:
In Java 9 to 11:
Path.of(String); // ~ Paths.get(String) ⑪
Files.readString(Path); // ⑪
Files.writeString(Path, CharSequence, ...); // ⑪
Reader.transferTo(Writer); // ⑩
InputStream.transferTo(OutputStream); // ⑨
Reader.nullReader(); // ⑪
Writer.nullWriter(); // ⑪
InputStream.nullInputStream(); // ⑪
OutputStream.nullOutputStream(); // ⑪
In Java 12 and 13:
Files.mismatch(Path, Path); // ⑫
FileSystems.newFileSystem(Path, ...); // ⑬
ByteBuffer.get(int, ...) // ⑬
ByteBuffer.put(int, ...) // ⑬
// in Java 14
StrictMath.decrementExact(int);
StrictMath.decrementExact(long);
StrictMath.incrementExact(int);
StrictMath.incrementExact(long);
StrictMath.negateExact(int);
StrictMath.negateExact(long);
// in Java 15
Math.absExact(int)
Math.absExact(long)
StrictMath.absExact(int)
StrictMath.absExact(long)
In Java 10:
DateTimeFormatter.localizedBy(Locale);
In Java 11:
Collection.toArray(IntFunction<T[]>);
Predicate.not(Predicate<T>); // static
Pattern.asMatchPredicate(); // ⇝ Predicate<String>
In Java 12:
NumberFormat::getCompactNumberInstance
In Java 15:
// instance version of String::format
String.formatted(Object... args);
In Java 16:
Objects.checkIndex(long, long)
Objects.checkFromToIndex(long, long, long)
Objects.checkFromIndexSize(long, long, long)
Vector |
Address Resolution SPI ⑱ |
Reactive HTTP/2 Client ⑪⑯ |
Collection Factories ⑨⑩ |
Reactive Streams ⑨ |
Stack-Walking ⑨ |
New APIs are added over time.
Vector |
Address Resolution SPI ⑱ |
Reactive HTTP/2 Client ⑪⑯ |
Collection Factories ⑨⑩ |
Reactive Streams ⑨ |
Stack-Walking ⑨ |
Make full use of your CPUs.
[E]xpress vector computations that reliably compile at run time to optimal vector hardware instructions on supported CPU architectures and thus achieve superior performance to equivalent scalar computations
I don’t have much on this
but others do:
FizzBuzz – SIMD Style!
article &
repo with benchmark
Vector |
Address Resolution SPI ⑱ |
Reactive HTTP/2 Client ⑪⑯ |
Collection Factories ⑨⑩ |
Reactive Streams ⑨ |
Stack-Walking ⑨ |
Plug in host name and address resolution.
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);
}
public class TransparentInetAddressResolverProvider
extends InetAddressResolverProvider {
@Override
public InetAddressResolver get(
Configuration config) {
return new TransparentInetAddressResolver(
config.builtinResolver());
}
// name()
}
// in module declaration
provides InetAddressResolverProvider
with TransparentInetAddressResolverProvider;
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);
}
Vector |
Address Resolution SPI ⑱ |
Reactive HTTP/2 Client ⑪⑯ |
Collection Factories ⑨⑩ |
Reactive Streams ⑨ |
Stack-Walking ⑨ |
HTTP/2! And reactive! Woot!
To send a request and get a response:
use builder to create immutable HttpClient
use builder to create immutable HttpRequest
pass request to client to receive HttpResponse
HttpClient client = HttpClient.newBuilder()
.version(HTTP_2)
.connectTimeout(ofSeconds(5))
.followRedirects(ALWAYS)
.build();
More options:
proxy
SSL context/parameters
authenticator
cookie handler
HttpRequest request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("https://nipafx.dev"))
.setHeader("header-name", "header-value")
.build();
more HTTP methods (duh!)
individual timeout
individual HTTP version
request "100 CONTINUE"
before sending body
create prefilled builder from existing request ⑯
// the generic `String`...
HttpResponse<String> response = client.send(
request,
// ... comes from this body handler ...
BodyHandlers.ofString());
// ... and defines `body()`s return type
String body = response.body();
status code, headers, SSL session
request
intermediate responses
(redirection, authentication)
Great, but where’s the reactive sauce?
Three places:
send request asynchronously
provide request body with
Publisher<ByteBuffer>
receive response body with
Subscriber<String>
or
Subscriber<List<ByteBuffer>>
Submit request to thread pool until completes:
CompletableFuture<String> responseBody = client
.sendAsync(request, BodyHandlers.ofString())
.thenApply(this::logHeaders)
.thenApply(HttpResponse::body);
uses "a default executor" to field requests
pool can be defined when client is built with
HttpClient.Builder.executor(Executor)
If a request has a long body,
no need to prepare it in its entirety:
Publisher<ByteBuffer> body = // ...
HttpRequest post = HttpRequest.newBuilder()
.POST(BodyPublishers.fromPublisher(body))
.build();
client.send(post, BodyHandlers.ofString())
client
subscribes to body
as body
publishes byte buffers,
client
sends them over the wire
If a response has a long body,
no need to wait before processing:
Subscriber<String> body = // ...
HttpResponse<Void> response = client.send(
request,
BodyHandlers.fromLineSubscriber(subscriber));
client
subscribes body
to itself
as client
receives response bytes,
it parses to lines and passes to body
Benefits of reactive
request/response bodies:
receiver applies backpressure:
on requests, client
on responses, body
body
controls memory usage
early errors lead to partial processing
need "reactive tools" to create body
from higher-level Java objects (e.g. File
)
Short version:
there’s a class WebSocket
send[Text|Binary|…]
methods
return CompletableFuture
socket calls Listener
methods
on[Text|Binary|…]
(WebSocket
and Listener
behave like
Subscription
and Subscriber
.)
No long version. 😛
Vector |
Address Resolution SPI ⑱ |
Reactive HTTP/2 Client ⑪⑯ |
Collection Factories ⑨⑩ |
Reactive Streams ⑨ |
Stack-Walking ⑨ |
Easy creation of ad-hoc collections.
Wouldn’t this be awesome?
List<String> list = [ "a", "b", "c" ];
Map<String, Integer> map = [ "one" = 1, "two" = 2 ];
Not gonna happen!
language change is costly
binds language to collection framework
strongly favors specific collections
List<String> list = List.of("a", "b", "c");
Map<String, Integer> mapImmediate = Map.of(
"one", 1,
"two", 2,
"three", 3);
Map<String, Integer> mapEntries = Map.ofEntries(
Map.entry("one", 1),
Map.entry("two", 2),
Map.entry("three", 3));
collections are immutable
(no immutability in types, though)
collections are value-based
null
elements/keys/values are forbidden
iteration order is random between JVM starts
(except for lists, of course!)
Creating immutable copies:
/* on List */ List<E> copyOf(Collection<E> coll);
/* on Set */ Set<E> copyOf(Collection<E> coll);
/* on Map */ Map<K, V> copyOf(Map<K,V> map);
Great for defensive copies:
public Customer(List<Order> orders) {
this.orders = List.copyOf(orders);
}
Vector |
Address Resolution SPI ⑱ |
Reactive HTTP/2 Client ⑪⑯ |
Collection Factories ⑨⑩ |
Reactive Streams ⑨ |
Stack-Walking ⑨ |
The JDK as common ground for reactive stream libraries.
Publisher
produces items to consume
can be subscribed to
Subscriber
subscribes to publisher
onNext
, onError
, onComplete
Subscription
connection from subscriber to publisher
request
, cancel
create Publisher pub
and Subscriber sub
call pub.subscribe(sub)
pub creates Subscription script
and calls sub.onSubscription(script)
sub
can store script
sub
calls script.request(10)
pub
calls sub.onNext(element)
(max 10x)
pub
may call sub.OnError(err)
or sub.onComplete()
sub
may call script.cancel()
JDK only provides three interfaces
and one simple implementation.
(Also called Flow API.)
So far, only reactive HTTP/2 API ⑪ uses Flow.
Vector |
Address Resolution SPI ⑱ |
Reactive HTTP/2 Client ⑪⑯ |
Collection Factories ⑨⑩ |
Reactive Streams ⑨ |
Stack-Walking ⑨ |
Examining the stack faster and easier.
StackWalker::forEach
void forEach (Consumer<StackFrame>);
public static void main(String[] args) { one(); }
static void one() { two(); }
static void two() {
StackWalker.getInstance()
.forEach(System.out::println);
}
// output
StackWalkingExample.two(StackWalking.java:14)
StackWalkingExample.one(StackWalking.java:11)
StackWalkingExample.main(StackWalking.java:10)
StackWalker::walk
T walk (Function<Stream<StackFrame>, T>);
static void three() {
String line = StackWalker.getInstance().walk(
frames -> frames
.filter(f -> f.getMethodName().contains("one"))
.findFirst()
.map(f -> "Line " + f.getLineNumber())
.orElse("Unknown line");
);
System.out.println(line);
}
// output
Line 11
getInstance
takes options as arguments:
SHOW_REFLECT_FRAMES
for reflection frames
SHOW_HIDDEN_FRAMES
e.g. for lambda frames
RETAIN_CLASS_REFERENCE
for Class<?>
forEach
and walk
operate on StackFrame
:
class and method name
class as Class<?>
bytecode index and isNative
Can upgrade to StackTraceElement
(expensive):
file name and line number
creating StackTraceElement
is expensive
(for file name and line number)
lazy evaluation pays off for partial traversal
(Benchmarks performed by Arnaud Roger)
Packaging Tool ⑯ |
Strong Encapsulation ⑯ |
Helpful NPEs ⑭ |
Launch Source File ⑪ |
Unified Logging ⑨ |
Multi-Release JARs ⑨ |
Redirected Platform Logging ⑨ |
Continuous improvements to the JVM and its tools.
Packaging Tool ⑯ |
Strong Encapsulation ⑯ |
Helpful NPEs ⑭ |
Launch Source File ⑪ |
Unified Logging ⑨ |
Multi-Release JARs ⑨ |
Redirected Platform Logging ⑨ |
Creating self-contained, OS-typical Java apps.
I don’t have much on this
but others do:
Packaging Tool ⑯ |
Strong Encapsulation ⑯ |
Helpful NPEs ⑭ |
Launch Source File ⑪ |
Unified Logging ⑨ |
Multi-Release JARs ⑨ |
Redirected Platform Logging ⑨ |
JDK internals - hidden by default.
Recap:
all JDK code is in modules
only public classes/members in
exported packages can be accessed
static or reflective access to others is illegal
But…
But JVM can make an exception for:
code on the class path
that accesses pre-JDK-9 packages
Can be configured with --illegal-access
.
$ java --class-path 'mods/*'
--illegal-access=deny
org.example.Main
Four modes:
permit
: warning on first reflective access to package
warn
: warning on each reflective access
debug
: like warn
plus stack trace
deny
: illegal access denied (static + reflective)
First three permit static access.
Configuration:
Java 9-15: permit
is default
Java 16: deny
is default
Java 17: --illegal-access
is no-op
Java ??: the option will be removed
I recommend:
--illegal-access=deny
to prevent unnoticed access
(before Java 16)
--add-exports
/--add-opens
for specific exceptions
A common case:
Code needs types that
the owning module doesn’t export.
Export them with --add-exports
:
$ java --class-path 'mods/*'
# --add-exports <module>/<package>=<target>
# class path ~ ALL-UNNAMED
--add-exports java.base/java.util=ALL-UNNAMED
org.example.Main
(Works for javac
and java
.)
Another common case:
A module reflects on types from a package that
the owning module doesn’t open.
Open packages with add-opens
:
$ java --class-path 'mods/*'
# --add-opens <module>/<package>=<target>
# class path ~ ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
org.example.Main
(Works for java
.)
These command line flags
mark technical debt.
⇝ Work hard to get rid of them ASAP!
And henceworth enjoy strong encapsulation!
Packaging Tool ⑯ |
Strong Encapsulation ⑯ |
Helpful NPEs ⑭ |
Launch Source File ⑪ |
Unified Logging ⑨ |
Multi-Release JARs ⑨ |
Redirected Platform Logging ⑨ |
Finally can NPEs be helpful!
java.lang.NullPointerException
at dev.nipafx.Regular.doing(Regular.java:28)
at dev.nipafx.Business.its(Business.java:20)
at dev.nipafx.Code.thing(Code.java:11)
Ok-ish for coders, but suck for everybody else.
With -XX:+ShowCodeDetailsInExceptionMessages
:
java.lang.NullPointerException:
Cannot invoke "String.length()" because the return
value of "dev.nipafx.Irregular.doing()"
is null
at dev.nipafx.Regular.doing(Regular.java:28)
at dev.nipafx.Business.its(Business.java:20)
at dev.nipafx.Code.thing(Code.java:11)
The command line option
is needed (for now), because:
performance
security
compatibility
But:
It is intended to enable code details
in exception messages by default
in a later release.
Packaging Tool ⑯ |
Strong Encapsulation ⑯ |
Helpful NPEs ⑭ |
Launch Source File ⑪ |
Unified Logging ⑨ |
Multi-Release JARs ⑨ |
Redirected Platform Logging ⑨ |
Faster feedback with fewer tools.
Compiling and running
simple Java programs is verbose.
Not any more!
java HelloJava11.java
How it works:
compiles source into memory
runs from there
Details:
requires module jdk.compiler
processes options like class/module path et al.
interprets @files
for easier option management
Mostly similar to jshell
:
easier demonstrations
more portable examples
experimentation with new language features
(combine with --enable-preview
)
But also: script files!
Steps towards easier scripting:
arbitrary file names
shebang support
Use --source
if file doesn’t end in .java
:
java --source 11 hello-java-11
To create "proper scripts":
include shebang in source:
#!/opt/jdk-11/bin/java --source 11
name script and make it executable
execute it as any other script:
# from current directory:
./hello-java-11
# from PATH:
hello-java-11
Packaging Tool ⑯ |
Strong Encapsulation ⑯ |
Helpful NPEs ⑭ |
Launch Source File ⑪ |
Unified Logging ⑨ |
Multi-Release JARs ⑨ |
Redirected Platform Logging ⑨ |
Observing the JVM at work.
New logging infrastructure for the JVM
(e.g. OS interaction, threading, GC, etc.):
JVM log messages pass through new mechanism
works similar to known logging frameworks:
textual messages
log level
time stamps
meta information (like subsystem)
output can be configured with -Xlog
Plain use of -Xlog
:
$ java -Xlog -version
# truncated a few messages
> [0.002s][info][os ] HotSpot is running ...
# truncated a lot of messages
You can see:
JVM uptime (2ms)
log level (info
)
tags (os
)
message
-Xlog
This can be configured:
which messages to show
where messages go
what messages should say
How? -Xlog:help
lists all options.
Configure with selectors: $TAG_SET=$LEVEL
:
# "exactly gc" on "warning"
-Xlog:gc=warning
# "including gc" on "warning"
-Xlog:gc*=warning
# "exactly gc and os" on "debug"
-Xlog:gc+os=debug
# "gc" on "debug" and "os" on warning
-Xlog:gc=debug,os=warning
Defaults:
-Xlog # the same as -Xlog:all
-Xlog:$TAG # same as -Xlog:$TAG=info
Three possible locations:
stdout
(default)
stderr
file=$FILENAME
(file rotation is possible)
Example:
# all debug messages into application.log
-Xlog:all=debug:file=application.log
Decorators define what is shown:
time
: time and date (also in ms and ns)
uptime
: time since JVM start (also in ms and ns)
pid
: process identifier
tid
: thread identifier
level
: log level
tags
: tag-set
Example:
# show uptime in ms and level
-Xlog:all:stdout:level,uptimemillis
Formal syntax:
-Xlog:$SELECTORS:$OUTPUT:$DECORATORS:$OUTPUT_OPTS
$SELECTORS
are pairs of tag sets and log levels
(the docs call this what-expression)
$OUTPUT
is stdout
, stderr
, or file=<filename>
$DECORATORS
define what is shown
$OUTPUT_OPTS
configure file rotation
Elements have to be defined from left to right.
(But can be empty, e.g. -Xlog::stderr
.)
Packaging Tool ⑯ |
Strong Encapsulation ⑯ |
Helpful NPEs ⑭ |
Launch Source File ⑪ |
Unified Logging ⑨ |
Multi-Release JARs ⑨ |
Redirected Platform Logging ⑨ |
"Do this on Java X, do that on Java Y."
Main
calls Version
:
public class Main {
public static void main(String[] args) {
System.out.println(new Version().get());
}
}
Version
exists twice:
public class Version {
public String get() { return "Java 8"; }
}
public class Version {
public String get() { return "Java 9+"; }
}
(Btw, IDEs hate this!)
Now, here’s the magic:
compile Main
and Version[8]
to out/java-8
compile Version[9]
to out/java-9
use new jar
flag --release
:
jar --create --file out/mr.jar
-C out/java-8 .
--release 9 -C out/java-9 .
└ dev
└ nipafx ... (moar folders)
├ Main.class
└ Version.class # 8
└ META-INF
└ versions
└ 9
└ dev
└ nipafx ... (moar folders)
└ Version.class # 9
With java -cp out/mr.jar …Main
:
prints "Java 8"
on Java 8
prints "Java 9+"
on Java 9 and later
Great Success!
Packaging Tool ⑯ |
Strong Encapsulation ⑯ |
Helpful NPEs ⑭ |
Launch Source File ⑪ |
Unified Logging ⑨ |
Multi-Release JARs ⑨ |
Redirected Platform Logging ⑨ |
Use your logging framework of choice
as backend for JDK logging.
New logging infrastructure for the core libraries
(i.e. this does not apply to JVM log messages!)
new interface System.Logger
used by JDK classes
instances created by System.LoggerFinder
The interesting bit:
LoggerFinder
is a service!
Logger
public class SystemOutLogger implements Logger {
public String getName() { return "SystemOut"; }
public boolean isLoggable(Level level) { return true; }
public void log(
Level level, ResourceBundle bundle,
String format, Object... params) {
System.out.println(/* ...*/);
}
// another, similar `log` method
}
LoggerFinder
public class SystemOutLoggerFinder
extends LoggerFinder {
public Logger getLogger(
String name, Module module) {
return new SystemOutLogger();
}
}
Module descriptor for system.out.logger:
module system.out.logger {
provides java.lang.System.LoggerFinder
with system.out.logger.SystemOutLoggerFinder;
}
Module system and JDK take care of the rest!
In Java 12:
constants API (JEP 334)
HmacPBE (JDK-8215450)
finer PKCS12 KeyStore config (JDK-8076190)
In Java 15:
Nashorn was removed (JEP 372)
Garbage Collection Improvements ◎ |
Application Class-Data Sharing ⑩⑫⑬ |
Compact Strings ⑨ |
Indified String Concatenation ⑨ |
Garbage Collection Improvements ◎ |
Application Class-Data Sharing ⑩⑫⑬ |
Compact Strings ⑨ |
Indified String Concatenation ⑨ |
Each Java version sees (incremental) GC improvements.
All following diagrams come
from Stefan Johansson's post
GC progress from JDK 8 to JDK 17.
Garbage Collection Improvements ◎ |
Application Class-Data Sharing ⑩⑫⑬ |
Compact Strings ⑨ |
Indified String Concatenation ⑨ |
Improving application launch times.
JVM steps to execute a class’s bytecode:
looks up class in JAR
loads bytecode
verifies bytecode
stores class-data in
internal data structure
This takes quite some time.
If classes don’t change, the resulting
class-data is always the same!
Idea behind class-data sharing:
create class-data once
dump it into an archive
reuse the archive in future launches
(file is mapped into memory)
My experiments with a large desktop app
(focusing on classes required for launch):
archive has 250 MB for ~24k classes
launch time reduced from 15s to 12s
Bonus: Archive can be shared across JVMs.
Two variants:
just for JDK classes
JDK + application classes
Create JDK archive on Java 10/11:
# possibly as root
java -Xshare:dump
Java 12+ downloads include
CDS archive for JDK classes.
Use the archive:
$ java
-Xshare:on
# [... class path for app and deps ...]
org.example.Main
If archive is missing or faulty:
-Xshare:on
fails fast
-Xshare:auto
(default) ignores archive
(Slides rely on default, i.e. no -Xshare
.)
Create an AppCDS archive:
manually on Java 10+
dynamically on Java 13+
First manually, then dynamically.
To manually create an AppCDS archive,
first create a list of classes
$ java
-XX:DumpLoadedClassList=classes.lst
# [... class path for app and deps ...]
org.example.Main
Then, classes.lst
contains
slash-separated names of loaded classes.
Use the list to create the archive:
$ java
-Xshare:dump
-XX:SharedClassListFile=classes.lst
-XX:SharedArchiveFile=app-cds.jsa
# [... class path for app and deps ...]
Creates archive app-cds.jsa
.
Use the archive:
$ java
-XX:SharedArchiveFile=app-cds.jsa
# [... class path for app and deps ...]
org.example.Main
Java 13 can create archive when
program exits (without crash):
steps #0 and #1 are replaced by:
$ java
-XX:ArchiveClassesAtExit=dyn-cds.jsa
# [... class path for app and deps ...]
org.example.Main
step #2 as before:
$ java
-XX:SharedArchiveFile=app-cds.jsa
# [... class path for app and deps ...]
org.example.Main
The dynamic archive:
builds on the JDK-archive
contains all loaded app/lib classes
including those loaded by
user-defined class loaders
What are the two biggest challenges
in software development?
naming
cache invalidation
off-by-one errors
The archive is a cache!
It’s invalid when:
a JAR is updated
class path is reordered
a JAR is added
(unless when appended)
To invalidate the archive:
during creation:
Java stores used class path in archive
class path may not contain wild cards
class path may not contain exploded JARs
when used:
Java checks whether stored path
is prefix of current path
Class path, class path…
what about the module path?
In this release, CDS cannot archive classes from user-defined modules (such as those specified in
--module-path
). We plan to add that support in a future release.
For more, read this article:
tiny.cc/app-cds
Observe sharing with
-Xlog:class+load
(unified logging)
Garbage Collection Improvements ◎ |
Application Class-Data Sharing ⑩⑫⑬ |
Compact Strings ⑨ |
Indified String Concatenation ⑨ |
Going from UTF-16 to ISO-8859-1.
20% - 30% of heap are char[]
for String
a char
is UTF-16 code unit ⇝ 2 bytes
most strings only require ISO-8859-1 ⇝ 1 byte
10% - 15% of memory is wasted!
For Java 9, String
was changed:
uses byte[]
instead of char[]
bytes per character:
1 if all characters are ISO-8859-1
2 otherwise
Only possible because String
makes
defensive copies of all arguments.
Simple benchmark:
(by Aleksey Shipilëv)
String method = generateString(size);
public String work() {
return "Calling method \"" + method + "\"";
}
Depending on circumstances:
throughput 1.4x
garbage less 1.85x
Background on String
performance improvements:
Garbage Collection Improvements ◎ |
Application Class-Data Sharing ⑩⑫⑬ |
Compact Strings ⑨ |
Indified String Concatenation ⑨ |
"Improving" + "String" + "Concatenation"
What happens when you run:
String s = greeting + ", " + place + "!";
bytecode uses StringBuilder
JIT may (!) recognize and optimize
by writing content directly to new byte[]
breaks down quickly
(e.g. with long
or double
)
new optimizations create new bytecode
new optimizations require recompile
test matrix JVMs vs bytecodes explodes
String::concat
?There is no such method.
concat(String… args)
requires toString
concat(Object… args)
requires boxing
Nothing fancy can be done
because compiler must use public API.
Invokedynamic came in Java 7:
compiler creates a recipe
runtime has to process it
defers decisions from compiler to runtime
(Used for lambda expressions and in Nashorn.)
With Indy compiler can express
"concat these things"
(without boxing!)
JVM executes by writing content
directly to new byte[]
.
Background on String
performance improvements:
And many, many smaller changes.
new releases are generally faster
in the cloud:
less CPU/memory ⇝ lower costs
⇝ Updating saves money!
(And you’re doing it anyway sooner or later.)
random generator API diagrams:
Nicolai Parlog
(CC-BY-NC 4.0)