LS Price Increase Launch

The Price of all “Learn Spring” course packages will increase by $40 on next Friday:

>> GET ACCESS NOW

1. Overview

Abstract classes and constructors may not seem to be compatible. A constructor is a method called when a class is instantiated, and an abstract class cannot be instantiated. It sounds counterintuitive, right?

In this article, we'll see why abstract classes can have constructors and how using them provides benefits in subclasses instantiation.

2. Default Constructor

When a class doesn't declare any constructor, the compiler creates a default constructor for us. This is also true for abstract classes. Even when there's no explicit constructor, the abstract class will have a default constructor available.

In an abstract class, its descendants can invoke the abstract default constructor using super():

public abstract class AbstractClass {
    // compiler creates a default constructor
}

public class ConcreteClass extends AbstractClass {

    public ConcreteClass() {
        super();
    }
}

3. No-Arguments Constructor

We can declare a constructor with no arguments in an abstract class. It will override the default constructor, and any subclass creation will call it first in the construction chain.

Let's verify this behavior with two subclasses of an abstract class:

public abstract class AbstractClass {
    public AbstractClass() {
        System.out.println("Initializing AbstractClass");
    }
}

public class ConcreteClassA extends AbstractClass {
}

public class ConcreteClassB extends AbstractClass {
    public ConcreteClassB() {
        System.out.println("Initializing ConcreteClassB");
    }
}

Let's see the output we get when calling new ConcreateClassA():

Initializing AbstractClass

While the output for calling new ConcreteClassB() will be:

Initializing AbstractClass
Initializing ConcreteClassB

3.1. Safe Initialization

Declaring an abstract constructor with no arguments can be helpful for safe initialization.

The following Counter class is a superclass for counting natural numbers. We need its value to start from zero.

Let's see how we can use a no-arguments constructor here to ensure a safe initialization:

public abstract class Counter {

    int value;

    public Counter() {
        this.value = 0;
    }

    abstract int increment();
}

Our SimpleCounter subclass implements the increment() method with the ++ operator. It increments the value by one on each invocation:

public class SimpleCounter extends Counter {

    @Override
    int increment() {
        return ++value;
    }
}

Notice that SimpleCounter does not declare any constructor. Its creation relies on counter's no-arguments constructor to be invoked by default.

The following unit test demonstrates the value property being safely initialized by the constructor:

@Test
void givenNoArgAbstractConstructor_whenSubclassCreation_thenCalled() {
    Counter counter = new SimpleCounter();

    assertNotNull(counter);
    assertEquals(0, counter.value);
}

3.2. Preventing Access

Our Counter initialization works fine, but let's imagine we don't want subclasses to override this safe initialization.

First, we need to make the constructor private to prevent subclasses from having access:

private Counter() {
    this.value = 0;
    System.out.println("Counter No-Arguments constructor");
}

Second, let's create another constructor for subclasses to call:

public Counter(int value) {
    this.value = value;
    System.out.println("Parametrized Counter constructor");
}

Finally, our SimpleCounter is required to override the parameterized constructor, otherwise, it won't compile:

public class SimpleCounter extends Counter {

    public SimpleCounter(int value) {
        super(value);
    }

    // concrete methods
}

Notice how the compiler expects we call super(value) on this constructor, to restrict the access to our private no-arguments constructor.

4. Parametrized Constructors

One of the most common uses for constructors in abstract classes is to avoid redundancy. Let's create an example using cars to see how we can take advantage of parametrized constructors.

We begin with an abstract Car class to represent all types of cars. We also need a distance property to know how much it has traveled:

public abstract class Car {

    int distance;

    public Car(int distance) {
        this.distance = distance;
    }
}

Our superclass looks good, but we don't want the distance property to be initialized with a non-zero value. We also want to prevent subclasses from changing the distance property or overriding the parameterized constructor.

Let's see how to restrict access to distance and use constructors to initialize it securely:

public abstract class Car {

    private int distance;

    private Car(int distance) {
        this.distance = distance;
    }

    public Car() {
        this(0);
        System.out.println("Car default constructor");
    }

    // getters
}

Now, our distance property and parameterized constructor are private. There's a public default constructor Car() that delegates the private constructor to initialize distance.

To use our distance property, let's add some behavior to get and display the car's basic information:

abstract String getInformation();

protected void display() {
    String info = new StringBuilder(getInformation())
      .append("\nDistance: " + getDistance())
      .toString();
    System.out.println(info);
}

All subclasses need to provide an implementation of getInformation(), and the display() method will use it to print all details.

Let's now create ElectricCar and FuelCar subclasses:

public class ElectricCar extends Car {
    int chargingTime;

    public ElectricCar(int chargingTime) {
        this.chargingTime = chargingTime;
    }

    @Override
    String getInformation() {
        return new StringBuilder("Electric Car")
          .append("\nCharging Time: " + chargingTime)
          .toString();
    }
}

public class FuelCar extends Car {
    String fuel;

    public FuelCar(String fuel) {
        this.fuel = fuel;
    }

    @Override
    String getInformation() {
        return new StringBuilder("Fuel Car")
          .append("\nFuel type: " + fuel)
          .toString();
    }
}

Let's see those subclasses in action:

ElectricCar electricCar = new ElectricCar(8);
electricCar.display();

FuelCar fuelCar = new FuelCar("Gasoline");
fuelCar.display();

The output produced looks like:

Car default constructor
Electric Car
Charging Time: 8
Distance: 0

Car default constructor
Fuel Car
Fuel type: Gasoline
Distance: 0

5. Conclusion

Like any other classes in Java, abstract classes can have constructors even when they are only called from their concrete subclasses.

In this article, we went through each type of constructor from the perspective of abstract classes – how they're related to concreate subclasses and how can we use them in practical use cases.

As always, code samples can be found over on GitHub.

LS Price Increase Launch

The Price of all “Learn Spring” course packages will increase by $40 on next Friday:

>> GET ACCESS NOW
Generic footer banner
guest
0 Comments
Inline Feedbacks
View all comments