Friday, August 11, 2023

Quiz yourself: Abstract classes and the difference between Java’s super() and this()

Quiz Yourself, Abstract classes, Java Career, Java Skills, Java Jobs, Java Prep, Java Preparation


Given the following two classes

01:  abstract class SupA {
02:    SupA() { this(null); }
03:    SupA(SubA s) {this.init();}
04:    abstract void init();
05:  }
06:  class SubA extends SupA {
07:    void init() {System.out.print("SubA");}
08:  }

Which statement is correct if you try to compile the code and create an instance of SubA? Choose one.

A. Compilation fails at line 02.
B. Compilation fails at line 03.
C. A runtime exception occurs at line 02.
D. A runtime exception occurs at line 03.
E. SubA is printed.
F. SubASubA is printed.

Answer. This question investigates object initialization, overridden method invocation, and the difference between this() and this.

Consider the process of instantiating and initializing an object, and assume that the class and all its parent types are fully loaded and initialized at the point of instantiation.

◉ First, the invocation of new causes the allocation of memory for the entire object, including all the parent elements of which it is made. That memory is also zeroed in this phase.
◉ Next, assuming no errors (such as running out of memory) occur in the first step, control is transferred to the constructor with an argument type sequence that matches, or is compatible with, the actual parameters of the invocation.

In contemporary releases of Java, including Java 17 and later, all constructors start with one of three code elements. First, there is an implicit call to super() with no arguments. This is followed by either an explicit call to super(...), which may take arguments, or by a call to this(...), which, again, may take arguments. Strictly, evaluation of any actual parameters to these calls executes before those delegating calls. (Note that there’s a proposal to allow explicit code to be placed before those calls, provided no reference is made to the uninitialized this object, but that’s for the future.)

The class SupA has two constructors. The one on line 02 delegates to the one on line 03 using this(null), while the one on line 03 has an implicit call to super().

The class SubA has no explicit constructors; therefore, the compiler gives it an implicit constructor, which delegates using super().

From that outline, consider the flow of construction and initialization of an instance of SubA.

First, the invocation new SubA() begins by allocating and zeroing memory for the entire object, including the storage necessary for the SubA, SupA, and Object parts. There are no instance fields in the code you see in this question, but the principle is the same.

Next, the newly allocated object is passed as the implicit this argument into the implicit constructor for SubA. That constructor immediately delegates—using super()—to the zero-argument constructor for SupA. That constructor in turn immediately delegates to the one-argument constructor for SupA on line 03 using the explicit call this(null).

The body of the constructor on line 03 begins with an implicit call to super() that was generated by the compiler. That call passes control up to the zero-argument constructor of java.lang.Object. When control returns from the constructor, any instance initialization on the SupA class would be executed, but of course there is none in this case. So, execution continues with the explicit body of the constructor. The constructor body calls this.init();, which invokes the implementation on line 07 and prints the message SubA. At that point, the constructor on line 03 is finished and control returns to the constructor on line 02, which also is finished since there’s no more code after this(null).

After all the constructors for SupA have finished, control returns to the implicit constructor of SubA. The implicit constructor would then perform any instance initialization called for by the SubA class, but there is none. At this point, the construction and initialization process has been completed.

Notice that in the description above, the message SubA was printed exactly once, which makes option E the correct answer and options A, B, C, D, and F incorrect.

To dive deeper, here are some specific considerations for clarification and additional points.


A call of the form this() (with parentheses) is a call to an overloaded constructor in the same class. Contrast this with the reference this, which is an explicit reference to the current instance of the class. The second form (this without parentheses) is the prefix used explicitly to invoke the init() method. Also note that init() is an ordinary instance method that implements the abstract method declared in SupA; Java does not attach any special meaning to the name init.

The constructor on line 02 passes null to the constructor on line 03. There’s nothing tricky here. If the constructor on line 03 attempted to refer to the object, it would cause a null pointer exception, but because no such reference is made, there is no problem.

The call to this.init() on line 03 is entirely valid and safe. It’s not possible to enter a constructor except via a call to new, and new must be followed by a concrete class name. This in turn means that the object referred to by this on line 03 must have a proper implementation of the init() method.

Creating a new instance of SubA does not cause an instance of SupA to be created, so there is only one instance, and when you call this.<something>, it will be tried on SubA. If this element is missing, it will be looked up for an inherited element in superclasses. In the case of the init() method, it will be directly present in SubA, so it will be called when the this.init(); statement runs.

One more point regarding good coding practice: Although it’s done in this question, it’s a bad idea to call overridable methods during instance initialization, for two reasons.

◉ The code that’s executed might not be what the programmer intended when the parent class was written.
◉ If the invocation occurs during initialization of a parent class but invokes an implementation in a subclass, the subclass implementation might refer to fields in the subclass that have not yet been initialized. This can cause unexpected behavior and commonly causes null pointer exceptions.

Conclusion. The correct answer is option E.

Source: oracle.com

Related Posts

0 comments:

Post a Comment