概述

抽象类和构造函数乍看起来似乎不相容。构造函数是在类实例化时调用的方法,而抽象类不能被直接实例化。这听起来似乎有违直觉,对吧?

在这篇文章中,我们将探讨为什么抽象类可以有构造函数,并了解如何在子类实例化时利用它们。

1. 默认构造函数

当一个类没有显式声明任何构造函数时,编译器会自动为我们创建一个默认构造函数。对于抽象类也是一样,即使没有明确的构造函数,抽象类也会有一个可用的默认构造函数。

在抽象类中,其子类可以通过super()调用抽象的默认构造函数:

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

public class ConcreteClass extends AbstractClass {

    public ConcreteClass() {
        super();
    }
}

2. 无参数构造函数

我们可以在抽象类中声明一个无参数的构造函数,它将覆盖默认构造函数,任何子类的创建都会在构造链中首先调用它。

让我们通过两个抽象类的子类来验证这种行为:

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");
    }
}

当我们调用new ConcreateClassA()时,输出如下:

Initializing AbstractClass

而调用new ConcreteClassB()的输出将是:

Initializing AbstractClass
Initializing ConcreteClassB

2.1. 安全初始化

在抽象类中声明一个无参数的构造函数有助于安全初始化。

以下Counter类是表示自然数计数的超类,我们需要它的值从零开始。

让我们看看如何使用无参数构造函数确保安全初始化:

public abstract class Counter {

    int value;

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

    abstract int increment();
}

SimpleCounter子类实现了increment()方法,使用++操作符每次调用时增加value的值:

public class SimpleCounter extends Counter {

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

请注意,SimpleCounter没有声明任何构造函数,它的创建依赖于计数器的无参数构造函数默认被调用。

下面的单元测试演示了value属性通过构造函数被安全初始化:

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

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

2.2. 防止访问

Counter的初始化工作正常,但假设我们不想让子类重写这个安全初始化。

首先,我们需要将构造函数设置为私有,以阻止子类访问:

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

其次,为子类创建另一个构造函数供调用:

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

最后,SimpleCounter需要重写带参数的构造函数,否则编译会失败:

public class SimpleCounter extends Counter {

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

    // concrete methods
}

注意,编译器期望我们在构造函数中调用super(value),以限制对私有无参数构造函数的访问。

3. 参数化构造函数

在抽象类中使用构造函数的一个常见用途是避免冗余。让我们以汽车为例,看看如何利用参数化构造函数。

我们首先创建一个抽象的Car类来代表所有类型的汽车,还需要一个distance属性来记录行驶距离:

public abstract class Car {

    int distance;

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

我们的超类看起来不错,但我们不想让distance属性以非零值初始化,也不想让子类改变或重写参数化构造函数。

让我们看看如何限制distance的访问并安全地使用构造函数初始化它:

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
}

现在,distance属性和参数化构造函数都是私有的。有一个公共的默认构造函数Car(),它委托私有构造函数来初始化distance

为了使用distance属性,我们需要添加一些行为来获取和显示车辆的基本信息:

abstract String getInformation();

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

所有子类都需要实现getInformation()方法,display()方法将使用它来打印所有细节。

现在,让我们创建ElectricCarFuelCar子类:

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();
    }
}

让我们看看这些子类的运行情况:

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

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

产生的输出如下:

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

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

4. 总结

就像Java中的其他类一样,即使它们仅从其具体子类调用,抽象类也可以有构造函数。

在这篇文章中,我们从抽象类的角度审视了每种类型的构造函数——它们与具体子类的关系以及在实际用例中如何使用它们。

如往常一样,代码示例可以在GitHub上找到。