Stranger Things in Java: Constructors
An insight about Java constructors...
Join the DZone community and get the full member experience.Join For Free
What you are reading is the second in a series of articles titled “Stranger things in Java” (find the first article here), inspired by the contents of my book “Java for Aliens”. These articles are dedicated to insights of the Java language. Deepening the topics we use every day, will allow us to master Java coding even in the strangest scenario.
In this post, we will explore some scenarios in which the use of a Java programming base concept, such as the constructor, can hide some pitfalls. In particular, after having clarified some fundamental definitions, we will explore the relationships between constructors and inheritance, constructors and polymorphism, and the hidden work of the compiler.
Definition and Properties
We already know that a constructor is considered a special method, which by its nature, is especially suitable for the initialization of instance variables. A simple example of a constructor is the one contained within the following class:
Note that, while methods are invoked using the dot operator (the "
The constructors are invoked through the
new operator :
In particular, unlike an ordinary method, a constructor has the following characteristics:
- has the same name as the class to which it belongs;
- has no return type;
- is called automatically (and only) whenever an object of the class to which it belongs is instantiated, for that object;
- is present in every class.
The last point is clarified in the next section.
There is always a constructor in every compiled class. In fact, even if a class does not declare a constructor explicitly, the compiler will automatically add the so-called default constructor within the bytecode. The default constructor contains only one statement which we will discuss shortly, and also has no parameters. But an explicitly declared parameterless constructor is not a default constructor. For example, the constructor in the following class is not a default constructor, but simply a parameterless constructor:
The compiler in this case will not add a default constructor, because an explicit constructor in this class already exists. Note that the compiler's decision does not depend on the number of parameters in the constructor, but only on whether there is an explicit constructor or not. For example, no default constructor would be added, even in the case of the first example of this article. In fact, the compiler adds the default constructor, only to allow the class to be instantiated using the new operator. In this way, it is possible to learn object-oriented programming without knowing the constructor concept and make the learning curve less steep in the early days.
Inheritance and Constructors
Inheritance is not applicable to constructors. Even when they are declared public, constructors are not inherited for a very simple reason: their own name. For example, let’s consider the following
JavaBook class extension of the
If the constructors were to be inherited, the
JavaBook class would inherit a constructor called
Book. In practice it is as if the
JavaBook class were written in the following way:
But a constructor called
Book in a class called
JavaBook can never be invoked! In fact, to instantiate an object from the
JavaBook class, you must necessarily call a constructor called
JavaBook using the
For example, if we want to instantiate a
JavaBook object we will write:
invoking the constructor of the
JavaBook class with the
Now suppose we inherited the
Book constructor in the
JavaBook class. In order to use this constructor, we should be able to write the following statement:
which, however, implies the creation of an object of the
Book class, and not of an object of the
JavaBook class, also resulting in a compilation error, because it is not possible to assign a
JavaBook type reference to a
Book type object. In fact, follows the output:
Constructors and Paradigms
The fact that constructors are not inherited from subclasses, is consistent with the syntax of the language, but at the same time contradicts the principles of object-oriented programming. In particular, the paradigms of reuse and abstraction seem to be violated. In fact, when the developer decided to implement the inheritance mechanism, he had to test its validity using the so-called is a relationship. To the question: "can an object instantiated by the candidate subclass also be considered an object of the candidate superclass?" he has indeed answered affirmatively.
In this particular case, a Java book is a book, and therefore it must have all the characteristics of a book. In particular, the constructor must also be reused. Not being able to inherit it, however, abstraction seems to have been violated. Instead, it is precisely in such a situation that Java demonstrates its consistency. In fact, we can add another property to the constructor definition:
- any constructor (even the default constructor), always invokes a superclass constructor in its first statement.
For example, let’s add some convenience constructors to the
The reader, having learned that the constructors are not inherited, should conclude that the instance of a
JavaBook, through a syntax of the type:
would output the following string:
The output instead will be:
In fact, the
JavaBook constructor first invokes the constructor of the superclass
Book and then it executes. The mandatory call to a superclass constructor is made using the
super keyword, which is introduced below.
super Keyword and Compiler Hidden Work
this keyword represents an implicit reference to the current object, we can define the
super keyword as an implicit reference to the intersection between the current object and its superclass. This reference allows us to access the components of the superclass, and in particular its constructor.
In fact, in every constructor, there is always a call to the constructor of the superclass, through a special syntax that uses the
super keyword, which is used as a method to pass parameters to. Again, if the instruction is not explicitly present, it will be added in the bytecode at compile time. For example, in the
JavaBook class, the constructor will be modified by the compiler as follows:
That is why the (parameterless) constructor of the
Book class is invoked by the constructor of the
JavaBook class. We can make the call to
super explicit, but if we don't, the compiler will implicitly add
this statement. The call to a superclass constructor is therefore inevitable.
Note that the constructor of the
Book class will also call the constructor of its superclass
Object, by means of a
super statement added implicitly at compile time.
Now suppose we want to modify the superclass
Book as we saw in the first example of this article:
This class can compile successfully, but its
JavaBook subclass no longer:
So, modifying the superclass, we introduce an error in the subclass. By interpreting the compiler's error message, however, one can guess what happened: it all depends on the hidden work of the compiler.
In fact, the subclass constructor, through the implicit
super() instruction, tried to invoke a constructor of the superclass that does not exist: the constructor without parameters. In fact, since we have explicitly added a constructor in the
Book class, the compiler's default constructor will no longer be implicitly added in it. The compiler, however, has inserted a call to the constructor without parameters of the superclass
Book, as the first statement of the constructor of the subclass
JavaBook (see code). But now the parameterless constructor (the one that was the default constructor) in the
Book superclass is gone.
To fix the problem, the best solution seems to modify the
JavaBook class as follows:
With the previous syntax, we have explicitly invoked the superclass constructor which takes a string as input.
If we had multiple constructors in the superclass, we could choose which one to call. For example, if the
Book class defined multiple constructors as follows:
JavaBook class could call the most suitable constructor depending on the case:
Overload, Constructors, and the this Keyword
We now know that we can invoke a constructor of a superclass using the
super keyword, but actually we can also use the
this keyword to invoke the constructors of the same class. Here is a simple example of a constructor overloading that uses the this keyword as a call to a constructor method of the same class:
Note how the constructors invoke each other avoiding duplicating already written code.
In this way the reuse paradigm will be favored.
Clarification on How Constructors Call Each Other
The call via the
super keyword to a superclass constructor or through the
this keyword to a constructor of the same class, can only be used as the first instruction in a constructor. This implies that only one instruction between
this will be present in a constructor. If we explicitly add the
this statement, then the
super statement cannot be placed in the same constructor. However, note that if a constructor calls another constructor using the
this command, this other constructor, in turn, either calls a third constructor via another
this statement, or calls a superclass constructor with the
super statement. In short, the superclass constructor will be called in any case sooner or later. For example, the following class that represents a character (font) to be used in an editor:
declares a first constructor that invokes the second with the
this() statement. In the second constructor, however, the compiler explicitly adds the
super() statement as the first statement. The call to a superclass constructor (which in this case is the
Object class) has only been postponed.
We cannot even call a constructor using the
this keywords from an ordinary method. Only constructor can use these instructions.
Override and Constructors
We know that there is no constructor inheritance, but the instantiation of a subclass, thanks to the
super statement, will always cause a call to a superclass constructor. In any case, since there is no inheritance, it makes no sense to speak of override of constructors, or constructor polymorphism. But there is a situation that concerns the constructors and the order in which they are executed with respect to the initialization of the attributes, to be taken into account, and which we will explain below.
Since the scenario is a bit complex to explain in the abstract, let's get into the context of a code example. Let's consider the following abstract class
This class declares a constructor that invokes the
doWork method defined within the same class. Now consider the following
Hammer class extending
Tool and overriding the
Finally, consider the following class that instantiates the
These classes are compiled correctly, but by launching the
ToolsTest class, we will get the following output at runtime:
in fact, the
data variable is used before it can be initialized in the subclass, and therefore still has a
null value. In particular, when we invoked the constructor of the
Hammer class, as the first (implicit) instruction, the constructor of the
Tool superclass was invoked, which in turn directly invokes the
doWork method, which prints the message when the
data variable had not yet been initialized.
In this case, we only got an incorrect output, but actually, the use of references with
null values can easily lead to the runtime being interrupted by
NullPointerException type exceptions. For example, if we modify the
doWork method in the subclass as follows:
then the program would print the following output:
In this article, we have seen how a basic argument of the language such as constructors actually hides even complex scenarios. As usual, having a solid theoretical basis will allow us to manage all use cases without surprises. With the keywords
super, we will be able to correctly manage reuse and abstraction paradigms even with constructors, although they are not inherited and consequently it is not possible to rewrite them in subclasses. We have also seen how important it is to know the compiler behavior. In fact, although the insertions of implicit code in the bytecode have been designed to facilitate learning and reduce the verbosity of the language, actually they can also lead to malfunctions of the code if not properly mastered.
This article is inspired by various paragraphs the book 'Java for Aliens.' Actually, the topic is explored further in other chapters .
For more information visit https://www.javaforaliens.com.
Published at DZone with permission of Claudio De Sio Cesari. See the original article here.
Opinions expressed by DZone contributors are their own.