Java抽象类中的构造函数
1. 概述
抽象类和构造函数似乎不兼容。构造函数是类实例化时调用的方法,抽象类不能实例化。这听起来违反直觉,对吧? 在本文中,我们将了解为什么抽象类可以具有构造函数,以及使用它们如何为子类实例化带来好处。
2. 默认构造函数
当一个类没有声明任何构造函数时,编译器会为我们创建一个默认构造函数。对于抽象类也是如此。即使没有显式构造函数,抽象类也会有一个可用的默认构造函数。 在抽象类中,其后代可以使用 *super()*调用抽象默认构造函数:
public abstract class AbstractClass {
// compiler creates a default constructor
}
public class ConcreteClass extends AbstractClass {
public ConcreteClass() {
super();
}
}
3. 无参数构造函数
我们可以在抽象类中声明一个不带参数的构造函数。它将覆盖默认构造函数,任何子类创建都会在构造链中首先调用它。
让我们用抽象类的两个子类来验证这种行为:
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
3.1. 安全初始化
声明不带参数的抽象构造函数有助于安全初始化。 下面的 Counter类是计算自然数的超类。我们需要它的值从零开始。
让我们看看如何在这里使用无参数构造函数来确保安全初始化:
public abstract class Counter {
int value;
public Counter() {
this.value = 0;
}
abstract int increment();
}
我们的SimpleCounter子类使用*++运算符实现了increment()*方法。它在每次调用时将值加一:
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);
}
3.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)来限制对我们私有的无参数构造函数的访问。
4. 参数化构造函数
抽象类中构造函数的最常见用途之一是避免冗余。让我们使用汽车创建一个示例,看看我们如何利用参数化构造函数。 我们从一个抽象的Car类开始来表示所有类型的汽车。我们还需要一个distance属性来知道它已经走了多少:
public abstract class Car {
int distance;
public Car(int distance) {
this.distance = 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()*方法将使用它来打印所有详细信息。 现在让我们创建 ElectricCar和 FuelCar子类:
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