概述
抽象类和构造函数乍看起来似乎不相容。构造函数是在类实例化时调用的方法,而抽象类不能被直接实例化。这听起来似乎有违直觉,对吧?
在这篇文章中,我们将探讨为什么抽象类可以有构造函数,并了解如何在子类实例化时利用它们。
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()
方法将使用它来打印所有细节。
现在,让我们创建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
4. 总结
就像Java中的其他类一样,即使它们仅从其具体子类调用,抽象类也可以有构造函数。
在这篇文章中,我们从抽象类的角度审视了每种类型的构造函数——它们与具体子类的关系以及在实际用例中如何使用它们。
如往常一样,代码示例可以在GitHub上找到。