84. Introducing the canonical and compact constructors for records
In the previous problem, we created the MelonRecord Java record and we instantiated it via the following code:
MelonRecord melonr = new MelonRecord(“Cantaloupe”, 2600);
How is this possible (since we didn’t write any parameterized constructor in MelonRecord)? The compiler just followed its internal protocol for Java records and created a default constructor based on the components that we provided in the record declaration (in this case, there are two components, type and weight). This constructor is known as the canonical constructor and it is always aligned with the given components. Every record has a canonical constructor that represents the only way to create instances of that record.But, we can redefine the canonical constructor. Here is an explicit canonical constructor similar to the default one – as you can see, the canonical constructor simply takes all the given components and sets the corresponding instance fields (also generated by the compiler as private final fields):
public MelonRecord(String type, float weight) {
this.type = type;
this.weight = weight;
}
Once the instance is created it cannot be changed (it is immutable). It will serve the only purpose of carrying this data around your program. This explicit canonical constructor has a shortcut known as the compact constructor – this is specific to Java records. Since the compiler knows the list of given components it can accomplish its job from this compact constructor which is equivalent to the previous one:
public MelonRecord {}
Pay attention to not confuse this compact constructor with the one without arguments. The following snippets are not equivalent:
public MelonRecord {} // compact constructor
public MelonRecord() {} // constructor with no arguments
Of course, it doesn’t make sense to write an explicit canonical constructor just to mimic what the default one does. So, let’s examine several scenarios when we redefine the canonical constructor to make sense.
Handling validation
At this moment, when we create a MelonRecord, we can pass the type as null, or the melon’s weight as a negative number. This leads to corrupted records containing non-valid data. Validating the record components can be handled in an explicit canonical constructor as follows:
public record MelonRecord(String type, float weight) {
// explicit canonical constructor for validations
public MelonRecord(String type, int weight) {
if (type == null) {
throw new IllegalArgumentException(
“The melon’s type cannot be null”);
}
if (weight < 1000 || weight > 10000) {
throw new IllegalArgumentException(“The melon’s weight
must be between 1000 and 10000 grams”);
}
this.type = type;
this.weight = weight;
}
}
Or, via the compact constructor as follows:
public record MelonRecord(String type, float weight) {
// explicit compact constructor for validations
public MelonRecord {
if (type == null) {
throw new IllegalArgumentException(
“The melon’s type cannot be null”);
}
if (weight < 1000 || weight > 10000) {
throw new IllegalArgumentException(“The melon’s weight
must be between 1000 and 10000 grams”);
}
}
}
Validation handling is the most common use case for explicit canonical/compact constructors. Next, let’s see two more less-known use cases.