IntelliJ IDEA
IntelliJ IDEA – the Leading Java and Kotlin IDE, by JetBrains
Constructor Makeover in Java 22
Typically, we create alternative solutions for tasks that are necessary, but not officially permitted. For instance, executing statements before super()
in a derived class constructor was not officially allowed, even though it was important for, say, validating values being passed to the base class constructor. A popular workaround involved creating static methods to validate values and then calling these methods on the arguments of super()
. Though this approach worked well, it could make the code look complicated. This is changing with Statements before super(), a preview language feature in Java 22.
By using this feature, you can opt for a more direct approach, that is, drop the workaround of creating static methods, and execute code that validates arguments, just before calling super()
. Terms and conditions still apply, such as, not accessing instance members of a derived class before execution of super()
completes.
I know you are wondering, why was this constraint added in the first place, how is this being resolved, does it change the internal JVM working, and the most important question – how can you benefit from this feature? I’ll answer all these questions in this blog post.
Let’s start with an example.
Example 1 – validating values passed to super() in a derived class constructor
Imagine you need to create a class, say, IndustryElement
, that extends class Element
, which is defined as follows:
public class Element { int atomicNumber; Color color; public Element(int atomicNumber, Color color) { if (color == null) throw new IllegalArgumentException("color is null"); this.atomicNumber = atomicNumber; this.color = color; } // rest of the code }
The constructor of the class Element
misses checking if the atomicNumber
is in the range of 1-118 (all known elements have atomic numbers between 1 to 118). Often the source code of a base class is not accessible or open for modification. How would you validate the values passed to atomicNumber
in the constructor of class IndustryElement
?
Until Java 21, no statements were allowed to execute before super()
. Here’s one of the ways we developers found a workaround by defining and calling static methods (static methods belong to a class and not to instances and can be executed before any instance of a class exists):
public class IndustryElement extends Element{ private static final int MIN_ATOMIC_NUMBER = 1; private static final int MAX_ATOMIC_NUMBER = 118; public IndustryElement(int atomicNumber, Color color) { super(checkRange(atomicNumber, MIN_ATOMIC_NUMBER , MAX_ATOMIC_NUMBER), color); } private static int checkRange(int value, int lowerBound, int upperBound) { if (value < lowerBound || value > upperBound) throw new IllegalArgumentException("Atomic number out of range"); return value; } }
Starting Java 22, you could inline the contents of your static method in the constructor for your derived class, as shown in the following gif:
Here’s the resultant code for your reference:
public class IndustryElement extends Element{ private static final int MIN_ATOMIC_NUMBER = 1; private static final int MAX_ATOMIC_NUMBER = 118; public IndustryElement(int atomicNumber, Color color) { if (atomicNumber < MIN_ATOMIC_NUMBER || atomicNumber > MAX_ATOMIC_NUMBER) throw new IllegalArgumentException("Atomic number out of range"); super(atomicNumber, color); } }
I know you have questions about the preceding code, but before we move forward, let me share the IntelliJ IDEA Project Configuration you’d need for working with this feature in IntelliJ IDEA. If you are already familiar with it, skip the next section.
IntelliJ IDEA Configuration
Java 22 support is available in IntelliJ IDEA 2024.1 EAP. The final release of this version is planned for March 2024.
In your Project Settings, set the SDK to Java 22. For the language level, select ‘22 (Preview) – Statements before super(), string templates (2nd preview etc.)’ on both the Project and Modules tab, as shown in the below settings screenshot:
Why do you care about this feature?
Often folks ask – isn’t it better to enclose the validation logic in a separate method since it makes the code easier to read and understand? For example, in the previous example, wouldn’t it be better to keep the validation logic in a separate method? My opinion – though it is a good question, it is not what this feature is trying to address.
You could call the static method checkRange, before calling super()
in the constructor of class IndustryElement
as follows (the validation code is still delegated to another method):
public class IndustryElement extends Element{ private static final int MIN_ATOMIC_NUMBER = 1; private static final int MAX_ATOMIC_NUMBER = 118; public IndustryElement(int atomicNumber, Color color) { checkRange(atomicNumber, MIN_ATOMIC_NUMBER , MAX_ATOMIC_NUMBER); super(atomicNumber, color); } private static int checkRange(int value, int lowerBound, int upperBound) { if (value < lowerBound || value > upperBound) throw new IllegalArgumentException("Atomic number out of range"); return value; } }
I believe one of the real benefits here is that you have a choice. You are free to define code in the way you see it, that is, validate the arguments, if there are no issues, call the super class constructor.
You might argue that one could call the super class constructor first, and validate its arguments later. I would never recommend it. It could even lead to technical debts. What if the validation code finds issues but logs an error message, instead of terminating the initialisation process? This is poor coding practice.
Why was execution of statements before super not allowed in the past?
Compare a base class with a seed and the derived class with a plant that is formed using it. A seed needs to be fully formed, before any part of the plan or its processes can be accessed. Similarly, the constructor of a base class must execute completely, before any member variables or methods of a derived class can be called. So with this constraint, the Java platform was trying to protect the state of your instances.
Also, if you do not explicitly call the constructor of a base class in a derived class constructor, the compiler inserts a call to the implicit, no-argument, base class constructor. In short, at least one base class constructor always gets executed for a derived class, whether you do that explicitly or not. This was the case since Java 1.0.
How does it work behind the scenes?
The language syntax has been relaxed but it doesn’t change or impact the internal JVM instructions. There are no changes to the JVM instructions for this new feature because the order of execution of the constructors still remains unchanged – from base class to a derived class. Also, this feature still doesn’t allow you to use members of a derived class instance, until
super()
executes.
Let’s access and compare the instruction set of the constructor of class IndustryElement
, before and after its modification – one that can execute statements before super()
and the one that doesn’t. To do so, use the following command:
javap -c IndustryElement.class
Here’s the instruction set for the constructor that doesn’t explicitly execute statements before super()
and calls static methods to validate range of atomic number:
Here’s instruction set for the constructor that explicitly executes statements before super() to validate range of atomic number:
The most important point to note here is that in both the cases, the constructor of the base class, that is, Element
is called, after the execution of all other statements. Essentially, it means, you are still doing the same thing, it is just packaged in a way that makes things easier for you.
I understand it is difficult to remember what each of these instruction codes means. Access the following link and search for the instruction code and following the above instructions set would be a breeze:
https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-6.html#jvms-6.5.aload_n
Can you execute ‘any’ statements before calling super()?
No. If the statements before super()
try to access instance variables or execute methods of your derived class, your code won’t compile. For example, if you change the static checkRange()
method to an instance method, your code won’t compile, as shown below:
Before everythings gets boring, lets work with another example 🙂
Example 2 – base class constructor parameters that use annotations for validations
Assume that constructor of base class Element checks for a non null value being passed to its parameter via
@NotNull
annotation as follows:
public class Element { int atomicNumber; Color color; public Element(int atomicNumber, @NotNull Color color) { this.atomicNumber = atomicNumber; this.color = color; } // rest of the code }
Annotations can give a false sense of security. Even though it might seem that this constructor wouldn’t accept a null value for the parameter color
, annotations can be disabled at runtime. So you must include code that checks for null safety in the derived class constructor, as follows:
public class IndustryElement extends Element{ private static final int MIN_ATOMIC_NUMBER = 1; private static final int MAX_ATOMIC_NUMBER = 118; public IndustryElement(int atomicNumber, Color color) { Objects.requireNonNull(color, "color can't be null"); if (atomicNumber < MIN_ATOMIC_NUMBER || atomicNumber > MAX_ATOMIC_NUMBER) throw new IllegalArgumentException("Atomic number out of range"); super(atomicNumber, color); } }
Example 3 – Transforming variable values received in a derived class constructor, before calling a base class constructor.
Our applications often receive data in multiple and varied formats, and may not be in the format we want to store or use that data. Easy solution-we transform the values to the ones that we require. However, handling the transformation is easier with methods other than the constructors.
Imagine the constructor of the derived class IndustryElement
receives the following JSON as a String value:
{ "atomicNumber": 6, "color": { "r": 255, "g": 128, "b": 0 } }
Or as a Comma Separated value, as follows:
6,255,128,0
There could be other formats too, say XML. The idea is that the data that is received by the constructor of a derived class can’t be passed to the constructor of a base class in its original form. It must be transformed.
Before this new feature that allowed statements before super()
, you would have to define the constructor of the derived class IndustryElement
as follows:
public IndustryElement(String elementAsCSV) throws IOException { super(getAtomicNumberFromCSV(elementAsCSV), getColorFromCSV(elementAsCSV)); } public static int getAtomicNumberFromCSV(String csv) { Objects.requireNonNull(csv, "CSV value can't be null"); String[] parts = csv.split(","); int atomicNumber = Integer.parseInt(parts[0]); return atomicNumber; } public static Color getColorFromCSV(String csv) { Objects.requireNonNull(csv, "CSV value can't be null"); String[] parts = csv.split(","); int r = Integer.parseInt(parts[1]); int g = Integer.parseInt(parts[2]); int b = Integer.parseInt(parts[3]); return new Color(r, g, b); }
With this new feature, you can simplify the preceding code as follows:
public IndustryElement(String elementAsCSV) throws IOException { Objects.requireNonNull(elementAsCSV, "CSV value can't be null"); String[] parts = elementAsCSV.split(","); int atomicNumber = Integer.parseInt(parts[0]); int r = Integer.parseInt(parts[1]); int g = Integer.parseInt(parts[2]); int b = Integer.parseInt(parts[3]); super(atomicNumber, new Color(r, g, b)); }
Example 4 – Executing statements before this() in constructor of Records
Records, as we know can’t extend other records or classes, because they implicitly extend
java.lang.Record
class. Records can benefit from this feature too by executing statements before calling this()
in their constructors.
For example, here’s the code of a record Palette2Colors
that represents a color palette with only two colors. For a specific business logic, the constructor uses either a brighter or a darker shade of the color value passed to its constructor:
public record Palette2Colors (String name, Color color1, Color color2) { public Palette2Colors(String name, Color color1, Color color2) { this.name = name.toUpperCase(); this.color1 = color1.brighter(); this.color2 = color2.darker(); } }
Imagine when you start using this record, you find multiple use cases where you have to pass an array of colors to this record instead of individual Color instances. You would prefer calling the other constructor since it has additional transformation logic.
Here’s how you might create this additional constructor (before Java 22):
public Palette2Colors(String name, Color[] colors) { this(Objects.requireNonNull(name), Objects.requireNonNull(colors, "Color arr shouldn't be null")[0], Objects.requireNonNull(colors[1])); }
Though the preceding code is valid, this line of code Objects.requireNonNull(colors, "Color arr shouldn't be null")[0]
, it is a bit difficult to read. Records can benefit from this feature too, and your could define the preceding constructor as follows (with Java 22):
public Palette2Colors(String name, Color[] colors) { Objects.requireNonNull(name); Objects.requireNonNull(colors, "Color arr shouldn't be null"); Objects.requireNonNull(colors[0], "Color[0] shouldn't be null"); Objects.requireNonNull(colors[1], "Color arr shouldn't be null"); this(name, colors[0], colors[1]); }
You might be wondering you could have inlined the code defined in the other constructor in the preceding constructor as follows:
public Palette2Colors(String name, Color[] colors) { Objects.requireNonNull(name); Objects.requireNonNull(colors, "Color arr shouldn't be null"); Objects.requireNonNull(colors[0], "Color[0] shouldn't be null"); Objects.requireNonNull(colors[1], "Color arr shouldn't be null"); this.name = name.toUpperCase(); this.color1 = color1.brighter(); this.color2 = color2.darker(); }
However, this would result in code repetition across constructors, which is never good. What happens if the business logic no longer needs to assign a lighter or darker shade of the color values passed as method arguments, and this logic is changed in just one constructor? Perfect place for bugs to hide!
Example 5 – Executing statements before this() in Enum constructors
Similar to a record, you could execute statements before this() in Enum constructors too. Here is an example of an enum, say,
BrightColor
, that defines two constructors:
public enum BrightColor { MAGENTA(255, 0, 255), CYAN(0, 255, 255), YELLOW(255, 255, 0), GREEN(Color.GREEN); private int r; private int g; private int b; private BrightColor(int r, int g, int b) { this.r = r; this.g = g; this.b = b; } private BrightColor(Color color) { Objects.requireNonNull(color); this(color.getRed(), color.getGreen(), color.getBlue()); } //.. rest of the code }
Example 6 – Executing statements before this() in classes
Starting Java 22, you could also execute statements before
this()
in a class constructor, unless it doesn’t call other instance members of the class. Below is an example:
public class Voter{ String name; int age; public Voter(String name, int age) { Objects.requireNonNull(name); if (age < 18) throw new IllegalArgumentException(); this.name = name; this.age = age; } public Voter(String firstName, String lastName, int age) { Objects.requireNonNull(firstName); Objects.requireNonNull(lastName); this(STR."\{firstName} \{lastName}", age); } // ..rest of the code }
The benefits of this approach is similar to what we covered in this blog post. The second constructor of class Voter, the one that accepts three method parameters can validate the parameters and then call this()
which includes the validation and initialization code.
Preview Features
Statements before super() is being introduced as a preview language feature in Java 22. With Java’s new release cadence of six months, new language features are released as preview features. They may be reintroduced in later Java versions in the second or more preview, with or without changes. Once they are stable enough, they may be added to Java as a standard language feature.
Preview language features are complete but not permanent, which essentially means that these features are ready to be used by developers, although their finer details could change in future Java releases depending on developer feedback. Unlike an API, language features can’t be deprecated in the future. So, if you have feedback about any of the preview language features, feel free to share it on the JDK mailing list (free registration required).
Because of how these features work, IntelliJ IDEA is committed to only supporting preview features for the current JDK. Preview language features can change across Java versions, until they are dropped or added as a standard language feature. Code that uses a preview language feature from an older release of the Java SE Platform might not compile or run on a newer release.
Summary
In Java 22, the feature Statements before super() lets you execute code before calling
super()
in your derived class constructors, this() in your records or enums, so that you could validate the method parameters, or transform values, as required. This avoids creating workarounds like creating static methods and makes your code easier to read and understand.
This feature doesn’t change how constructors would operate now vs. how they operated earlier – the JVM instructions remain the same.
The relaxation of this syntax rule starting Java 22 still ensures that super()
completes before your access any members of a derived class. Your code wouldn’t compile if you try otherwise.