Абстрактные классы и интерфейсы в Java
Абстрактные классы и интерфейсы встречаются повсюду как в Java-приложениях, так и в самом Java Development Kit (JDK). Каждый из них служит своей цели:
- Интерфейс — это контракт, который должен быть реализован конкретным классом.
- Абстрактный класс похож на обычный, но отличается тем, что может содержать абстрактные методы — методы без реализации, и нельзя создать экземпляр абстрактного класса.
Многие разработчики не видят разницы между интерфейсами и абстрактными классами, но на самом деле между ними есть весьма существенное различие.
Интерфейсы
Интерфейс — это контракт, который реализуется в некотором классе. У интерфейса не может быть состояния, поэтому в нем нельзя использовать изменяемые поля экземпляра. В интерфейсе могут быть только неизменяемые final-поля.
Когда использовать интерфейсы
Интерфейсы очень полезны для уменьшения связанности (coupling) кода и реализации полиморфизма. Для примера давайте взглянем на интерфейс List из JDK:
public interface List extends Collection
Как вы, вероятно, заметили, код весьма краток и лаконичен. Здесь мы видим сигнатуры методов, которые будут реализованы в конкретном классе, реализующем этот интерфейс.
Контракт интерфейса List реализуется классами ArrayList , Vector , LinkedList и другими.
При использовании полиморфизма тип переменной объявляем как List , и присваиваем ей любую из доступных реализаций. Например:
List list = new ArrayList(); System.out.println(list.getClass()); List list = new LinkedList(); System.out.println(list.getClass());
class java.util.ArrayList class java.util.LinkedList
В этом случае в каждом классе присутствует своя реализация методов. И это отличный пример использования интерфейсов. Если вы заметили, что ряд ваших классов содержит одинаковые методы, но с разными реализациями, то стоит использовать интерфейс.
Переопределение метода интерфейса
Помните, что интерфейс — это контракт, который должен быть реализован конкретным классом. Методы интерфейса неявно абстрактны и обязаны быть реализованы в классе, реализующем этот интерфейс.
Рассмотрим следующий пример:
public class OverridingDemo < public static void main(String[] args) < Challenger challenger = new JavaChallenger(); challenger.doChallenge(); >> interface Challenger < void doChallenge(); >class JavaChallenger implements Challenger < @Override public void doChallenge() < System.out.println("Challenge done!"); >>
Результат будет следующий:
Challenge done!
Обратите внимание еще раз, что методы интерфейса неявно абстрактны и их не нужно явно объявлять как abstract.
Неизменяемые переменные
Еще одно правило, которое следует помнить, заключается в том, что интерфейс может содержать только неизменяемые переменные. Следующий код вполне рабочий:
public interface Challenger
Обратите внимание, что обе переменные неявно final и static . Это означает, что они являются константами, не зависят от экземпляра и не могут быть изменены.
При попытке изменить поля в интерфейсе Challenger , например, следующим образом:
Challenger.number = 8; Challenger.name = "Another Challenger";
будет ошибка компиляции:
Cannot assign a value to final variable 'number' Cannot assign a value to final variable 'name'
Default-методы
После появления в Java 8 методов по умолчанию, некоторые разработчики решили, что интерфейсы стали абстрактными классами. Однако это не так, поскольку у интерфейсов не может быть состояния.
У методов по умолчанию может быть реализация, а у абстрактных методов — нет. Методы по умолчанию — результат появления лямбда-выражений и Stream API, но использовать их нужно с осторожностью.
В качестве примера default-метода из JDK можно привести метод forEach() из интерфейса Iterable . Вместо копирования кода этого метода во все реализации Iterable , мы можем переиспользовать метод forEach :
default void forEach(Consumer action) < // Code implementation here…
Любая реализация Iterable может использовать метод forEach() без необходимости реализации этого нового метода.
Давайте рассмотрим пример с методом по умолчанию:
public class DefaultMethodExample < public static void main(String[] args) < Challenger challenger = new JavaChallenger(); challenger.doChallenge(); >> class JavaChallenger implements Challenger < >interface Challenger < default void doChallenge() < System.out.println("Challenger doing a challenge!"); >>
Challenger doing a challenge!
Важно отметить, что у default-метода должна быть реализация и default-метод не может быть статическим.
Абстрактные классы
У абстрактных классов может быть состояние в виде изменяемых полей экземпляра. Например:
public abstract class AbstractClassMutation < private String name = "challenger"; public static void main(String[] args) < AbstractClassMutation abstractClassMutation = new AbstractClassImpl(); abstractClassMutation.name = "mutated challenger"; System.out.println(abstractClassMutation.name); >> class AbstractClassImpl extends AbstractClassMutation
mutated challenger
Абстрактные методы в абстрактных классах
Аналогично интерфейсам в абстрактных классах могут быть абстрактные методы. Абстрактный метод — это метод без тела (без реализации). Но в отличие от интерфейсов, абстрактные методы в абстрактных классах должны быть явно объявлены как абстрактные.
public abstract class AbstractMethods
Попытка объявить метод без реализации и без ключевого слова abstract , например, следующим образом:
public abstract class AbstractMethods
приведет к ошибке компиляции:
Missing method body, or declare abstract
Когда использовать абстрактные классы
Рекомендуется использовать абстрактный класс, когда вам нужно изменяемое состояние. В качестве примера можно привести класс AbstractList из Java Collections Framework, который использует состояние.
Если хранить состояние класса не нужно, обычно лучше использовать интерфейс.
Хороший пример использования абстрактных классов — паттерн "шаблонный метод" (template method). Шаблонный метод манипулирует переменными экземпляра (полями) внутри конкретных методов.
Различия между абстрактными классами и интерфейсами
С точки зрения объектно-ориентированного программирования основное различие между интерфейсом и абстрактным классом заключается в том, что интерфейс не может иметь состояния, тогда как абстрактный класс может (в виде полей экземпляра).
Другое ключевое различие заключается в том, что классы могут реализовывать более одного интерфейса, но расширять только один абстрактный класс. Множественное наследование может привести к тупиковым ситуациям в коде, поэтому авторы Java решили этого избежать, отказавшись от него.
Еще одно различие состоит в том, что интерфейс может быть реализован классом или расширен другим интерфейсом, а класс может быть только расширен.
Также важно отметить, что лямбда-выражения могут использоваться только с функциональными интерфейсами (интерфейс только с одним методом), но не с абстрактными классами с одним абстрактным методом.
В таблице 1 обобщены различия между абстрактными классами и интерфейсами.
Таблица 1. Сравнение интерфейсов и абстрактных классов
Интерфейсы
Абстрактные классы
Могут содержать только final static поля. Интерфейс никогда не может изменять свое состояние.
Могут быть любые поля, в том числе статические, изменяемые и неизменяемые.
Класс может реализовывать несколько интерфейсов.
Класс может расширять только один абстрактный класс.
Может быть реализован с помощью ключевого слова implements.
Может расширять другой интерфейс с помощью extends.
Может быть только расширен с помощью extends.
Можно использовать только static final поля. Параметры и локальные переменные в методах.
Могут быть изменяемые поля экземпляра. Параметры и локальные переменные в методах.
В лямбда-выражениях могут использоваться только функциональные интерфейсы.
Абстрактные классы с одним абстрактным методом не могут использоваться в лямбда-выражениях.
Не может быть конструктора.
Может содержать конструктор.
Могут быть абстрактные методы.
Могут быть default и static методы (c Java 8).
Могут быть private методы с реализацией (с Java 9).
Могут быть любые методы.
Задачка
Давайте изучим основные различия между интерфейсами и абстрактными классами с помощью небольшой задачки. Вы также можете посмотреть данный материал в формате видео (англ.).
В приведенном ниже коде объявлены интерфейс, абстрактный класс и используются лямбда-выражения.
public class AbstractResidentEvilInterfaceChallenge < static int nemesisRaids = 0; public static void main(String[] args) < Zombie zombie = () ->System.out.println("Graw. " + nemesisRaids++); System.out.println("Nemesis raids: " + nemesisRaids); Nemesis nemesis = new Nemesis() < public void shoot() < shoots = 23; >>; Zombie.zombie.shoot(); zombie.shoot(); nemesis.shoot(); System.out.println("Nemesis shoots: " + nemesis.shoots + " and raids: " + nemesisRaids); > > interface Zombie < Zombie zombie = () ->System.out.println("Stars. "); void shoot(); > abstract class Nemesis implements Zombie
Как вы думаете, какой будет вывод, когда мы запустим этот код? Выберите один из следующих вариантов:
Вариант 1
Compilation error at line 4
Вариант 2
Graw. 0 Nemesis raids: 23 Stars. Nemesis shoots: 23 and raids:1
Вариант 3
Nemesis raids: 0 Stars. Graw. 0 Nemesis shoots: 23 and raids: 1
Nemesis raids: 0 Stars. Graw. 1 Nemesis shoots: 23 and raids:1
Вариант 5
Compilation error at line 6
Разбор задачи
Эта задачка демонстрирует понятия об интерфейсах, абстрактных методах и о некоторых других вещах. Давайте разберем код строка за строкой.
В первой строке main() присутствует лямбда-выражение для интерфейса Zombie. Обратите внимание, что в этой лямбде мы инкрементируем статическое поле. Здесь также можно было использовать поле экземпляра, но не локальную переменную, объявленную вне лямбда-выражения. То есть код компилируется без ошибок. Также обратите внимание, что это лямбда-выражение еще не выполняется, оно только объявлено, и поле nemesisRaids не будет увеличено.
Далее мы выводим значение поля nemesisRaids , которое еще не увеличено. Следовательно, вывод будет:
Nemesis raids: 0
Еще один интересный момент заключается в том, что мы используем анонимный внутренний класс. Мы создаем не экземпляр абстрактного класса Nemesis , но экземпляр анонимного класса, расширяющего Nemesis . Также обратите внимание, что первый конкретный класс в иерархии наследования всегда будет обязан реализовать абстрактные методы.
В интерфейсе Zombie есть поле с типом интерфейса Zombie , объявленное с помощью лямбда-выражения. Поэтому, когда мы вызываем метод Zombie.zombie.shoot() , получим следующий вывод:
Stars.
В следующей строке вызывается лямбда-выражение, которое мы создали в начале. Следовательно, переменная nemesisRaids будет увеличена. Однако, поскольку мы используем оператор постинкремента, она будет увеличена только после этого выражения. Следующий вывод будет:
Graw. 0
Далее вызовем метод shoot для nemesis , который изменяет поле экземпляра shoots на 23. Обратите внимание, что как раз здесь мы видим основную разницу между интерфейсом и абстрактным классом.
Наконец, мы выводим значение nemesis.shoots и nemesisRaids .
Nemesis shoots: 23 and raids: 1
Правильный ответ — вариант 3:
Nemesis raids: 0 Stars. Graw. 0 Nemesis shoots: 23 and raids: 1
Материал подготовлен в преддверии старта специализации Java-разработчик.
Недавно в рамках специализации прошел открытый урок, на котором мы обсудили алгоритм бинарного поиска, разобрались, почему он быстрее линейного. А также познакомились с понятием «О-большое». Делимся записью этого урока.
- java
- абстрактные классы
- интерфейсы
- бинарный поиск
- Блог компании OTUS
- Программирование
- Java
Отличия абстрактного класса от интерфейса (abstract class and interface)
Абстрактный класс — это класс, у которого не реализован один или больше методов (некоторые языки требуют такие методы помечать специальными ключевыми словами).
Интерфейс — это абстрактный класс, у которого ни один метод не реализован, все они публичные и нет переменных класса.
Интерфейс нужен обычно когда описывается только интерфейс (тавтология). Например, один класс хочет дать другому возможность доступа к некоторым своим методам, но не хочет себя «раскрывать». Поэтому он просто реализует интерфейс.
Абстрактный класс нужен, когда нужно семейство классов, у которых есть много общего. Конечно, можно применить и интерфейс, но тогда нужно будет писать много идентичного кода.
В некоторых языках (С++) специального ключевого слова для обозначения интерфейсов нет.
Можно считать, что любой интерфейс — это уже абстрактный класс, но не наоборот.
Отслеживать
11.5k 8 8 золотых знаков 42 42 серебряных знака 69 69 бронзовых знаков
ответ дан 10 июл 2013 в 8:34
112k 6 6 золотых знаков 93 93 серебряных знака 159 159 бронзовых знаков
А где я писал о "pure virtual методах" ? Я писал о том, что у метода отсутствует реализация. А виртуальный он или нет - это детали реализации языка.
11 июл 2013 в 6:55
Проще сказать что интерфейс это частный случай абстрактного класса
2 дек 2015 в 5:10
В с++ можно наследоваться от произвольного количества абстрактных классов. Употребление слов наследует/реализует - просто соглашение.
31 мая 2016 в 19:00
Java и новый C# имеют несколько расширенные определения интерфейса по сравнению с вашим
22 сен 2017 в 11:17
К слову, для C#8+ Ваш ответ более неактуален ¯_(ツ)_/¯
24 июн 2019 в 14:14
tl;dr: Абстрактный класс — средство разработки классов на нижнем уровне, средство для повторного использования кода; интерфейс — средство выражения семантики класса. Таким образом, это совершенно разные, мало связанные между собой понятия.
Думайте об этом по-другому.
Абстрактный класс — это «заготовка» класса: реализовано большинство методов (включая внутренние), кроме нескольких. Эти несколько нереализованных методов вполне могут быть внутренними методами класса, они лишь уточняют детали имплементации. Абстрактный класс — средство для повторного использования кода, средство, чтобы указать, какой метод обязан быть перекрыт для завершения написания класса.
Интерфейс же — это своего рода контракт: интерфейсы используются в определениях чтобы указать, что объект, который будет использован на самом деле, должен реализовывать (для входных параметров) или будет гарантированно реализовывать (для выходных параметров) набор методов и (что намного важнее!) иметь определённую семантику. Интерфейс вполне может быть и пустым, тем не менее, имплементировать интерфейс означает поддерживать данную семантику.
Абстрактные классы идеологически схожи с шаблонами C++: и те, и другие являются заготовками классов, но шаблону для получения класса нужно специфицировать шаблонные типы, а абстрактному классу — абстрактные методы.
Интерфейсы идеологически схожи с заголовочными файлами C++: они раскрывают методы и скрывают конкретную реализацию.
Вопрос о том, является ли интерфейс или абстрактный класс собственно классом — техническая подробность реализации, зависящая от конкретного языка программирования. Например, в C++ интерфейсы отсутствуют вовсе, и их приходится эмулировать классами без данных. Абстрактный класс в C++ как таковой также отсутствует, но им можно считать любой класс с абстрактными методами. (Отсюда ограничение C++: как минимум 1 абстрактный метод в абстрактном классе.) Также в C++ можно (непрямо) инстанциировать абстрактный класс, вызвать абстрактный метод и (возможно) получить ошибку времени выполнения. В C# интерфейсы и абстрактные классы встроены в язык.
Пример (на C#, конкретный язык значения не имеет):
// общий код для всех животных abstract class АбстрактноеЖивотное < public int Возраст < get; protected set; >public int Вес < get; protected set; >public bool Спит < get; protected set; >public void ПодатьГолос() < if (!Спит && Возраст >ВозрастПрорезанияГолоса) РеализацияПодатьГолос(); > abstract protected void РеализацияПодатьГолос(); readonly protected int ВозрастПрорезанияГолоса; > class Собака : АбстрактноеЖивотное < override protected void РеализацияПодатьГолос() < Гав(); >public void Гав() < // реализация >public Собака() < ВозрастПрорезанияГолоса = 2; >> class Кошка : АбстрактноеЖивотное < override protected void РеализацияПодатьГолос() < Мяу(); >public void Мяу() < // реализация >public Кошка() < ВозрастПрорезанияГолоса = 1; >>
interface IЖивотное < int ИнвентарныйНомер < get; >> class Лев : ОбитательЗоопарка, IЖивотное < // . >class Зебра : ОбитательЗоопарка, IЖивотное < // . >class Сторож : ОбитательЗоопарка < >// . void Инвентаризация() < Listобитатели = // . foreach (var обитатель in обитатели) if (обитатель is IЖивотное) // отделяем животных от неживотных ДобавитьЖивотное((IЖивотное)обитатель); > void ДобавитьЖивотное(IЖивотное животное) // сюда сможет попасть только животное < .
Отслеживать
ответ дан 10 июл 2013 в 12:52
207k 28 28 золотых знаков 293 293 серебряных знака 526 526 бронзовых знаков
IЖивотное я думаю, даром инквизицию отменили. нельзя так код писать. уже хочется на русском - так на русском. Да и у автора был php.
10 июл 2013 в 13:23
@KoVadim: Была бы инквизиция, писал бы на латыни. Почему нельзя? Приведите хотя бы один довод. Это ж пример. А отличие интерфейса от абстрактного класса одно и то же что у PHP, что у C#. (Ну, кроме незначительных технических деталей.)
10 июл 2013 в 15:00
это легкий троллинг, что кто то путает 1с и шарп. а по существу - есть один довод - переключаться долго. Хорошо, у меня капсом, а у людей по две кнопки жать нужно или человек будет смотреть и не понимать, почему переменная класса Сторож не приводиться к типу Стopoж (это для особых любителей загадка)
10 июл 2013 в 17:44
смысл киррилических идентификаторов понимается, когда видишь код с китайскими и арабскими идентификаторами (не забываем, что китайский пишется справа на лево!). Вот рефакторинг такого кода срывает крышу намертво.
10 июл 2013 в 19:28
@VladD, не хочу обидеть, но ваши регулярные ответы аналогичного объема смахивают на графоманию. Читать их интересно, но весь их смысл можно передать куда меньшим количеством слов. Ответ @KoVadim и есть пример такого "рефакторинга" ваших ответов.
10 июл 2013 в 20:47
Мне кажется достаточно любопытным, что данный вопрос помечен тегом «ооп», но при этом во многих ответах явно просачиваются специфические аспекты конкретных языков программирования. Я же постараюсь дать ответ исходя из понятий ООП и лишь потом показать, почему вообще это различие появилось в некоторых языках программирования.
Абстрактные классы и интерфейсы имеют определенное отношение к наследованию, точнее к моделированию мира. С их помощью мы хотим выразить, что у определенной группы вещей в нашей системе есть что-то общее: некоторое общее поведение, которое отличает эту группу штуковин от всех остальных.
Допустим, для примера, мы хотим смоделировать кнопки в интерфейсе пользователя. Поскольку мы говорим об ООП, то мы выделим некоторый тип Кнопки с некоторым набором операций (которые определяют поведение) и скрытого состояния, на которое опирается поведение (да, скрытого состояния может и не быть). При этом мы можем выделить три вида операции:
- Конкретная фиксированная операция, которая должна быть абсолютно стабильно для всех типов кнопок.
- Конкретная операция с поведением по умолчанию (т.е. операция, чье поведение подходит для многих типов кнопок, но могут быть кнопки с другим поведением).
- Декларация операции без конкретной реализации (т.е. операция, чье поведение определить невозможно, поскольку на этом этапе не известно разумное поведение по умолчанию или же операции могут слишком сильно различаться у разных кнопок).
Другими словами, тип Кнопки может содержать невиртуальные методы (non-virtual methods), виртуальные методы (virtual methods) и абстрактные методы (abstract methods).
Наличие разных типов методов является очень важным инструментом моделирования и позволяет весьма точно выражать намерения проектировщика. Например, мы можем добавить невиртуальную операцию «Нажатия на кнопку», которая будет делегировать часть своей работы виртуальному (или абстрактному методу) «Обработать нажатие», но при этом всегда выполнять определенную часть работы (прозорливый читатель увидит в этом описании паттерн «Шаблонный метод»).
После того, как мы определили базовый тип, пришло время определить произвольные типы. И тут начинаются вопросы. Точнее, вопросов никаких не возникает, когда у типа есть лишь один непосредственный базовый тип или все базовые типы содержат лишь декларации операций. Не проблема, унаследовать «Кнопку меню» от «Кнопки» и переопределить метод «Нажать на кнопку». Но что, если наш тип «Кнопка меню» будет отнаследован от двух типов с одной и той же виртуальной операцией? Как переопределить лишь одну, а оставить другую? А как быть клиенту нового типа и различить, какую операцию вызвать? А что если у двух базовых типов есть поле с одним именем? А что если у одного базового типа метод «Нажать кнопку» реализован, а у другого – лишь описан в виде декларации?
Нет, все эти проблемы решаемы, и в С++, и Eiffel, и других языках программирования вы можете довольно гибко контролировать, что и как переопределять, что прятать, что выставлять наружу и как вызвать метод определенного базового типа. Но для авторов некоторых языков программирования подобная сложность показалась излишней, и они пошли на хитрость и отделили типы, которые содержат лишь декларации методов в отдельную категорию, и так появились интерфейсы.
Теперь будет легко провести разницу между тремя понятиями – интферфейса, абстрактного базового класса и конкретного базового класса.
- Интерфейс – описывает некоторое семейство типов и содержит лишь декларации операций (да, я осознанно пишу слово «декларация», а не использую слово «контракт», которое в ООП имеет вполне определенное значение).
- Абстрактный базовый класс описывает некоторое семейство типов, но помимо декларации операций может содержать реализации по умолчанию (виртуальные методы) и фиксированные операции (невиртуальные методы).
- Конкретный класс описывает некоторое семейство типов, которое готово для использования клиентами. Такой класс не может содержать декларации операций и все его операции должны быть либо фиксированными (невиртуальные методы) или содержать реализацию по умолчанию (виртуальные методы). Есть еще один подвид конкретных классов – запечатанный (sealed) класс – это разновидность конкретного класса отнаследоваться от которого невозможно, а значит он может содержать лишь конкретные операции.
Выделение интерфейсов в отдельную категорию полезно не только с точки зрения упрощения реализации языков программирования, но и для выделения разных подходов к моделированию. Так, например, наследование классов моделирует отношение «Является» («Кнопка меню» ЯВЛЯЕТСЯ «Кнопкой»), а базовые классы обычно содержат определенный функционал, тесно связанный с функционалом производного класса. Базовые классы не просто моделируют группу типов, но и позволяют использовать повторно существующий функционал.
Интерфейсы же, по своей природе обладают меньшей связностью (low coupling), поскольку не обладают конкретным поведением, которое может осложнить жизнь класса-наследника. Интерфейсы также могут моделировать отношение «Является» («Кнопка меню» ЯВЛЯЕТСЯ «IКнопкой»), но могут определять и менее жесткое отношение «Может выполнять роль» (CAN DO). Например, интерфейс IEquatable из BCL определяет «дополнительное» поведение, которое говорит о возможности типов сравнивать значения объектов.
Абстрактные классы и интерфейсы в Java: погружение в продвинутую теорию
Java позволяет реализовывать полиморфизм двумя ключевыми механизмами: абстрактными классами и интерфейсами. Несмотря на очень похожие концепции, они имеют важные различия, которые важно понимать для разработки эффективных приложений и успешного прохождения технических собеседований. В этой статье мы рассмотрим основные сценарии использования и теоретические аспекты различий между абстрактными классами и интерфейсами, а также рассмотрим примеры реализации задач с их помощью.
Для кого предназначена эта статья:
- Вы используете язык Java в своей работе или только учитесь на нем программировать.
- Вы хотите детально разобраться в различиях абстрактных классов и интерфейсов в Java, включая такие новейшие изменения как Sealed Classes представленные в JEP 409 в 17-й версии Java.
Несмотря на то что мы будем уделять внимание детальному, иногда дотошному, разбору базовых вопросов, которые будут полезны начинающим свой путь в разработке, опытные инженеры-программисты также найдут для себя много полезного в этом материале.
Определения
Для начала нам нужно дать определения абстрактному классу и интерфейсу в Java. Давайте рассмотрим этот вопрос на аналогии из реальной жизни:
Представим, что у нас есть два человека: мужчина и женщина. После рождения они будут иметь базовые данные вроде имени, фамилии, роста и веса. Также они будут уметь дышать и употреблять пищу. Со временем, они будут учиться и получать новые навыки вроде проведения интегральных вычислений, съемки видео для TikTok или создания презентаций в PowerPoint.
- Человек — это базовый абстрактный класс, определяющий состояние объекта вроде имени, фамилии, возраста и базовое поведение в виде навыков дыхания и употребления пищи.
- Мужчина и женщина — это реализации абстрактного класса, определяющие поведение абстрактных методов и добавляющие свое состояние.
- Навык создания презентаций в PowerPoint — это интерфейс, который могут реализовывать разные люди и который не является для них обязательным.
UML-диграмма с абстрактным классом Человек, двумя конкретными классами Мужчина и Женщина, наследующими класс Человек и реализующими два интерфейса: PowerPointService и TikTokService
Стоит упомянуть, что это лишь аналогия, и она не может полностью отобразить сложность и разнообразие реального мира. В реальности люди не могут быть легко классифицированы или ограничены определенными атрибутами или способностями. Это также относится и к программированию: абстрактные классы, интерфейсы и их реализации часто могут быть гораздо сложнее и разнообразнее, чем может показаться на первый взгляд.
Формальное определение абстрактного класса будет звучать следующим образом:
Абстрактный класс — это класс, объявленый как abstract, который имеет возможность включать методы. Абстрактные классы не могут быть созданы (инстанциированы), но они могут быть расширены (наследованы) другими классами.
Формальное определение интерфейса:
Интерфейс — это ссылочный тип, который может содержать константы, сигнатуры методов, методы по-умолчанию, статические методы и вложенные типы, при этом тела методов могут иметь только методы по-умолчанию и статические методы. Интерфейсы не могут быть созданы (инстанциированы), но могут быть реализованы классами или расширены (наследованы) другими интерфейсами.
Абстрактный класс предоставляет структуру с базовым поведением и состоянием, присущим всем его наследникам. Интерфейс, в свою очередь, можно сравнить с набором навыков, которые могут иметь работники в конкретной области. Конкретные классы работников могут реализовывать этот интерфейс и добавлять свои собственные методы, связанные с особенностями их профессии.
Таким образом, абстрактный класс и интерфейс можно использовать вместе для создания гибких и расширяемых приложений, которые могут адаптироваться к изменяющимся условиям и потребностям в различных областях.
Отличия абстрактных классов и интерфейсов
Для начала давайте рассмотрим классическое определение различий между абстрактными классами и интерфейсами, которые можно чаще всего услышать в большинстве технический статей и дискуссий. В следующих разделах мы рассмотрим продвинутые нюансы, которые обычно упускаются при поверхностном изучении этого вопроса.
Итак, абстрактный класс — это класс, который может иметь абстрактные методы, не может быть создан (инстанциирован), но можем иметь конструктор и состояние. Абстрактные методы — это методы, имеющие сигнатуру (название метода, принимаемые параметры и возвращаемый тип), но не имеющие реализации (тела метода). Абстрактный класс также может иметь неабстрактные методы, которые имеют реализацию (тело метода). Все конкретные классы, наследующиеся от абстрактного класса, должны определять реализацию (тело метода) для всех абстрактных методов. Состояние класса можно определить как переменные экземпляра и неабстрактные методы, которые могут получать доступ к этим переменным и изменять их.
Интерфейс, в свою очередь, — это набор абстрактных методов, которые должны быть реализованы классом. Один класс может реализовать множество интерфейсов, но наследоваться только от одного класса. Это дает возможность использовать интерфейсы для, своего рода, реализации множественного наследования, которое не поддерживается Java-классами. Когда класс реализует интерфейс, он должен предоставить реализацию для всех методов, объявленных в интерфейсе — аналогично абстрактным методам в абстрактном классе.
Главное различие между абстрактным классом и интерфейсом заключается в том, что абстрактный класс может иметь состояние, тогда как интерфейс нет. Отсюда вытекает тот факт, что абстрактный класс может иметь конструктор, тогда как интерфейс нет. Когда создается подкласс, конструктор его супер-класса (включая любой абстрактный класс) вызывается автоматически. Интерфейс не может иметь конструктор, потому что он не может быть создан.
Здесь сразу возникает очевидный вопрос — почему мы не можем создать (инстанциировать) абстрактный класс, если у него есть конструктор? Несмотря на наличие конструктора, абстрактный класс не может быть создан (инстанциирован) напрямую потому что он не является полноценным классом из-за отсутствия деталей имплементации — абстрактных методов. Если бы мы давали возможность напрямую создавать абстрактные классы, то у нас бы возникали исключительные ситуации при попытке вызвать его абстрактные методы из-за отсутствия у них реализации (тела метода), поэтому такое решение просто не имеет смысла.
Когда мы создаем объект класса наследника, конструктор абстрактного класса вызывается неявно для инициализации полей абстрактного класса (его состояния). Следовательно, можно считать, что конструктор абстрактного класса вызывается косвенно, а не напрямую. Это же объясняет и отсутствия конструктора у интерфейса в Java — так как у интерфейса нет состояния в виде переменных экземпляра и методов имеющих к ним доступ, то нет необходимости и в конструкторе, который их инициализирует.
Рассмотрим несколько базовых примеров кода для иллюстрации различий между абстрактным классом и интерфейсом в Java.
Чем интерфейс отличается от абстрактного класса

Абстрактные классы vs интерфейсы в Java
Различия между абстрактными классами и интерфейсами в Java
В процессе профессионального развития каждого начинающего программиста всегда наступает тот момент, когда требуется понять и осознать разницу между интерфейсом и абстрактным классом. Ясно представлять себе границы их использования, а также уметь выбирать, что из этого лучше применить в том или ином месте кода.
Кроме того, обсуждаемый в данной статье вопрос о различиях, является одним из самых распространенных на собеседованиях, на который зачастую отвечают либо неполно, либо неверно.
Почему же на этот вопрос отвечают неправильно? Дело в том, что Java развивается, и границы отличий между абстрактным классом и интерфейсом постепенно стираются. А новички, читая устаревшие книги по Java (или не читая их вовсе) или даже современные статьи-перепечатки, где рассказывается об интерфейсах до Java 8, не имеют представления о том, что в своем развитии они ушли далеко вперед.
Например, до появления Java 8, при возникновении вопроса на собеседовании типа: «Назовите разницу между абстрактным классом и интерфейсом», вы смело могли сказать, что интерфейс представляет собой контракт, содержащий методы без реализации, которые должны быть реализованы в классах, имплементирующих этот интерфейс. А абстрактный класс — это класс, который содержит (или не содержит) абстрактные методы и общий для его потомков код.
Также раньше вы могли сказать, что интерфейс не может содержать статические, приватные или методы по умолчанию, и что в нем вообще не может быть реализовано никакой логики. В техническом плане все ограничения остались в прошлом — граница между этими понятиями сейчас уже почти стерта. Но с фундаментальной точки зрения ничего не изменилось!
В данной статье будут рассмотрены как фундаментальные, так и технические различия между интерфейсом и абстрактным классом.
1. Фундаментальное отличие
Фундаментальная разница между интерфейсом и абстрактным классом заключается в том, что интерфейс определяет только поведение. Он не сообщает ничего про объект, который будет его реализовывать.
Например, такое поведение как «движение» может быть применимо к разным типам объектов: машина, кот, котировки курса и т. д. Эти объекты не имеют ничего общего, кроме того, что они могут двигаться.
Перефразируем немного иначе — если существует «движущийся» объект, то глядя на реализованный им интерфейс, мы не сможем понять, какой именно тип объекта имеется ввиду. Это может быть машина, кот, котировка курса валют и много других вариантов.
Если перенести это поведение в код, написанный на языке Java, то получится следующее:
interface Movable
Абстрактный же класс описывает некий абстрактный объект (автомобиль, человека, кота и т. д.), а не только поведение.
Если мы рассуждаем про абстрактный «автомобиль», то в нашей голове сразу формируется картинка с объектом. Мы сразу понимаем, что автомобиль содержит двигатель, колеса и может двигаться, поворачивать, ускоряться или тормозить. Он также будет иметь поля для хранения внутренних деталей, таких как мотор, колеса и тормоза. Вы получите все это просто сказав слово «автомобиль».
При этом кот и котировка курса валют не могут быть автомобилем, даже если они также могут двигаться.
На основе вышесказанного создадим класс Automobile:
abstract class Automobile
Интерфейс и абстрактный класс — понятия не взаимозаменяемые. Даже если абстрактный класс с абстрактными методами выглядит подобно интерфейсу, а интерфейс, с его методами по умолчанию, подобен абстрактному классу с методами, имеющими реализации, то это все равно будут два фундаментально разных понятия. Если нам нужно поведение — необходимо использовать интерфейс. Если речь про концептуальный объект — мы должны использовать абстрактный класс.
А теперь поговорим про различия в технической реализации.
2. Технические отличия
В этом разделе мы обсудим технические (синтаксические) различия в реализации этих концепций в Java. Именно так обычно отвечают на вопрос о разнице между интерфейсами и абстрактными классами (но мы-то уже знаем, что это только вторая часть ответа, и при этом не главная).
2.1. Синтаксис создания
Итак, при создании абстрактного класса указывается ключевое слово abstract, а при определении интерфейса — interface.
Пример абстрактного класса:
public abstract class MyAbstractClass < // поля и конструкторы // абстрактные методы // методы с реализацией >
Пример интерфейса:
public interface MyInterface < // объявление констант // методы без реализации // статические методы // методы по умолчанию (default) // приватные методы >
2.2 Синтаксис использования
При наследовании от абстрактного класса используется ключевое слово extends (с англ. «расширяет»), а при реализации интерфейса — implements (с англ. «реализует»).
В приведенном ниже коде класс MyClass расширяет MyAbstractClass:
public class MyClass extends MyAbstractClass < // реализация абстрактных методов // иной код >
А тут класс MyClass реализует интерфейс MyInterface:
public class MyClass implements MyInterface < // реализация методов интерфейса // иной код >
Класс может одновременно и наследоваться от абстрактного класса (только одного) и реализовать один или множество интерфейсов.
Например, класс MyClass реализует интерфейс MyInterface, MyInterface_2 и MyInterface_3:
class MyClass extends MyAbstractClass implements MyInterface, MyInterface_2, MyInterface_3 < // реализация абстрактных методов абстрактного класса // реализация методов из интерфейсов // иной код >
2.3 Наличие конструктора
Как вы знаете, невозможно создать экземпляр абстрактного класса. Но, объявить и определить в нем конструктор мы можем. В противном случае за нас это сделает компилятор, создав конструктор по умолчанию. Без него код просто не скомпилируется, поскольку при создании конкретного класса первым оператором будет неявный вызов super() конструктора суперкласса, в данном случае абстрактного.
Добавим в абстрактный класс MyAbstractClass конструктор:
public abstract class MyAbstractClass < public MyAbstractClass() < System.out.println("Конструктор из MyAbstractClass"); >>
Также добавим конструктор в MyClass:
public class MyClass extends MyAbstractClass < public MyClass() < System.out.println("Конструктор из MyClass"); >>
В классе Main создадим объект и запустим программу:
public class Main < public static void main(String[] args) < MyAbstractClass myObject = new MyClass(); >>
Результат выполнения программы:

Для интерфейсов понятия «конструктор» не существует.
2.4 Типы переменных
Все переменные в интерфейсах неявно являются public static final (т.е. константами). «final» подразумевает, что переменной обязательно должно быть присвоено значение во время инициализации.
Рассмотрим следующий код:
public interface MyInterface < // эта строка не скомпилируется int value_1; int value_2 = 1; public final int value_3 = 1; static int value_4 = 1; public final static int value_5 = 1; static final int value_6 = 1; >
Поскольку value _1 не присвоено конкретное значение, а она является неявно final, код с такой строкой не скомпилируется. Остальные строки не вызовут ошибок, т.к. public static final можно не указывать (если указать, то IDE выделит их серым цветом, подчеркивая «избыточность»).
В абстрактных классах переменные могут быть любыми — абстрактность класса не накладывает ограничений.
Например, следующий код является корректным:
public abstract class MyAbstractClass
2.5 Модификаторы доступа методов
Модификаторы доступа для абстрактных классов могут быть любыми. При этом, все методы, кроме абстрактных, должны иметь реализацию.
А вот в случае с интерфейсами модификаторы доступа могут быть только двух типов, public и private (последний — начиная с Java 9).
Причем private может быть применим только к методам, имеющим реализацию, которые, в свою очередь, могут использоваться только методами по умолчанию, находящимися в интерфейсе. Класс, реализующий интерфейс, не будет иметь к ним доступ. Методы интерфейса без реализации являются неявно public, поэтому этот модификатор можно не писать.
Рассмотрим следующий код:
public interface MyInterface_2 < void publicAbstractMethod_1(); public void publicAbstractMethod_2(); void publicAbstractMethod_3(); private void privateMethod() < // Реализация метода >>
Методы publicAbstractMethod_1 и publicAbstractMethod_3 являются неявно public.
2.6 Методы с реализацией
Для абстрактного класса все методы, кроме абстрактных, должны иметь реализацию.
Для интерфейсов, начиная с Java 8, вводится понятие метода по умолчанию. Такие методы, во-первых, должны иметь реализацию в интерфейсе, а во-вторых, помечены ключевым словом default. Они также являются неявно public. При этом они не должны в обязательном порядке иметь реализацию в реализующем интерфейс классе, но могут быть в нем переопределены.
Также, начиная с Java 8, в интерфейсах допустимы статические методы, которые неявно являются public, но могут быть явно private. Т. е. если указать модификатор доступа private, метод будет приватным, а если ничего не указывать, то публичным.
Например, код ниже скомпилируется:
public interface MyInterface_3 < default void defaultMethod() < // Реализация метода >public default void defaultMethod_2() < // Реализация метода >private static void privateStaticMethod() < // Реализация метода >public static void publicStaticMethod_1() < // Реализация метода >static void publicStaticMethod_2() < // Реализация метода >>
2.7 Наследование
Интерфейс не может реализовывать интерфейс, не может наследовать абстрактный класс, но может наследовать (используя ключевое слово extends) множество других интерфейсов.
Абстрактный класс может наследовать как обычный класс, так и абстрактный. В обоих случаях это будет только один класс (в Java нет множественного наследования классов).
В то же время абстрактный класс также может реализовать до 65 535 интерфейсов (это связано с ограничением константы interfaces_count в структуре ClassFile).
В этом месте давайте немного отойдем от темы сравнения и затронем множественное наследование. В некоторых источниках однозначно указывается, что множественного наследования в Java нет. В других источниках говорится, что множественного наследования как бы нет, но оно реализуется через интерфейсы.
- наследование состояния (Inheritance of State)
- наследование реализации (Inheritance of Implementation)
- наследование типа (Inheritance of Type)
Java не поддерживает множественное наследование состояния, но поддерживает множественное наследование реализации (как вариант, на основе методов по умолчанию от интерфейсов). Она также поддерживает множественное наследование типов (поскольку может реализовать более одного интерфейса).
Именно таким должен быть ответ на вопрос о множественном наследовании в Java.
3. Рекомендации к применению абстрактных классов и интерфейсов
В соответствии с рекомендациями Oracle абстрактный класс нужно использовать в следующих случаях:
- Необходимо выделить общий код между несколькими тесно связанными классами
Пояснение: это типовой рефакторинг, целью которого является устранение дублирования кода.
- Мы ожидаем, что классы, расширяющие абстрактный класс, имеют много общих методов, полей или требуют модификаторов доступа, отличных от public (protected и private)
Пояснение: ранее мы писали, что в интерфейсах методы, имеющие реализацию (помеченные ключевым словом default), являются неявно public. Если же метод, имеющий реализацию, помечен явно как private, то он не сможет быть использован в классах, реализующих этот интерфейс, а только в других методах интерфейса. Поэтому, если нам нужны методы с модификаторами доступа не public, мы должны использовать абстрактный класс.
- Мы хотим объявить не static или не final поля для изменения состояния объекта.
Пояснение: ранее мы писали, что все переменные в интерфейсах неявно являются public static final — из-за чего они не могут быть изменены.
Для использования интерфейсов существуют следующие причины:
- Планируется, что несвязанные между собой классы будут реализовывать интерфейс. Например, интерфейсы Comparable и Cloneable реализуются многими несвязанными между собой классами.
Пояснение: как мы выяснили ранее, концептуальное назначение интерфейса — описание «поведения», а не «состояния» (в отличие от абстрактного класса). Соответственно, реализовать это поведение могут любые классы, несвязанные между собой, которые просто должны что-то «уметь» делать или «как-то» себя вести.
- Требуется детализировать или определить поведение определенного типа данных, но при этом мы не хотим беспокоиться о том, кто реализует его поведение.
Пояснение: пояснение к прошлому пункту также актуально и для этого — нам все равно, какие классы будут реализовывать наш интерфейс и в каких пакетах находятся. Мы просто хотим, чтобы они вели себя в соответствии с «нашим контрактом», прописанным в интерфейсе в виде методов без реализации.
- Мы хотим воспользоваться преимуществами множественного наследования типов
Приведем наглядный пример выбора между абстрактным классом и интерфейсом.
Существует паттерн «Шаблонный метод», определение которого звучит так: «шаблонный метод определяет основу алгоритма и позволяет подклассам переопределить некоторые его шаги, не изменяя структуру, в целом».
Суть паттерна заключается в размещении в абстрактном классе метода с реализацией, в котором вызываются абстрактные методы. Далее подклассы абстрактного класса реализуют их каждый по-своему.
Так вот вопрос, а что нам мешает разместить абстрактные и шаблонные методы в интерфейсе? В качестве шаблонного метода мы могли бы использовать метод по умолчанию. Вроде звучит логично, согласитесь?
Но обратите еще раз внимание на определение шаблонного метода. Там есть такие слова: «… не изменяя структуру, в целом». Подклассы абстрактного класса не должны иметь возможность переопределить шаблонный метод с целью изменения логики (алгоритма) его работы. По этой причине шаблонные методы объявляются как final и не могут использовать интерфейсы с их методами по умолчанию (могут быть переопределены) вместо абстрактных классов. Вся идея паттерна в этом случае будет нарушена.
Для того чтобы рассмотреть разницу между интерфейсом и абстрактным классом немного глубже, рекомендуется обратиться к книге «Java. Эффективное программирование», Блох Д., 3-е издание.
В разделе 4.6 этой книги говорится о следующих причинах предпочтения интерфейсов абстрактным классам:
1. Существующие классы можно легко приспособить для реализации нового интерфейса.
Пояснение: для этого достаточно написать implements и реализовать необходимые методы. Но уже имеющиеся классы в общем случае не могут быть переделаны для расширения нового абстрактного класса. Если вы хотите, чтобы два класса расширяли один и тот же абстрактный класс, вам придется поднять этот класс в иерархии настолько высоко, чтобы он стал предком обоих этих классов. Это может привести к нарушению логики иерархии типов, заставляя всех потомков нового абстрактного класса расширять его независимо от того, насколько это целесообразно.
2. Интерфейсы идеально подходят для создания миксинов.
Пояснение: Миксин (mixin) — это тип, который класс может реализовать в дополнение к своему «первичному типу», объявляя о том, что этот класс предоставляет некоторое необязательное поведение. Например, Comparable является таким интерфейсом-миксином, т. к. добавляет (примешивает) к первоначальным возможностям типа дополнительную функциональность.
Использовать абстрактные классы для создания миксинов нельзя по той же причине, по которой их невозможно приспособить к уже имеющимся классам: класс не может иметь больше одного родителя, и в иерархии классов нет подходящего места, куда можно поместить миксин.
3. Интерфейсы позволяют создавать неиерархические каркасы типов.
Пояснение: иерархии типов прекрасно подходят для организации некоторых сущностей, но зато сущности других типов невозможно аккуратно уложить в строгую иерархию. Альтернативой им является раздутая иерархия классов.
4. Интерфейсы обеспечивают безопасное и мощное развитие функциональности с использованием шаблона «Декоратор». Паттерн «Декоратор» позволяет динамически (в ходе выполнения программы) добавлять объекту новые возможности (состояние и/или поведение) на основе композиции.