(Read this article on the blog)
Last updated on 2023/09/22 to include changes up to JDK 21.
This article is also available in Chinese by Alex Tan.
When Java 8 introduced Streams and Lambdas it was a big change, enabling functional programming style to be expressed with much less boilerplate. Since then Java switched to a more rapid release cadence, making a new Java version appear every six months. These versions constantly bring new features to the language. Among the recent features probably the most important ones are Records, Pattern matching and Sealed classes that makes it easier to program with plain data.
Related Articles
Language enhancements after Java 8
Java 21
- Pattern Matching for switch
- Record Patterns
- Unnamed Patterns and Variables (Preview 🔍)
- String Templates (Preview 🔍)
- Unnamed Classes and Instance Main Methods (Preview 🔍)
Java 17
- Sealed Classes
- Tip: Consider sealed classes vs Enums
Java 16
- Record Classes
- Tip: Use Local Records to model intermediate transformations
- Tip: Check your libraries
- Pattern Matching for instanceof
Java 15
- Text Blocks
- Tip: Preserve trailing spaces
- Tip: Produce the correct newline characters for Windows
- Tip: Pay attention to consistent indentation
- Helpful NullPointerExceptions
- Tip: Check your tooling
Java 14
- Switch Expressions
- Tip: Use arrow syntax
Java 11
- Local-Variable Type Inference
- Tip: Keep readability in mind
- Tip: Pay attention to preserve important type information
- Tip: Make sure to read the official style guides
Java 9
- Allow private methods in interfaces
- Diamond operator for anonymous inner classes
- Allow effectively-final variables to be used as resources in try-with-resources statements
- Tip: Watch out for released resources
- Underscore is no longer a valid identifier name
- Improved Warnings
For an overview of all the JEPs shaping the new platform, including API, performance and security improvements check the curated list of all enhancements since Java 8.
Pattern Matching for switch
Available since: JDK 21 (Preview in JDK 18 JDK 19 JDK 20)
Previously, switch
was very limited: the cases could only test exact equality, and only for values of a few types: numbers, Enum types and Strings.
This feature enhances switch
to work on any type and to match on more complex patterns.
These additions are backwards compatible, switch
with the traditional constants work just as before, for example, with Enum values:
var symbol = switch (expression) {
case ADDITION -> "+";
case SUBTRACTION -> "-";
case MULTIPLICATION -> "*";
case DIVISION -> "/";
};
However, now it also works with type patterns introduced by JEP 394: Pattern Matching for instanceof:
return switch (expression) {
case Addition expr -> "+";
case Subtraction expr -> "-";
case Multiplication expr -> "*";
case Division expr -> "/";
};
A pattern supports guards, written as type pattern when guard expression
:
String formatted = switch (o) {
case Integer i when i > 10 -> String.format("a large Integer %d", i);
case Integer i -> String.format("a small Integer %d", i);
default -> "something else";
};
This makes a very nice symmetry with the type patterns used in if
statements, because similar patterns can be used as conditionals:
if (o instanceof Integer i && i > 10) {
return String.format("a large Integer %d", i);
} else if (o instanceof Integer i) {
return String.format("a large Integer %d", i);
} else {
return "something else";
}
Similarly to the type patterns in if
conditions, the scope of the pattern variables are flow sensitive. For example, in the case below
the scope of i
is the guard expression and the right hand side expression:
case Integer i when i > 10 -> String.format("a large Integer %d", i);
Generally it works just as you’d expect, but there are many rules and edge cases involved. If you are interested, I recommend to read the corresponding JEPs or see the Pattern matching for instanceof chapter.
Switch can now also match null
values. Traditionally, when a null
value was supplied to a switch
, it threw a NullPointerException
.
For backwards compatibility, this is still the case when there’s no explicit null
pattern defined. However, now an explicit case for null
can be added:
switch (s) {
case null -> System.out.println("Null");
case "Foo" -> System.out.println("Foo");
default -> System.out.println("Something else");
}
Switch expressions always had to be exhaustive, in other words, they have to cover all possible input types.
This is still the case with the new patterns, the Java compiler emits an error when the switch
is incomplete:
Object o = 1234;
// OK
String formatted = switch (o) {
case Integer i -> String.format("a small Integer %d", i);
default -> "something else";
};
// Compile error - 'switch' expression does not cover all possible input values
// Since o is an Object, the only way to fix it is to add a default case
String formatted = switch (o) {
case Integer i -> String.format("a small Integer %d", i);
};
This feature synergizes well with enums, Sealed Classes and generics. If there’s only a fixed set of possible alternatives that exist, the default case can be omitted. Also, this feature helps a lot to maintain the integrity of the codebase when the domain is extended — for example, with a new constant in the enum. Due to the exhaustiveness check, all related switch expressions will yield a compiler error where the default case is not handled. For another example, consider the example code from the JEP:
sealed interface IT> permits A, B {}
final class AX> implements IString> {}
final class BY> implements IY> {}
static int testGenericSealedExhaustive(IInteger> i) {
return switch (i) {
case BInteger> bi -> 42;
};
}
This code compiles because the compiler can detect that only A
and B
are the valid subtypes of I
, and that due to the generic parameter Integer
, the parameter can only be an instance of B
.
Exhaustiveness is checked at compile time but if at runtime a new implementation pops up (e.g. from a separate compilation), the compiler also inserts a synthetic default case that throws a MatchException
.
The compiler also performs the opposite of the exhaustiveness check: it emits an error when a case completely dominates another.
Object o = 1234;
// Compile error - the second case is dominated by a preceding case label
String formatted = switch (o) {
case Integer i -> String.format("a small Integer %d", i);
case Integer i when i > 10 -> String.format("a large Integer %d", i);
default -> "something else";
};
This makes default cases to emit a compile time error if all possible types are handled otherwise.
For readability reasons, the dominance checking forces constant case labels to appear before the corresponding type-based pattern. The goal is to always have the more specific cases first. As an example, the cases in the following snippet are only valid in this exact order. If you’d try to rearrange them, you would get a compilation error.
switch(num) {
case -1, 1 -> "special case";
case Integer i && i > 0 -> "positive number";
case Integer i -> "other integer";
}
Resources:
- Inside Java Podcast Episode 17 “Pattern Matching for switch” with Gavin Bierman
- Inside Java Podcast Episode 26: “Java 19 is Here!” with Brian Goetz and Ron Pressler
- Inside Java Podcast Episode 28: “Java Language - State of the Union” with Gavin Bierman
Record Patterns
Available since: JDK 21 (Preview in JDK 19 JDK 20)
With Switch and instanceof it’s possible match on patterns. The first kind of supported pattern was the type pattern that tests if the parameter is of a given type, and if so, it captures it to a new variable:
// Pattern matching with a type pattern using instanceof
if (obj instanceof String s) {
// ... use s ...
}
// Pattern matching with a type pattern using switch
switch (obj) {
case String s -> // ... use s ...
// ... other cases ...
};
Record Patterns extends the pattern matching capabilities of Java beyond simple type patterns to match and deconstruct Record values. It supports nesting to enable declarative, data focused programming.
For a demonstration, consider the following ColoredPoint
, which is composed of a Point2D
and a Color
:
interface Point { }
record Point2D(int x, int y) implements Point { }
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) { }
Object r = new ColoredPoint(new Point2D(3, 4), Color.GREEN);
Note, that while we use a Point2D
to construct r
, ColoredPoint
accepts other Point
implementations as well.
Without pattern matching, we’d need two explicit type checks to detect if object r
is a ColoredPoint
that holds Point2D
, and also quite some work to
extract the values x
, y
, and c
:
if (r instanceof ColoredPoint) {
ColoredPoint cp = (ColoredPoint) r;
if (cp.p() instanceof Point2D) {
Point2D pt = (Point2D) cp.p();
int x = pt.x();
int y = pt.y();
Color c = cp.c();
// work with x, y, and c
}
}
Using type patterns for instanceof makes things a bit better as there’s no longer a need for the casts, but conceptually the same thing needs to be done as previously:
if (r instanceof ColoredPoint cp && cp.p() instanceof Point2D pt) {
int x = pt.x();
int y = pt.y();
Color c = cp.c();
// work with x, y, and c
}
However this can be greatly simplified with Record Patterns that offer a concise way to test the shape of the data and to deconstruct it:
if (r instanceof ColoredPoint(Point2D(int x, int y), Color c)) {
// work with x, y, and c
}
Patterns can be nested, which allows easy navigation in the objects, allowing the programmer to focus on the data expressed by those objects. Also it centralizes error handling. A nested pattern will only match if all subpatterns match. There’s no need to manually handle each individual branch manually.
It is worth noting the symmetry between the construction and the deconstruction of the record values:
// construction
var r = new ColoredPoint(new Point2D(3, 4), Color.GREEN);
// deconstruction
if (r instanceof ColoredPoint(Point2D(int x, int y), Color c)) { }
Record patterns also support not just instanceof
but the Switch expression as well:
var length = switch (r) {
case ColoredPoint(Point2D(int x, int y), Color c) -> Math.sqrt(x*x + y*y);
case ColoredPoint(Point p, Color c) -> 0;
}
Resources:
- Data Oriented Programming in Java by Brian Goetz
Unnamed Patterns and Variables (Preview 🔍)
Available since: Preview in JDK 21
In Java 8, the compiler emitted a warning when ‘_’ was used as an identifier. Java 9 took this a step further making the underscore character illegal as an identifier, reserving this name to have special semantics in the future. The goal of these actions was to prepare the stage for unnamed variables, which is a preview feature in Java 21.
With unnamed variables, now it’s possible to use the underscore character wherever a variable name would be used, for example at variable declaration,
catch clause, or the parameter list of a lambda. However, it’s not a regular variable name: it means “don’t care” as underscore can be redeclared, and
can not be referenced. It does not pollute the scope with an unnecessary variable (potentially causing trouble and leakage), and never shadows any other variable.
Simply put, it enforces and communicates the intention of the programmer much better than the old alternatives, of using variable names such as ignore
.
var _ = mySet.add(x); // ignore the return value
try {
// ...
} catch (Exception _) { // ignore the exception object
// ...
}
list.stream()
.map((_) -> /* ... */) // ignore the parameter
.toList();
A related feature is called unnamed patterns, which can be used in pattern matching to ignore subpatterns.
Let’s say, we have the following pattern, where we don’t need the Color
information:
if (r instanceof ColoredPoint(Point(int x, int y), Color c)) {
// do something with x and y, but c is not needed
}
With var
, it’s possible to ignore the type of c
, but the match will make c
available in the scope:
if (r instanceof ColoredPoint(Point(int x, int y), var c)) {
// ... x ... y ...
}
Using underscore, marking it an unnamed pattern however ignores not just the type, but the name as well:
if (r instanceof ColoredPoint(Point(int x, int y), _)) {
// ... x ... y ...
}
Note, that underscore may only be used in a nested position, but not on the top level.
This feature is in preview 🔍, it has to be explicitly enabled with the --enable-preview
flag.
String Templates (Preview 🔍)
Available since: Preview in JDK 21
String Templates are an extension to the single-line String literals and Text Blocks, allowing String interpolation and much more.
In previous Java versions String interpolation was a lot of manual work. For example, one could do it with the +
operator or the StringBuilder
.
It’s also possible to do with String::format
but with that it is very easy to accidentally have the incorrect number of arguments.
The main use-case for String Templates is to make this use-case easier:
// string interpolation in Java
var name = "Duke";
var info = STR."My name is \{name}";
Let’s compare this with a similar feature, Template literals in JavaScript:
// string interpolation in JavaScript
var name = "Duke";
var info = `My name is ${name}`;
The most obvious difference is that the new syntax introduced in Java for string interpolation is not a new kind of literal (e.g. a backtick \
as in Javascript),
but the programmer must explicitly use a *template processor* —
STR` in this case.
The template processor is responsible including the expression results into the template, performing any validation and escaping if needed. The evaluation of the embedded expressions happens eagerly when the template is constructed. Templates can’t exist on their own, they can only be used in combination with a template processor.
STR
is a template processor instance is used for the string interpolation of a supplied template. It is a public static final field which is automatically imported to all java source files
for convenience.
In case of simple String interpolation with STR
, the processor returns a String, but it’s not a requirement. Other processors might emit objects of different types.
Also, for STR
not much validation and escaping happens, but other processors might choose to do something more complex.
Allowing the programmer to choose and implement template processors as they wish make this feature very flexible and robust.
For example, as mentioned in the JEP, it’s possible to easily create a JSON processor, that can be used to turn a templated String into a JSONObject
.
var JSON = StringTemplate.Processor.of(
(StringTemplate st) -> new JSONObject(st.interpolate())
);
JSONObject doc = JSON."""
{
"name": "\{name}",
"phone": "\{phone}",
"adress": "\{address}"
};
""";
Because the template processor has fine-grained access to the evaluation mechanism (like text fragments and embedded expressions), it’s also possible to create a processor to produce prepared statements, XMLs, or localization strings, performing all the necessary validation and escaping.
Note, that by convention the name of template processors are always written in uppercase, even in case of local variables. A processor may also throw a checked exception in which case the usual rules apply: the caller has to handle or propagate the exception.
In addition to STR
, there are a few more built-in processors.
One is FMT
, which is just like STR
but accepts format specifiers that appear left to the embedded expression as specified by java.util.Formatter
:
double value = 17.8D;
var result = FMT."Value is %7.2f\{value}";
// => "Value is 17.80"
Finally, there’s the RAW
processor which can be used to create StringTemplate
objects, to be processed later by an other processor:
StringTemplate template = RAW."Hello \{name}";
// ...
String result = STR.process(template);
Java uses backslash (\
) instead of the usual dollar sign ($
) for embedded expressions in templates for backwards compatibility reasons.
In previous Java versions "${...}"
is a valid String, however “{}” result in a compilation error, complaining about an illegal escape character.
By choosing a new symbol, it is guaranteed that no existing program will break with the addition of String Templates.
To summarize, Java aims to have a string interpolation that is a bit different from similar features in other languages, trading verboseness for flexibility and robustness, and for backwards compatibility.
This feature is in preview 🔍, it has to be explicitly enabled with the --enable-preview
flag.
Resources:
- Inside Java Podcast Episode 26: “Java 19 is Here!” with Brian Goetz and Ron Pressler
- Inside Java Podcast Episode 28: “Java Language - State of the Union” with Gavin Bierman
Unnamed Classes and Instance Main Methods (Preview 🔍)
Available since: Preview in JDK 21
In old Java versions, one needed write quite some boilerplate code even for the simplest of the applications:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
Usually, this only matters at project setup, but there are cases where it might be problematic:
- Newcomers to the language have to learn quite some extra keywords and concepts before they can try anything.
- It’s tedious to write small programs such as shell scripts.
The introduction of unnamed classes and instance main methods makes life a bit easier in these two cases.
Instance main methods makes the Java launch protocol more flexible, by making some aspects of the main
method optional.
From now on, visibility does not matter as long as the main
method is non-private, the String[]
parameter can also be ommitted,
and it can be an instance method.
Because it’s possible to define main
methods in multiple ways, the new launch protocol defines priorities to choose which one to use:
- static main with args
- static main without args
- instance main with args
- instance main without args
In case of the non-static main method, an instance of the enclosing class will be automatically created at startup. The introduction of instance main methods brings a slight breaking change: if there’s an instance main method present, then static main methods from superclasses won’t be considered. In these cases the JVM emits a warning.
class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}
This is already quite some improvement, but there’s more. Methods to exist outside of an enclosing class in which case they are automatically wrapped into a synthetic unnamed class.
Unnamed classes work similarly to unnamed packages and unnamed modules. If a class does not have a package
declaration, it will be part of the unnamed package, in which case they can not be referenced by classes from named packages. If a package is not part of a module, it will be part of the unnamed module, so packages from other modules can’t refer them.
Unnamed classes are the extension of this idea to one more micro level: when the source file has no explicit class declaration, then the contents will become a member of an unnamed class. The unnamed class is always a member of the unnamed package and the unnamed module. It is final, can not implement any interface or extend any class (other than Object), and can not be referenced. This also means that it’s not possible to create instances of an unnamed class, so it is only useful as an entry point to a program. For this reason the compiler even enforces that unnamed classes must have a main method. Otherwise, an unnamed class can be used as a regular class, for example it can have instance and static methods and fields.
So putting it together, the main
method can be written as follows:
void main() {
System.out.println("Hello, World!");
}
Which makes it very convenient to write short shell scripts:
(The warnings regarding the preview features sadly can not be turned off, but once this feature is production ready, it will go away.)
Resources and potential future work:
- Paving the on-ramp by Brian Goetz
- Script Java Easily in 21 and Beyond - Inside Java Newscast #49
- JEP draft: Launch Multi-File Source-Code Programs
This feature is in preview 🔍, it has to be explicitly enabled with the --enable-preview
flag.
Sealed Classes
Available since: JDK 17 (Preview in JDK 15 JDK 16)
Sealed classes and interfaces can be used to restrict which other classes or interfaces may extend or implement them. It gives a tool to better design public APIs, and provides an alternative to Enums to model fixed number of alternatives.
Older Java versions also provide some mechanisms to achieve a similar goal. Classes marked with the final
keyword can not be extended at all, and with
access modifiers it’s possible to ensure that classes are only extended by members of the same package.
On top of these existing facilities Sealed classes add a fine-grained approach, allowing the authors to explicitly list the subclasses.
public sealed class Shape
permits Circle, Quadrilateral {...}
In this case, it’s only permitted to extend the Shape
class with the Circle
and the Quadrilateral
classes.
Actually, the term permits might be a bit misleading, since it not only permits, but it is required that the listed classes directly extend the sealed class.
Additionally, as one might expect from such a grant, it’s a compile error if any other classes try to extend the sealed class.
Classes that extend a sealed class have to conform to a few rules.
Authors are forced to always explicitly define the boundaries of a