Why do constructors invoke overridable methods?

Java Constructors: What Are They?

Definition:

In Java, a constructor is a special type of method used to initialize objects. This method is invoked automatically when an object is instantiated using the new keyword. It sets up the initial state of the object. A constructor has a name like that of the class and does not have any return type (not even void).

Constructors and Their Uses:

The main purpose of a constructor is to set up an object by initializing its fields either by default or by provided values. Constructors help ensure that an object starts in a valid and predictable state.

Principal Aspects:

  • Exactly the Same Name as Class: The name of a constructor must be exactly the same as that of class.
  • No Return Type: There is no return type for constructors, not even void.
  • Automatically Called: In creating an object using new, a constructor is called.
  • Default Constructor: If you do not define any constructor, Java automatically supplies a no-argument default constructor.

How Constructors Can Be Used to Initialize Objects:

The most significant use of constructors is the initialization of an object’s fields with designated values upon the creation of that object. They allow the developer to provide objects that are pre-configured with data they would require, thus preventing partial or incorrect initialization.

As an illustration:

class Car {
    String model;
    int year;

    // Constructor
    public Car(String model, int year) {
        this.model = model;
        this.year = year;
    }
}

public class Main {
    public static void main(String[] args) {
        // Creating an object of Car
        Car myCar = new Car("Tesla Model S", 2020);

        // The constructor is called to initialize the object with specific values
        System.out.println("Model: " + myCar.model);
        System.out.println("Year: " + myCar.year);
    }
}

Explanation:

  • Above, in the Car class, the constructor which is invoked as follows: new Car(“Tesla Model S”, 2020) to create an object myCar with model set to “Tesla Model S”, year as 2020, will be invoked to establish the state of the object.

Constructors types:

  1. No-Argument Constructor: The object is initialized with default values.
  2. Parameterized Constructor: This constructor takes arguments to set initial values to the object based on the parameters passed.

To sum up,constructors are a fundamental part of object-oriented programming in Java, ensuring that every object is properly initialized before it is used.

What are Overridable Methods?

In Java, overridable methods are the methods that can be modified or redefined by its subclass. Such methods will not be declared as final, static, or private, and such methods allow subclasses to provide their particular implementation while overriding methods from the parent class.

In other words, the very useful functionality of method overriding makes it possible for different classes to implement their own behavior, even while the names of the methods are identical. It is one of the central ideas underlying polymorphism in Java, allowing different class objects to be processed through a common interface.

Method Overriding

Method overriding is a feature wherein a subclass provides a specific implementation for any method, which is already defined in its superclass. The method in the child class should have:

  • The same name,
  • The same return type (or a covariant return type),
  • The same parameter list as the method in the superclass.

When the subclass method overrides, the method redefines the inherited behavior by providing a more specialized or fitting implementation for that class.

Example of Method Overriding:

class Animal {
    public void makeSound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Animal();
        Animal myDog = new Dog();

        myAnimal.makeSound();  // Outputs: Animal makes a sound
        myDog.makeSound();     // Outputs: Dog barks
    }
}

In this example:

  • The Dog class overrides the makeSound method from the Animal class.
  • When an instance of Dog calls makeSound(), the overridden method in Dog is executed instead of the parent class’s method.

Differentiating Between Abstract, Overridable, Static, and Final Methods

1.Final Methods:

  • A method with a final keyword in its declaration cannot be overridden in any subclass.
  • It is useful when one wants to restrict subclasses from changing the behavior of a particular method.

Example:

class Parent {
    public final void display() {
        System.out.println("This method cannot be overridden");
    }
}

class Child extends Parent {
    // The following code would cause a compile-time error
    // public void display() { }
}

2.Static Methods:

  1. static methods are the properties of the class and not an instance of the class, so they can’t be overridden.
  2. However, static methods can be hidden and not overridden by redefining them in subclass but that’s not true overriding because they belong to class not object.

Example:

class Parent {
    public static void display() {
        System.out.println("Parent static method");
    }
}

class Child extends Parent {
    public static void display() {
        System.out.println("Child static method");
    }
}

public class Main {
    public static void main(String[] args) {
        Parent.display();  // Outputs: Parent static method
        Child.display();   // Outputs: Child static method
    }
}

3.Abstract Methods:

  • A method declared in an abstract class, without an implementation, and intended to be overridden by subclasses.
  • Intrinsic in being abstract is the notion of these methods being overridable, by virtue of declaring a method signature that the subclass must implement.

Example:

abstract class Shape {
    abstract void draw();  // Abstract method with no body
}

class Circle extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a circle");
    }
}

4.Overridable Methods:

  • Overridable methods are regular instance methods (i.e., not marked as final, static, or private).
  • They can be overridden in subclasses to provide specific behavior.

Example:

class Vehicle {
    public void start() {
        System.out.println("Vehicle starts");
    }
}

class Car extends Vehicle {
    @Override
    public void start() {
        System.out.println("Car starts with a key");
    }
}

Things to Remember:

  • Final Methods – cannot be overridden
  • Static methods – No overriding but can be hidden
  • Abstract methods – Must be overridden in concrete subclasses
  • Overrideable methods – Instance methods that may be overridden in subclasses for customization

The above distinctions are necessary when designing the classes in Java so that appropriate methods are intended for overriding or inheritance and rest are not disturbed as such.

Why Constructors Should not call Overridable Method

Risks and Challenges courtesy

When a constructor calls an overridable method, several risks and challenges pop up. Probably the most omnipresent problem stems from the fact that, during construction, the object is not yet fully initialized. But there is one problem: if a constructor calls a method that can be overridden in a subclass then the overridden method may well try to use fields or resources that have not yet been initialized, with unpredictable results, including possible runtime exceptions.

Major Risk:

1.Object Incompleteness During Initialization:

  • If the constructor of a superclass invokes a method that has overridden in one of its subclasses, the subclass object may not be fully initialized when the method is invoked.
  • Because of this, the subclass’s overridden method operates an incomplete object, which may also lead to a NullPointerException, misbehavior, or other runtime errors.

2.Unpredictable Behavior:

  • The overridden method can be dependent on properties or resources that are not yet set during the partial construction of the object. This may lead to logic errors, which are difficult to debug as the flow of execution behaves in unexpected ways .

3.Breach of Encapsulation:

  • A constructor is supposed to put an object into a predictable state. By calling an overridable method, it exposes part of the object to outside modification before it’s finished initializing, violating the encapsulation of the class.

4.Difficult to Debug:

  • Since invocation of overridable methods during construction can be very subtle and hard to track, identifying why a method isn’t behaving properly usually becomes complicated because constructors and method invocations while the object is being created are ordered.

Example of Potential Issues:

class Parent {
    public Parent() {
        // Calls an overridable method
        display();
    }
    
    public void display() {
        System.out.println("Parent display method");
    }
}

class Child extends Parent {
    private String message = "Child display method";

    @Override
    public void display() {
        // Overridden method depends on initialized field
        System.out.println(message);  // Could print null or cause an exception
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();  // Outputs: null or exception
    }
}

In this example:

  • The constructor of the Parent class calls the method display() which is overridden in the Child class.
  • However, when the constructor of Parent calls display(), the fields belonging to the Child class – like message – may not yet be initialized and hence prints null or throws an exception.

Explanation of How Polymorphism Works

Polymorphism is one of the key concepts in object-oriented programming; it is basically the ability of a method in a subclass to override a method in its superclass. This will mean that the method invoked upon an object depends on the actual runtime type of the object at runtime and not on the type reference that holds it.

For example:

Animal animal = new Dog();
animal.makeSound();  // This will call Dog's version of makeSound(), not Animal's.

This nature of polymorphism, however, gets in the way when an object is being constructed if an overridable method is invoked. In constructing an object, Java first initialises the superclass-the parent constructor runs before the child constructor. Therefore, if a method overridden in the subclass is called in the construction of the parent class, the following sequence occurs:

  1. Superclass Constructor Runs: The superclass constructor runs before the subclass constructor.
  2. Method Invocation: If the superclass constructor calls an overridable method, then due to polymorphism, it will call the overridden one in its subclass.
  3. Incomplete Object State: The subclass at that stage is not yet fully initialized, so the overriding method may access fields or methods that are yet to be set up, causing unexpected behavior.

Why Calling an Overridable Method in a Constructor Leads to Unexpected Behavior

The problem here is that this polymorphism allows the overridden method to be called in the subclass before the subclass is properly initialized. This can lead to:

  1. Uninitialized Fields: By default, the fields of the subclass may not have proper values assigned as yet, or make use of null values or default values within an overridden method.
  2. Broken Invariants: During the invocation of an overridden method, the state of the subclass may not be consistent with its own invariants. To say it in other terms, it may violate its own intended behavior.
  3. Unexpected Side Effects: This could involve calling a subclass method from the parent constructor with side effects like events being fired or actions, such as database connectivity or network calls, that get invoked before the object is fully constructed.

Example of Unexpected Behavior:

class Base {
    public Base() {
        init();  // Calls an overridable method
    }

    public void init() {
        System.out.println("Base init");
    }
}

class Derived extends Base {
    private int value = 42;

    @Override
    public void init() {
        System.out.println("Derived init, value = " + value);  // Value might be 0
    }
}

public class Main {
    public static void main(String[] args) {
        Derived derived = new Derived();
        // Outputs:
        // Derived init, value = 0
        // Actual value is expected to be 42, but it's 0 during construction
    }
}

In this example:

  • The constructor of class Base calls the init() method.
  • Because Derived overrides init(), the subclass’s version is called. However, the value for field in the class Derived has not yet been initialized, so it has an incorrect value printed.

Impact of Calling Overridable Methods in Constructors

Calling overridable methods from constructors in Java can have serious negative consequences, especially given that a method may be based on subclass-specific fields or behavior. By far the most common result is that the object will be only partially constructed when the overridden method is called, causing a runtime error or undefined behavior. This would be because the subclass’s constructor has not yet completed and its fields may be default-valued or otherwise uninitialized.

Important Impacts:

1. Partially Initialized Objects:

  • In the case of calling an overridden method in the constructor of a superclass, the subclass fields may get partially initialized with wrong values, such as null, or default values-for example, integers take the value 0.
  • If the overridden method depends on these fields, it will behave incorrectly, possibly leading to bugs that are hard to detect.

2.Broken Invariants:

  • Classes often have internal rules or invariants that must hold true for the object to be in a valid state. If an overridden method depends on such invariants before they are completely set up, then the object may become invalid.

3.Unpredictable Behaviour:

  • That is, overridden methods in such classes may perform operations that involve external systems, which can easily fail or behave unexpectedly due to the incomplete state of the object.

4. Inconsistent Initialization Flow:

  • Java guarantees that the superclass constructor will always run before the subclass constructor. However, overriding a method during this process introduces an inconsistency where a subclass method is executed before the subclass is fully initialized.

Real-World Scenarios Where Overridable Methods Go Awry

Scenario 1: GUI Frameworks (Swing/AWT)
For example, in most GUI libraries and frameworks-developers subclass components such as JPanel and JFrame-and override certain methods, like paintComponent or initComponents, to specify some custom behavior. However, if the constructor of a super class component calls back (invokes) an overridable method, like paintComponent, before the subclass has completed initialization of its fields, serious graphical glitches or throwing exceptions may result.

Example:

class BasePanel extends JPanel {
    public BasePanel() {
        // Calls an overridable method
        setup();
    }
    
    public void setup() {
        System.out.println("BasePanel setup");
    }
}

class CustomPanel extends BasePanel {
    private Color backgroundColor;

    public CustomPanel() {
        backgroundColor = Color.BLUE; // Initializes color
    }

    @Override
    public void setup() {
        // Uses backgroundColor that might not be initialized yet
        System.out.println("CustomPanel setup with background color: " + backgroundColor);
    }
}

public class Main {
    public static void main(String[] args) {
        CustomPanel panel = new CustomPanel();
        // Output: CustomPanel setup with background color: null
        // The panel's background color is null because the field is not initialized yet
    }
}

In this case, setup() is called from the BasePanel constructor before CustomPanel has a chance to initialize its backgroundColor field. This results in null getting printed instead of a color as intended, which can lead to an unreliably rendered graphical application.

Scenario 2: Logging During Construction
Suppose we have a logging framework that logs object state at instance construction time. If a constructor in a superclass invokes an overridden method to log information about the object being constructed, then that logging method may log incomplete or incorrect information because subclass-specific fields are not yet initialized.

Example:

class Parent {
    public Parent() {
        // Calls an overridable method
        logInfo();
    }
    
    public void logInfo() {
        System.out.println("Parent log info");
    }
}

class Child extends Parent {
    private String name = "Child Object";

    @Override
    public void logInfo() {
        // Logs information before 'name' is initialized
        System.out.println("Child log info: " + name);
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
        // Output: Child log info: null
        // The field 'name' is not initialized yet when the constructor is called
    }
}

logInfo() method overridden in Child class is called from Parent constructor. That means, since name field in Child hasn’t been initialized yet, it will be null and will log null, meaning that we will provide incorrect logs.

Scenario 3: Dependency Injection and Service Initialization
In enterprise applications, the object will be injected with services or resources by a dependency injection framework such as Spring or Hibernate. If an overridable method is invoked before the injections are complete, it will lead to NullPointerExceptions or services that are ill-configured.

Example:

class ParentService {
    public ParentService() {
        // Calls an overridable method
        initialize();
    }

    public void initialize() {
        System.out.println("ParentService initialized");
    }
}

class ChildService extends ParentService {
    private DatabaseConnection dbConnection;

    public ChildService(DatabaseConnection dbConnection) {
        // Injects the database connection
        this.dbConnection = dbConnection;
    }

    @Override
    public void initialize() {
        if (dbConnection == null) {
            throw new IllegalStateException("Database connection not initialized!");
        }
        System.out.println("ChildService initialized with DB connection");
    }
}

In this scenario, if the initialize() method is called from the ParentService constructor, the dbConnection in ChildService might not be injected yet, leading to an IllegalStateException.

How Invoking Overridable Methods May Result in Partially Initialized Objects

The process of object construction in Java involves a call to the constructor of the superclass and then that of the subclass. The gap this causes, between the initialization of the superclass and the full initialization of the subclass, may result in partially initialized objects.

When the constructor of a superclass calls an overridable method:

  1. Superclass Constructor Runs First: The superclass constructor initializes the fields and behavior specific to the superclass.
  2. Overridable Method Called: The overridable method in the subclass will be called if the superclass constructor calls it due to polymorphism.
  3. Subclass Fields May Be Uninitialized: Since the subclass constructor hasn’t executed yet, any fields or properties in the subclass might still have default values-for example, null for objects, 0 for numbers.
  4. Unreliable Methods Behavior: An invoked method may misbehave since it operates on fields which are not properly set up.

Example of Partially Initialized Object:

class BaseClass {
    public BaseClass() {
        initialize();
    }

    public void initialize() {
        System.out.println("BaseClass initialized");
    }
}

class SubClass extends BaseClass {
    private String config;

    public SubClass() {
        config = "Initialized Configuration";
    }

    @Override
    public void initialize() {
        System.out.println("SubClass initialized with config: " + config);
    }
}

public class Main {
    public static void main(String[] args) {
        SubClass obj = new SubClass();
        // Output: SubClass initialized with config: null
        // Config is not yet initialized when the method is called
    }
}

Explanations of the above code are as follows:

  • The constructor of the BaseClass calls the initialize() method which is overridden in the subclass.
  • Because the config field in the subclass hasn’t been initalized when initialize() is invoked, it prints null instead.

Examples of Invoking Overridable Methods in Constructors

In Java, calling overridable methods in constructors results in potential unpredictive behavior or even runtime errors. Following is an example code snippet demonstrating calling an overridable method in a constructor as bad behavior, followed by a safe alternative example.

Unsafe Pattern: Calling Overridable Method in Constructor

In this example, a superclass constructor calls an overridable method (displayInfo()). The subclass overrides this method, but its fields are not initialized at the time of the call, leading to unexpected output.

class Parent {
    public Parent() {
        // Calls an overridable method from the constructor
        displayInfo();
    }

    public void displayInfo() {
        System.out.println("Parent displayInfo");
    }
}

class Child extends Parent {
    private String message = "Child message";

    public Child() {
        System.out.println("Child constructor");
    }

    @Override
    public void displayInfo() {
        System.out.println("Child displayInfo: " + message);
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
        // Output:
        // Child displayInfo: null  <-- Unexpected output (because message is not initialized yet)
        // Child constructor
    }
}

Explanation:

  • The Parent constructor calls the displayInfo() method.
  • Because Child overrides displayInfo(), its Child version gets executed, but the message field in Child hasn’t yet been initialized; therefore, it prints null instead of “Child message”.
  • This is a very important example of how calling overridable methods in constructors can be dangerous because the fields in the subclass have not yet been initialized.

Safe Pattern: Avoid Calling Overridable Methods in Constructor

The following code demonstrates a safe pattern by using private or final methods in the constructor to avoid invoking an overridable method:

class Parent {
    public Parent() {
        // Use a private method or final method to ensure no overrides
        safeDisplayInfo();
    }

    // Final method to prevent overriding
    private void safeDisplayInfo() {
        System.out.println("Parent safeDisplayInfo");
    }
}

class Child extends Parent {
    private String message = "Child message";

    public Child() {
        System.out.println("Child constructor");
    }

    // This method is safe because it's not called from the parent constructor
    public void displayInfo() {
        System.out.println("Child displayInfo: " + message);
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
        child.displayInfo();
        // Output:
        // Parent safeDisplayInfo
        // Child constructor
        // Child displayInfo: Child message  <-- Correct output
    }
}

Explanation:

  • The Parent class now calls a private method (safeDisplayInfo()) in the constructor, ensuring that no overridden method from a subclass can be executed.
  • The Child class has its own displayInfo() method, but it is not invoked during the parent class construction, allowing it to function correctly after the Child constructor has fully initialized the object.
  • The output is as expected, with “Child message” printed when displayInfo() is called, ensuring safe behavior.
Share The Tutorial With Your Friends
Twiter
Facebook
LinkedIn
Email
WhatsApp
Skype
Reddit

Check Our Ebook for This Online Course

Advanced topics are covered in this ebook with many practical examples.