W3docs

Абстрактные классы в 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— editable, runs on the server

Что дальше

Вы познакомились с вариантом абстракции, включающим состояние и общий код. Вариант без них — чистый контракт, никакой реализации — это интерфейс. Продолжайте изучение: интерфейсы в Java.

Практика

Практика
Класс расширяет абстрактный класс, но не реализует все его абстрактные методы. Что произойдёт?
Класс расширяет абстрактный класс, но не реализует все его абстрактные методы. Что произойдёт?
Was this page helpful?