- Абстрактные классы и интерфейсы в ООП: что такое абстракции в объектно-ориентированном программировании
- Что такое абстракция в ООП
- Абстрактные классы
- Интерфейсы
- Отличия абстрактного класса и интерфейса
- Когда использовать абстрактный класс, а когда — интерфейс
- Заключение
Полиморфизм, наследование и инкапсуляция — три кита, на которых строится объектно-ориентированное программирование. Разбираемся, как устроены классы, что они собой представляют, чем отличаются от интерфейсов, когда и что использовать при разработке.
Что такое абстракция в ООП
Абстрагирование в жизни — это фокус на тех свойствах системы, которые важны в текущий момент времени. Абстракция в ООП — использование только определения характеристик объекта без детальной реализации. У объекта есть набор методов, но мы не предоставляем их конкретную логику: ее реализуют потомки.
Абстракция в ООП скрывает от разработчика реализацию. Мы создаем класс и говорим: «Сделай методы, которые будут реализованы в дочерних классах (потомках)». Классу A наследует класс B. В последнем мы указываем abstract и прописываем, что метод должен быть реализован в дочернем классе. Дочерний класс может отнаследоваться и реализовать указанный метод «по своим правилам». При этом наследование возможно от одного класса: у потомка может быть только один родитель (исключая ЯП, в которых допускается множественное наследование).
При этом базовый abstract может содержать стопку уже реализованных методов. Это значит, в нем же могут быть прописаны и реализованные дефолтные методы, которые автоматически «переедут» в потомка (за исключением приватных private). Такая особенность языка программирования значительно оптимизирует код: нам не нужно переписывать один и тот же код методов в потомках. Достаточно использовать extend, чтобы перетянуть методы.
Абстрактные классы
Разберем, как работают абстрактные классы в Java.
К примеру, у нас есть класс «кошачьи». У всех кошек есть общие признаки и функции: все они млекопитающие, ходят на четырех лапах и виляют хвостом. Но способы добычи еды, т.е. реализация условного метода findEat(), могут быть разными:
- домашняя кошка ждет, пока хозяин насыплет корм;
- кошка-рыболов ловит рыбу;
- дикий кот (тигр) охотится на лань.
В таком случае мы можем объявить общий для всех кошачьих class Cat с методом findEat():
class Cat {
findEat()
}
От родительского класса будут наследоваться дочерние class HomeCat, class CatFisherMan, class Tiger. Дочерние классы (потомки) перенимают от родителя характеристики и методы, но получают возможность изменять реализацию.
Объявляем родительский class с характеристиками (=методами), которые будут наследовать потомки:
class Cat {
public void walkOnFourLegs() {
System.out.println("Я хожу на четырех лапах");
}
public void wagTail() {
System.out.println("Я умею вилять хвостом");
}
public abstract void findEat();
}
Создаем потомка, который унаследует родительские свойства и реализует объявленный в нем метод findEat(), но по-своему:
class HomeCat extends Cat {
public void findEat() {
System.out.println("Я домашняя кошка, поэтому сижу и жду, пока меня покормит хозяин");
}
}
При этом наш потомок HomeCat унаследует от родителя четыре лапы walkOnFourLegs() и способность вилять хвостом wagTail(). Разработчик не увидит эти методы в коде class HomeCat, но они есть.
В этом можно убедиться, если видоизменить код и попытаться вызвать у HomeCat метод wagTail(), которого нет в коде.
abstract class Cat {
public void walkOnFourLegs() {
System.out.println("Я хожу на четырех лапах");
}
public void wagTail() {
System.out.println("Я умею вилять хвостом");
}
public abstract void findEat();
}
class HomeCat extends Cat {
public void findEat() {
System.out.println("Я домашняя кошка, поэтому сижу и жду, пока меня покормит хозяин");
}
}
public class MyClass {
public static void main(String args[]) {
HomeCat cat = new HomeCat();
cat.wagTail();
}
}
В public class MyClass мы создаем экземпляр класса HomeCat, обращаемся к нему и вызываем метод, которого не видно в коде, но который существует, поскольку предоставлен родителем. Результат вызова cat.wagTail() — надпись «Я умею вилять хвостом» в консоли.
Казалось, что наш class HomeCat умеет только ждать еду от хозяина. А он, оказывается, унаследовал от родителя способность вилять хвостом (и ходить на четырех лапах, к слову, тоже).

Если упростить, то абстрактные классы позволяют нам создавать потомков, похожих на родителей. Например, родительский class — «Кошачьи», а его потомки — «Домашний кот», «Дикий кот», «Кот-рыболов».

При этом наследование может быть более сложным. У классов все почти как у людей: иногда есть не только родитель, но дедушка, прадедушка и т.д. В таком случае цепочка будет выглядеть, например, так:
child <— abstract parent1 <— abstract parent 2
Потомок будет наследовать то, что ему передали и «дедушка», и «отец». При этом мы можем указать, что родитель не передает ребенку часть методов. Для этого используется private. Свойства и методы, обозначенные так, наследоваться не будут. Родитель сможет передать потомку только public и protected.
Interface же позволяет имплементировать методы разных интерфейсов и создать кота-«франкенштейна».
Например, у interface Bird наш класс возьмет метод «летать», у interface Human — «говорить», у interface Fish — «дышать под водой».

Интерфейсы
Преимущество интерфейсов — в том, что один класс может получить и реализовать методы нескольких интерфейсов (как в кейсе выше, с котом-франкенштейном).
Интерфейс на Java может выглядеть таким образом:
interface Drawable {
void draw();
void resize(int percentage);
}
В void draw() мы указали метод для рисования, а в void resize — задали метод для изменения размера. Теперь любые классы смогут «подписать контракт» и использовать методы.
Интерфейс (Interface) — это подписанный контракт, который должен быть реализован классом. Interface определяет методы и свойства, доступные в классе, но не предоставляет их конкретную реализацию.
Как работают контракты в интерфейсе? Например, в Interface мы можем прописать метод «Ходить на работу к 8.00», а в классе реализуем метод: ходить на автобусе, на метро, пешком, на велосипеде. Так и происходит реализация контракта.
Вернемся к кошкам. Мы «создаем контракт», т.е. объявляем методы:
interface Cat {
void walkOnFourLegs();
void wagTail();
void findEat();
}
Класс, который будет реализовывать наши методы, как бы «подписывает контракт» (в примере — OtherCat).
Далее мы готовы принять любого кота, который реализует интерфейс, будь то HomeCat или OtherCat, и можем вызывать методы согласно контакту.
interface Cat {
void walkOnFourLegs();
void wagTail();
void findEat();
}
class HomeCat implements Cat {
public void walkOnFourLegs() {
System.out.println("Кошка ходит на четырех лапах");
}
public void wagTail() {
System.out.println("Я умею вилять хвостом");
}
public void findEat() {
System.out.println("Я домашняя кошка, поэтому сижу и жду, пока меня покормит хозяин");
}
}
class OtherCat implements Cat {
public void walkOnFourLegs() {
System.out.println("Кошка ходит на четырех лапах");
}
public void wagTail() {
System.out.println("Я умею вилять хвостом");
}
public void findEat() {
System.out.println("Я other кошка, поэтому сижу и жду, пока меня покормит хозяин");
}
}
public class Main {
public static void main(String args[]) {
HomeCat cat = new HomeCat();
OtherCat otherCat = new OtherCat();
printCat(cat);
printCat(otherCat);
}
public static void printCat(Cat cat) {
cat.findEat();
}
}

При этом мы можем создать другой интерфейс, в котором будут методы для фэнтези-котов из сказок: разговаривать, починять примус, пить спирт, свистеть. У нас будет второй интерфейс, который смогут реализовать классы:
interface CatFromFairytale {
void talking();
void drinking();
void whistling();
}
Методы сможет реализовать наш класс OtherCat или HomeCat. В случае с абстрактным классом-родителем class HomeCat был ограничен: он получал только те методы, которые ему передавал родитель. Благодаря интерфейсам класс HomeCat становится «более свободным» и может выбирать, чьи методы ему имплементировать.
Пример с кошками выше достаточно условный. Он помогает понять общие принципы классов и интерфейсов. Но теперь мы можем решить более приближенную к реальности задачу. Пускай нам нужно подключить к сервису три способа платежа: кошелек YooMoney, банковскую карту МИР и мобильный телефон Beeline.
Мы можем создать один интерфейс, в котором будет метод pay(). Он будет передавать условные clientID и amount. Для всех способов платежа эти данные будут одинаковыми. Хэш будет считаться внутри каждой реализации отдельно (это важно, чтобы подписать сигнатуру и т.д.). Дальше создаем три class:
- class YooMoney;
- class BankCard;
- class Beeline.
Они будут реализовывать интерфейс. В каждом классе мы описываем метод pay() и прописываем конкретную реализацию. Например, для class YooMoney будем делать POST-запрос на условный yoomoney.ru, подписывать сигнатуру таким-то образом, response обрабатывать таким-то образом.
Отличия абстрактного класса и интерфейса
В таблице ниже постараемся привести основные особенности. Разница между abstract class и interface может зависеть от конкретного ЯП.
В одних можно наследоваться только один раз, в других возможно множественное наследование. Но суть в том, что абстрактный класс используют, когда есть семейство классов. Интерфейсы нужны, чтобы выкупить долю полиморфизма.
Критерий сравнения | Абстрактный класс | Интерфейс |
Какие методы содержит | Как абстрактные, так и с реализацией | Абстрактные, без реализации |
Синтаксис | Зависит от языка. Для объявления родителя используют abstract class, для наследования потомками — extends | Для объявления используется зарезервированное слово interface. Для реализации — implements |
Для чего используется | Создать общий родительский класс с теми дефолтными возможностями, которые унаследуют все потомки | Выкупить долю полиморфизма, декомпозировать систему на более стройные модули |
Работа с методами | Можно создать несколько дефолтных методов, которые переедут в потомка | Только декларация контракта (interface), реализация в дочерних классах |
Имеет переменные конструктора или экземпляра | Может | Не может |
Как происходят наследование и реализация | Класс может отнаследоваться только от одного абстрактного класса (в некоторых ЯП возможна реализация множественного наследования как в С++) | Класс может реализовать несколько интерфейсов (множественное наследование). Интерфейсов можно имплементировать в класс сколько угодно: 1, 5, 10 и так далее |
Когда использовать абстрактный класс, а когда — интерфейс
При применении абстрактного класса сущности становятся связанными. Использовать его целесообразно, когда у нас есть семейство продуктов. Например, мы работаем с автомобилями. У машин может быть общий предок, от которого происходит наследование: появляются универсал, лифтбэк, седан, купе и прочие.
Интерфейс стоит использовать, когда программист хочет выйти в сторону полиморфизма. Мы можем создавать разные классы, которые реализуют («подписали») один и тот же интерфейсный контракт. Но при этом у всех разное поведение, как в нашем случае с платежами. Также interface подходит для ситуаций, когда разработчику нужно сгруппировать все классы под одно поведение и «накрыть» одним интерфейсом.
В некоторых языках программирования можно наследоваться только от одного класса. Interface решает эту проблему: он позволяет имплементить несколько интерфейсов.
Заключение
На простых примерах и базовом уровне мы разобрали, как работают абстрактные классы и интерфейсы в объектно-ориентированном программировании. В ООП интерфейс и абстрактный класс — одни из ключевых элементов.