By a show of hands, who occasionally puts code before the super(...)
or this(...)
call when writing a constructor, only to be yelled at by the compiler?
class Name {
private final String first;
private final String last;
Name(String first, String last) {
this.first = first;
this.last = last;
}
// Version A - doesn't work on JDK ≤21
Name(String full) {
var names = full.split(" ");
this(names[0], names[1]);
}
// Version B - works but repeats code
Name(String full) {
this(full.split(" ")[0], full.split(" ")[1]);
}
}
Just me?
Oops.
But surely you occasionally want to put code there?
My favorite scenario to get angry over is when a constructor that is meant to ease use of a class needs to split an argument in two and then pass each part on to the "real" constructor.
Trying to put that logic into code feels like playing golf with a bowling ball.
But whether it's for such more "complicated" uses or just to validate or prepare arguments, it would be really handy to be able to start a constructor with some code before calling a super constructor with super(...)
or another one from the same class with this(...)
.
Lucky us, or still just me?
(Just you!)
(That actually sounds interesting, let's hear him out.)
So, lucky us, then, Java 22 will preview exactly that.
▚Intro
Welcome everyone, to the Inside Java Newscast, where we cover recent developments in the OpenJDK community. I'm Nicolai Parlog, Java Developer Advocate at Oracle, and today we'll have a look at JDK Enhancement Proposal 447, which is integrated into JDK 22, so come March, you'll be able to use it as a preview feature. Let's divehhh... I can't with those two guys staring at me disapprovingly. Also, the weather is really great, so why not go outside and check in on those cows. Let's go!
(Yes, the whole setup with the milk and with the hike was just to show you these two cows. There're usually way more here on that field.)
▚Constructor Chaining
Before we get into what's new we should recap how things stand today when it comes to constructor chaining. Maybe we'll start right there: Constructor chaining is the technique of calling one constructor from another, often in a chain until all calls land in the same canonical constructor, but that last part is not required.
class Name {
private final String first;
private final String last;
Name(String first, String last) {
this.first = first;
this.last = last;
}
Name(String last) {
this("", last);
}
Name() {
this("");
}
}
In the case of class inheritance, chaining is enforced:
A subclass constructor must call a superclass constructor with a super($ARGUMENTS)
statement.
You don't always have to type that out, though - if the superclass has a parameterless constructor, the compiler will let you get away without calling it explicitly and will slip it into the generated bytecode.
Note that this applies to all classes we write because, if nothing else, they still extend Object
.
You can also call constructors from the same class with a this($ARGUMENTS)
statement.
And while records actually require that, regular classes don't.
In that case, every constructor can decide whether it wants to assign fields itself or forward to another constructor.
For what it's worth, I prefer the latter, but due to the limitations we'll come to momentarily, that's not always feasible.
class Name {
private final String first;
private final String last;
// canonical constructor, assigns fields itself
Name(String first, String last) {
this.first = first;
this.last = last;
}
// Version A: forwards to the canonical constructor
Name(String last) {
this("", last);
}
// Version 2: assigns fields itself
Name(String last) {
this.first = "";
this.last = last;
}
}
By the way, I don't always want to say "super(...)
or this(...)
statement", so I'll switch to the technical term, which is "explicit constructor invocation".
Actually, I'll cut that even shorter to the less precise "constructor invocation".
▚No Statements Before Constructor Invocation
So what code can be executed before a constructor invocation?
Because you can't put any statements before it, you might be tempted to say "none" but it's not actually that bad.
You can use expressions for the invoked constructor's arguments as long as they don't touch instance members of the object being constructed.
In practice that often means having a small Stream
or Optional
pipeline or calling static methods like Objects::requireNonNull
.
Those expressions may validate arguments before passing them on, or they may process them and pass on derived values. As long as there's a clear 1:1 relationship between the received and the passed arguments, not being able to write statements before the constructor invocation can be annoying but easily worked around by calling dedicated static methods. And if those methods do something non-trivial and have a good name, you could even argue it's cleaner than putting everything into the constructor. But once you need to split arguments or create shared instances, it really gets nasty. Then you're suddenly knee-deep in inner classes and auxiliary constructors.
class Name {
private final String first;
private final String last;
Name(String first, String last) {
this.first = first;
this.last = last;
}
// a constructor-based solution
// that avoids two splits
Name(String full) {
this(full.split(" "));
}
private Name(String[] names) {
this(names[0], names[1]);
}
}
We'll see later how much better that works with JEP 447.
But why are the rules so strict?
It's most obvious for super(...)
calls:
Subclasses depend on the state of their superclass, for example when calling methods or accessing fields, and to make sure that works out during construction, the superclass must be initialized before the subclass does anything.
And the easiest way to enforce that no-access-before-initialization rule is to enforce a strict top-down execution of constructors, which means no statements before calling super(...)
.
It's less obvious for this(...)
calls but boils down to the same reason.
If a constructor could have statements before calling another with this
and that one... this one... the other one would call super
(which, remember it must do eventually), we'd still have statements in the subclass before the superclass constructor gets executed.
So no statements before calling this(...)
either.
▚JDK Enhancement Proposal 447
After explaining all that, JEP 447 comes to a conclusion:
If the Java language could guarantee top-down construction and no-access-before-initialization with more flexible rules then code would be easier to write and easier to maintain. [...] We need to move beyond the simplistic syntactic requirements enforced since Java 1.0, that is, "super(..) or this(..) must be the first statement", "no use of this", and so forth.
The JEP formulates those more flexible syntactic requirements by conceptually splitting the constructor into three blocks:
- the prologue contains the statements before the explicit constructor invocation
- then comes the invocation itself
- and finally, the remaining statements are called the epilogue
The invocation and the epilogue already exist today and nothing changes for them. The prologue is new, though, the compiler used to accept no statements there at all. That changes now - you can have a non-empty prologue that does all kinds of things.
But it's no free for all. As a rule of thumb, assume you're in a static context, meaning you can do everything in a prologue that you could do in a static method. It's actually a bit more than that - the JEP proposes a new pre-construction context - but the exact rules for what statements are allowed are a little complicated and even the JEP doesn't explicitly list them, so I'm not going to go into them here (which is code for "I didn't look them up").
JEP 447 proposes to allow, as a preview language feature, statements that comply with this pre-construction context before explicit constructor invocation. And as I mentioned, this is a preview feature in JDK 22.
▚Prologue Benefits
So with that feature in play, we can now do a number of common tasks more simply.
To validate arguments, we no longer have to push the validation logic into a static method that gets called in the constructor invocation, although in many situations that may still be the way to go for succinctness.
class ThreePartName extends Name {
private final String middle;
// JDK ≤21
ThreePartName(String first,
String middle,
String last) {
// can't have a middle name
// without first name
super(
requireNonNullNonEmpty(first),
last);
this.middle = middle;
}
// JDK 22 + PREVIEW FEATURES
ThreePartName(String first,
String middle,
String last) {
// can't have a middle name
// without first name
requireNonNullNonEmpty(first);
super(first, last);
this.middle = middle;
}
}
Likewise, preparing arguments like stripping a string or rearranging a collection can be done in the prologue now, but in some cases may still be embedded in the constructor invocation as long as the code remains readable.
class ThreePartName extends Name {
private final String middle;
// JDK ≤21
ThreePartName(String first,
String middle,
String last) {
// shorten first if middle is given
super(middle.length() == 1
? first.substring(0, 1)
: first,
last);
this.middle = middle;
}
// JDK 22 + PREVIEW FEATURES
ThreePartName(String first,
String middle,
String last) {
// shorten first if middle is given
var short1st = middle.length() == 1
? first.substring(0, 1)
: first;
super(short1st, last);
this.middle = middle;
}
}
Where the prologue really shines is with sharing and splitting arguments, though. No longer is it necessary to add inner classes or auxiliary constructors just to get those things done.
class ThreePartName extends Name {
private final String middle;
// JDK ≤21
ThreePartName(String full) {
// split "first middle last"
// on space (three times! 🤦🏾♂️)
super(
full.split(" ")[0],
full.split(" ")[2]);
this.middle = full.split(" ")[1];
}
// JDK 22 + PREVIEW FEATURES
ThreePartName(String full) {
// split "first middle last"
// on space (once 🙌🏾)
var names = full.split(" ");
super(names[0], names[2]);
this.middle = names[1];
}
}
So, yeah, definitely not a game changer but once again a nice little quality of life improvement. And as is so often the case, it comes with a chance to better understand Java and the reasons behind its limitations, which I often find just as interesting as the change itself. And since you're still here, I reckon you're feeling the same. Thanks for watching this video all the way through and for liking and subscribing. I'll see you again at Jfokus or otherwise right here on the Java YouTUbe channel in two weeks. So long...
Hah, I found them! See, there are more than two after all, I didn't imagine them.