W3docs

Абстракция в Java

Скрывайте детали реализации за абстрактными типами в Java с помощью абстрактных классов и интерфейсов.

Абстракция — четвёртый принцип ООП: описывайте что делает тип, не уточняя как. Вы объявляете операции, которые поддерживает тип, оставляете реализацию конкретным классам и пишете остальной код в терминах абстрактного типа. Эта глава — концептуальный обзор. Оба механизма Java — abstract class и interface — рассматриваются в отдельных главах.

Два вопроса

Любое объявление типа в Java отвечает на два вопроса:

  1. Что могут делать вызывающие стороны со значениями этого типа? (его API)
  2. Как реализована каждая из этих операций? (её тело)

Конкретный класс отвечает на оба. Абстрактный тип отвечает только на первый, оставляя второй подтипам:

public interface Shape {
  double area();         // what — every Shape has an area
}

public class Circle implements Shape {
  double r;
  public Circle(double r) { this.r = r; }
  public double area() { return Math.PI * r * r; }   // how
}
public class Square implements Shape {
  double side;
  public Square(double side) { this.side = side; }
  public double area() { return side * side; }
}

Shape говорит: «у каждой фигуры есть площадь». Circle и Square говорят, как её вычислить. Код, принимающий Shape, не знает о конкретном типе:

double sumAreas(List<Shape> shapes) {
  double sum = 0;
  for (Shape s : shapes) sum += s.area();
  return sum;
}

Эта функция замкнута на абстракцию. Сегодня она работает с Circle и Square; завтра — с Triangle; через полгода — с Polygon. Ни один новый тип не требует изменений в sumAreas.

Два механизма Java

МеханизмЧто предоставляетКогда использовать
abstract classЧастичный класс — одни методы абстрактны, у других есть тела, плюс поля и конструкторыКогда подтипы будут разделять состояние и инфраструктурный код
interfaceЧистый (или почти чистый) контракт — методы, которые должны предоставлять реализующие классы; без состояния экземпляраКогда подтипы должны лишь договориться о наборе операций и могут не иметь ничего общего

Класс расширяет один абстрактный класс. Класс может реализовывать много интерфейсов. Эта асимметрия влияет на многие проекты: если вам нужно «множественное наследование», ответом обычно будут интерфейсы.

Абстрактные классы — частичная реализация

abstract на классе означает «нельзя создавать экземпляр напрямую — только подклассы». abstract на методе означает «тела нет; каждый конкретный подкласс должен его предоставить»:

public abstract class Shape {
  public abstract double area();      // every Shape must define this

  // a concrete method, shared across all shapes
  public final String describe() {
    return getClass().getSimpleName() + " area=" + area();
  }
}

new Shape() — ошибка компиляции. new Circle() работает. Внутри describe вызов area() диспетчеризуется к реализации фактического подкласса — тот же механизм полиморфизма, что и для любого переопределённого метода.

Используйте абстрактный класс, когда подтипы действительно разделяют код. Если один и тот же вспомогательный метод повторяется в трёх подклассах, это сигнал перенести его в родитель.

Интерфейсы — контракт

Интерфейс объявляет операции и полностью оставляет реализацию тому, кто его реализует:

public interface Comparable<T> {
  int compareTo(T other);
}

public class Money implements Comparable<Money> {
  private final long cents;
  public int compareTo(Money other) {
    return Long.compare(this.cents, other.cents);
  }
}

Теперь Money работает везде, где ожидается ComparableCollections.sort(...), TreeMap, Arrays.sort(...), ваши собственные обобщённые алгоритмы. Стандартная библиотека и ваш код договариваются об общей абстракции Comparable; ни одна из сторон ничего не знает о другой.

Подавляющее большинство стандартных интерфейсов Java (List, Map, Iterable, Runnable, Function, Comparator, AutoCloseable) работают именно так: небольшой, сфокусированный контракт, в который подключается множество конкретных классов.

Абстракция как инструмент проектирования

Механическая часть абстракции — ключевое слово abstract, объявление interface — невелика. Сложность в том, чтобы решить, какие абстракции определять. Три паттерна, которые встречаются снова и снова:

  • Стратегия (Strategy). Определите интерфейс для «алгоритма». Разные реализации меняют алгоритм, не изменяя код, который его использует. Классический пример — Comparator.
  • Шаблонный метод (Template method). Абстрактный класс реализует общий поток с абстрактными методами в точках вариации. Подклассы заполняют конкретные шаги. Знаменитый пример — метод service у HttpServlet.
  • Плагин / точка расширения. Библиотека публикует интерфейс; пользовательский код реализует его; библиотека вызывает его. Servlet API, драйверы JDBC, BeanPostProcessor в Spring.

В каждом случае выигрыш одинаков: код, зависящий от абстракции, защищён от изменений в реализациях и открыт для добавления новых реализаций в будущем.

Инкапсуляция vs абстракция

Эти понятия близки и часто путаются.

  • Инкапсуляция скрывает реализацию одного конкретного класса (приватные поля, управляемые методы). Это внутреннее дело класса.
  • Абстракция скрывает с каким классом вы работаете, за общим контрактом. Это внешнее дело класса.

Класс с private-полями и аккуратным публичным API инкапсулирован, но ещё не абстрагирован — вызывающие всё ещё зависят от конкретного класса. Замените тип на границе API интерфейсом, и вызывающие начнут зависеть от контракта. Теперь можно менять реализации.

Чтобы увидеть их совместную работу, прочитайте главу об инкапсуляции: инкапсуляция закрывает отдельный класс, абстракция позволяет вызывающим не знать, какой класс они держат.

Распространённые ошибки

Несколько ловушек подстерегают новичков в области абстракции:

  • Попытка создать экземпляр абстрактного типа. new Shape() — ошибка компиляции, когда Shape объявлен abstract или является интерфейсом. Создавайте экземпляр конкретного подтипа (new Circle(2)) и присваивайте его абстрактной ссылке.
  • Преждевременная абстракция. Интерфейс ровно с одной реализацией, написанный «на случай, если понадобится ещё одна», обычно является мёртвым грузом. Добавляйте абстракцию, когда появляется вторая реализация, или когда действительно нужно разделить два модуля. Преждевременная абстракция добавляет косвенность, не давая гибкости.
  • Утечка конкретного типа. Объявление поля или параметра как ArrayList вместо List, или возврат HashMap вместо Map, привязывает вызывающих к конкретному классу и разрушает абстракцию. Предпочитайте наиболее абстрактный тип, который всё ещё выражает то, что вам нужно.
  • Путаница «нет тела» и «ничего не делает». Абстрактный метод не имеет тела, потому что подклассы должны его предоставить. Конкретный метод с пустым телом — настоящий метод, который ничего не делает; это совсем другой контракт.

Разобранный пример

Запустите программу ниже. Она задействует оба механизма: абстрактный класс Shape с общим кодом describe и чистый интерфейс Greeter. Ожидаемый вывод:

Circle area=12.566370614359172
Square area=9.0
total = 21.57

Dear Alice,
hey Alice!

Обратите внимание, что totalArea и цикл с приветствиями нигде не упоминают Circle, Square, FormalGreeter или CasualGreeter — они общаются только с абстракциями Shape и Greeter.

java— editable, runs on the server

Что дальше

Следующая глава посвящена конкретной механике абстрактных классов — абстрактным методам, тому, что может наследовать подкласс, и когда выбирать их вместо интерфейсов.

Практика

Практика
Что лучше всего описывает разницу между инкапсуляцией и абстракцией?
Что лучше всего описывает разницу между инкапсуляцией и абстракцией?
Практика
Когда следует использовать интерфейс вместо абстрактного класса?
Когда следует использовать интерфейс вместо абстрактного класса?
Was this page helpful?