Абстрактные классы в Java
Создавайте частичные реализации в Java с помощью абстрактных классов и методов, которые подклассы должны завершить.
Абстрактный класс — это класс, который нельзя инстанциировать напрямую. Он существует для того, чтобы его расширяли. Он может содержать как конкретные методы (с телом), так и абстрактные методы (без тела — подкласс обязан их реализовать). Именно это сочетание отличает его от обычного класса и от интерфейса.
На этой странице рассматривается, как объявить абстрактный класс, как подклассы его дополняют, почему абстрактные классы могут хранить состояние, паттерн «шаблонный метод» и как выбрать между абстрактным классом и интерфейсом.
Используйте абстрактный класс, когда конкретным подклассам нужно разделять состояние и инфраструктуру, а не только контракт API. Если вам нужен лишь контракт, лучше подойдёт интерфейс. Абстрактные классы опираются на наследование и полиморфизм, поэтому желательно сначала разобраться с ними.
Объявление
Добавьте abstract к заголовку класса. Добавьте abstract к любому методу, у которого нет тела:
public abstract class Shape {
protected final String name;
protected Shape(String name) { this.name = name; }
public abstract double area(); // no body — subclass must provide one
public String describe() { // concrete — inherited as-is
return name + " area=" + area();
}
}Из этого следует несколько вещей:
new Shape("circle")— это ошибка компиляции: абстрактные классы нельзя инстанциировать.- Подкласс, не реализующий все унаследованные абстрактные методы, сам должен быть объявлен
abstract. Абстрактные подклассы абстрактных классов допустимы. - Абстрактный класс может иметь конструктор — подклассы вызывают его через
super(...), как у обычного родителя.
Реализация абстрактных методов
Конкретный подкласс обязан предоставить тело для каждого унаследованного абстрактного метода:
public class Circle extends Shape {
private final double r;
public Circle(double r) {
super("circle");
this.r = r;
}
@Override
public double area() { return Math.PI * r * r; }
}Теперь new Circle(2) работает, а describe() (унаследованный от Shape) обращается к area() подкласса через динамическую диспетчеризацию.
Абстрактные классы могут хранить состояние
Это главная причина выбрать абстрактный класс вместо интерфейса. Родитель может объявлять поля, писать конструктор для их инициализации и предлагать методы, работающие с этим общим состоянием:
public abstract class HttpHandler {
private final String path;
protected HttpHandler(String path) { this.path = path; }
public final String path() { return path; }
public abstract Response handle(Request r); // subclass-specific behavior
}У каждого конкретного обработчика есть path; родитель хранит его и предоставляет доступ; каждый подкласс реализует только логику обработки запроса. Интерфейсы сами по себе этого не умеют (у них нет полей экземпляра).
Смешивание абстрактных и конкретных методов — шаблонный метод
Распространённый паттерн: абстрактный класс реализует общий алгоритм в виде конкретного метода, оставляя точки вариативности абстрактными. Подклассы заполняют только отличающиеся части:
public abstract class Beverage {
// Template — the algorithm, written once.
public final void prepare() {
boilWater();
brew(); // varies
pourIntoCup();
addCondiments(); // varies
}
protected abstract void brew();
protected abstract void addCondiments();
private void boilWater() { System.out.println("boiling water"); }
private void pourIntoCup() { System.out.println("pouring into cup"); }
}
public class Tea extends Beverage {
protected void brew() { System.out.println("steeping tea"); }
protected void addCondiments() { System.out.println("adding lemon"); }
}Tea.prepare() выполняет шаблон родителя, который через полиморфизм обращается к brew и addCondiments из Tea. Для добавления подкласса Coffee нужно реализовать лишь два абстрактных метода.
Это и есть паттерн «шаблонный метод» — самая распространённая причина выбрать абстрактный класс.
Abstract и final
abstract и final — противоположности, и компилятор это обеспечивает:
abstract class— должен быть унаследован.final class— не может быть унаследован.
То же касается методов: abstract методы должны быть переопределены; final методы не могут. Указать оба модификатора одновременно — ошибка компиляции.
Абстрактный класс vs интерфейс
| Абстрактный класс | Интерфейс | |
|---|---|---|
| Конструкторы | Да | Нет |
| Поля экземпляра | Да | Нет (только константы public static final) |
| Тела методов | Да (любое количество) | Да через default (используется редко) |
| Наследование | Одиночное — только один родительский класс | Множественное — много интерфейсов |
| Когда использовать | Подклассы разделяют состояние и инфраструктуру | Подклассы разделяют только контракт API |
Практическое правило: начинайте с интерфейса. Переходите к абстрактному классу (или добавляйте его) только если замечаете накопление общего кода в реализациях. Методы default в интерфейсах с Java 8+ отвоевали часть территории у абстрактных классов, но сценарий «общего изменяемого состояния» по-прежнему остаётся за абстрактными классами.
Абстрактные методы не могут быть private или static
privateсделает метод невидимым для подклассов — они не смогут его переопределить, и абстрактность потеряет смысл.staticметоды не диспетчеризируются динамически — их нельзя переопределить, только скрыть — поэтому абстрактный статический метод был бы бессмысленным.
Обе комбинации являются ошибками компиляции.
Типичные ошибки
- Забыть
abstractу класса. Метод без тела в неабстрактном классе не скомпилируется — компилятор требует объявить содержащий классabstract. - Попытка инстанциировать абстрактный класс.
new Shape("x")отклоняется на этапе компиляции. Вместо этого инстанциируйте конкретный подкласс. - Нереализованный метод в конкретном подклассе. Если хотя бы один унаследованный абстрактный метод не имеет тела, подкласс тоже должен быть объявлен
abstract. - Расчёт на множественное наследование. Класс может расширять только одного родителя (абстрактного или нет). Если нужно совместить несколько контрактов, используйте интерфейсы.
Рабочий пример
Что дальше
Вы познакомились с вариантом абстракции, включающим состояние и общий код. Вариант без них — чистый контракт, никакой реализации — это интерфейс. Продолжайте изучение: интерфейсы в Java.