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()它在每次調用時value

public class SimpleCounter extends Counter {

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

請注意SimpleCounter沒有聲明任何構造函數。它的創建依賴於默認情況下調用的 counter 的無參數構造函數。

以下單元測試演示了由構造函數安全初始化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) private無參數構造函數的訪問。

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()方法將使用它來打印所有詳細信息。

現在讓我們創建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

5. 結論

與 Java 中的任何其他類一樣,抽像類可以具有構造函數,即使它們僅從其具體子類中調用。
在本文中,我們從抽像類的角度瀏覽了每種類型的構造函數——它們如何與具體子類相關,以及我們如何在實際用例中使用它們。