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. Предотвращение доступа
Инициализация нашего счетчика
работает нормально, но давайте представим, что мы не хотим, чтобы подклассы переопределяли эту безопасную инициализацию.
Во-первых, нам нужно сделать конструктор закрытым, чтобы предотвратить доступ к подклассам:
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
для представления всех типов автомобилей. Нам также нужно свойство расстояния
, чтобы узнать, сколько он проехал:
public abstract class Car {
int distance;
public Car(int distance) {
this.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
}
Теперь наше свойство расстояния и параметризованный конструктор являются закрытыми.
Существует общедоступный конструктор по умолчанию 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 .