Java Constructors and Object Initialization
A canonical constructor takes parameters to initialize an object's fields, and it can be customized with additional code.
Guessing What a Constructor Does First
Every object in Java is created by a constructor call. When you write new followed by a type name and arguments, something has to take those arguments and put them into the object's fields. For many classes, that step is the only thing you need. Sometimes, though, an object should refuse to exist at all if the values passed in are wrong. A name that is missing, for example, may be a bug that you want to catch at the moment of creation rather than later, when the object is already in use.
What do you think happens?
If a constructor check fails and throws an exception during a new expression, is an object still handed back to the caller?
Reveal answer
No, the exception is thrown from inside the constructor before the object is handed back
The source describes the constructor body as the place where extra work happens, and the object is only handed back after that work finishes. If the body throws, the new expression does not complete normally, so the caller never receives an object. The fields may already hold their values, but the caller gets no object reference.
The Default Canonical Constructor
A record gets a constructor for free. This default constructor is called the canonical constructor. It takes one parameter for each record component, and it uses those parameters to set the fields. If a record has the components firstName and lastName, the default canonical constructor takes two parameters and assigns each one to the field with the same name. This is often exactly what you want, because the record then behaves like a simple container for its values.
The limitation is that the default constructor only copies values. It has no way to look at them and decide that they are unacceptable. Once the record is built, any values the caller passed in are accepted. Customizing the canonical constructor exists to fill that gap.
Following One Construction Step by Step
Before writing any custom code, it helps to see the order in which the pieces of a construction run. The sequence below follows one call. The caller supplies the values. Those values fill the fields. The constructor body then runs its extra work. Only after that is the object handed back to the caller.
The important detail is the position of the body in this sequence. The parameters are used first to fill the fields. The constructor's own code runs after that. This means that when the body checks a value, it is checking the value the caller supplied, and the field already holds that value. The body is the only place where you can enforce rules about what the fields contain, and it runs once the fields have been filled.
Writing a Customized Canonical Constructor
To customize the canonical constructor, you write a constructor in the record body that has the record's name and no parameter list. The block after the name holds the extra code that should run during construction. The parameter list is missing, but the parameters still exist. A call such as new FullName("Ada", "Lovelace") still supplies two arguments, and those arguments still initialize firstName and lastName. What you add is only the block of code that runs in addition to that initialization.
- The record header declares the two components, which become the canonical constructor parameters.
- public FullName with no parameter list marks this as the customized canonical constructor.
- The if statement is the validation. It tests the
firstNamevalue the caller passed in. - The throw statement stops construction when the check fails.
Tracing Both Calls in main
Consider a main method that makes two calls to the same constructor. The first call passes null as the first name. The second call changes only that first argument to "Ada". The fields are initialized the same way in both calls, but the test is different, so the two calls take different paths through the same code.
Exception in thread "main" java.lang.IllegalArgumentException: firstName must not be null- The call passes null, so the parameter
firstNameholds null when construction starts. - The check inside the constructor body is true, so the throw statement runs.
- The println line never runs, because the new expression did not finish.
FullName[firstName=Ada, lastName=Lovelace]- Only the first argument differs from the failing call.
- The check is false, so the throw statement is skipped.
- The constructor completes normally and the object is handed back.
Both calls go through the same parameter-to-field step. Only the check at the end of the body separates them. In the failing call, the fields have already received null before the throw happens. The object is never handed back, so nothing in main can use it.
Locating a Validation Failure
When a customized constructor throws, the failure is located at the construction call and inside the constructor body. It is not located at some later line where the object is used. That is the practical benefit of validating during construction: the error appears where the bad value was supplied, with the constructor's check responsible for it.
Reading the stack from the top, the throw statement sits inside the constructor body, and the constructor body was entered from the new expression in main. The line that caused the problem is the new expression in main, which supplied the bad argument. The line that detected the problem is the check in the constructor body. When you read an exception report, both locations are worth checking.
Common Mistakes With Custom Constructors
Common mistake Assuming that omitting the parameter list removes the parameters
What it looks likepublic FullName { } is written, and the author expects new FullName() with no arguments to compile.Why it failsThe parameters still exist and still belong to the call. Omitting the list only removes the written declaration, so the call still needs two arguments.
Do this insteadKeep passing both arguments to new FullName(...), and write only the extra code inside the block.
Common mistake Expecting the body to check values before the fields are filled
What it looks likeA check is written that assumes the field is still empty when the body starts.
Why it failsThe parameters initialize the fields before the constructor's own code runs, so the body sees the values that were passed in.
Do this insteadTest the component name, such as firstName, and expect it to hold the value the caller supplied.
Common mistake Expecting an object after a failed check
What it looks likeFullName name = new FullName(null, "Lovelace"); is followed by code that uses name, on the belief that a broken object was still created.
Why it failsWhen the constructor throws, the new expression does not finish, so no object is handed back.
Do this insteadLet the exception propagate, or handle it around the construction call, rather than using the variable afterward.
Common mistake Writing the validation as a separate method that the caller must remember to call
Adding a Second Check
Extend the FullName customized canonical constructor so that it also rejects a null lastName. Write the new check, choose a message, and predict whether new FullName("Ada", null) will throw. Then explain which line in the constructor body would report the failure.
Hints
- Add a second if statement inside the same body, next to the first.
- Remember that the body sees
lastNameas the value the caller passed in. - The failing line for the exception is the throw statement inside the constructor body, not the println in main.
Key Takeaways
- A record's default canonical constructor takes one parameter per component and uses those parameters to set the fields.
- Customizing the canonical constructor means writing a body without a parameter list; the parameters still exist and still initialize the fields.
- The parameters fill the fields before the constructor body runs, so validation in the body checks the values the caller supplied.
- When a validation check throws, the failure is located at the construction call and inside the constructor body, and no object is handed back.