1. 概要

抽象クラスとコンストラクターは互換性がないように見える場合があります。 コンストラクターは、クラスがインスタンス化されるときに呼び出されるメソッドであり抽象クラスはインスタンス化できません。 直感に反しているようですね。

この記事では、抽象クラスがコンストラクターを持つことができる理由と、それらを使用することでサブクラスのインスタンス化にどのように役立つかを説明します。

2. デフォルトコンストラクタ

クラスがコンストラクターを宣言しない場合、コンパイラーはusのデフォルトコンストラクターを作成します。 これは、抽象クラスにも当てはまります。 明示的なコンストラクターがない場合でも、抽象クラスにはデフォルトのコンストラクターがあります。

抽象クラスでは、その子孫は super()を使用して抽象デフォルトコンストラクターを呼び出すことができます。

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

public class ConcreteClass extends AbstractClass {

    public ConcreteClass() {
        super();
    }
}

3. 引数なしのコンストラクタ

抽象クラスで引数なしでコンストラクターを宣言できます。 これはデフォルトのコンストラクターをオーバーライドし、サブクラスを作成すると、コンストラクションチェーンの最初に呼び出されます。

抽象クラスの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

3.1. 安全な初期化

引数なしで抽象コンストラクターを宣言すると、安全な初期化に役立ちます。

次のCounterクラスは、自然数を数えるためのスーパークラスです。 その値はゼロから開始する必要があります。

安全な初期化を確実にするために、ここで引数なしのコンストラクターを使用する方法を見てみましょう。

public abstract class Counter {

    int value;

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

    abstract int increment();
}

SimpleCounter サブクラスは、 ++演算子を使用してincrement()メソッドを実装します。 呼び出しごとにが1ずつ増加します。

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
}

private 引数なしコンストラクターへのアクセスを制限するために、コンパイラーがこのコンストラクターで super(value)を呼び出すことをどのように期待しているかに注意してください。

4. パラメータ化されたコンストラクタ

抽象クラスのコンストラクターの最も一般的な使用法の1つは、冗長性を回避することです。 車を使用して例を作成し、パラメーター化されたコンストラクターを利用する方法を見てみましょう。

まず、すべてのタイプの車を表す抽象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プロパティとパラメーター化されたコンストラクターはプライベートです。 distanceを初期化するようにプライベートコンストラクターを委任するパブリックデフォルトコンストラクターCar()があります。

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

5. 結論

Javaの他のクラスと同様に、抽象クラスは、具象サブクラスからのみ呼び出される場合でも、コンストラクターを持つことができます。

この記事では、抽象クラスの観点から各タイプのコンストラクターについて説明しました。これらは、サブクラスの作成とどのように関連しているか、実際のユースケースでどのように使用できるかを示しています。

いつものように、コードサンプルはGitHubにあります。