Абстрактные классы и интерфейсы в 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
Абстрактные классы и интерфейсы в Java: погружение в продвинутую теорию
Java позволяет реализовывать полиморфизм двумя ключевыми механизмами: абстрактными классами и интерфейсами. Несмотря на очень похожие концепции, они имеют важные различия, которые важно понимать для разработки эффективных приложений и успешного прохождения технических собеседований. В этой статье мы рассмотрим основные сценарии использования и теоретические аспекты различий между абстрактными классами и интерфейсами, а также рассмотрим примеры реализации задач с их помощью.
5.2K показов
19K открытий
Для кого предназначена эта статья:
- Вы используете язык Java в своей работе или только учитесь на нем программировать.
- Вы хотите детально разобраться в различиях абстрактных классов и интерфейсов в Java, включая такие новейшие изменения как Sealed Classes представленные в JEP 409 в 17-й версии Java.
Несмотря на то что мы будем уделять внимание детальному, иногда дотошному, разбору базовых вопросов, которые будут полезны начинающим свой путь в разработке, опытные инженеры-программисты также найдут для себя много полезного в этом материале.
Определения
Для начала нам нужно дать определения абстрактному классу и интерфейсу в Java. Давайте рассмотрим этот вопрос на аналогии из реальной жизни:
Представим, что у нас есть два человека: мужчина и женщина. После рождения они будут иметь базовые данные вроде имени, фамилии, роста и веса. Также они будут уметь дышать и употреблять пищу. Со временем, они будут учиться и получать новые навыки вроде проведения интегральных вычислений, съемки видео для TikTok или создания презентаций в PowerPoint.
- Человек — это базовый абстрактный класс, определяющий состояние объекта вроде имени, фамилии, возраста и базовое поведение в виде навыков дыхания и употребления пищи.
- Мужчина и женщина — это реализации абстрактного класса, определяющие поведение абстрактных методов и добавляющие свое состояние.
- Навык создания презентаций в PowerPoint — это интерфейс, который могут реализовывать разные люди и который не является для них обязательным.
UML-диграмма с абстрактным классом Человек, двумя конкретными классами Мужчина и Женщина, наследующими класс Человек и реализующими два интерфейса: PowerPointService и TikTokService
Стоит упомянуть, что это лишь аналогия, и она не может полностью отобразить сложность и разнообразие реального мира. В реальности люди не могут быть легко классифицированы или ограничены определенными атрибутами или способностями. Это также относится и к программированию: абстрактные классы, интерфейсы и их реализации часто могут быть гораздо сложнее и разнообразнее, чем может показаться на первый взгляд.
Формальное определение абстрактного класса будет звучать следующим образом:
Абстрактный класс — это класс, объявленый как abstract, который имеет возможность включать методы. Абстрактные классы не могут быть созданы (инстанциированы), но они могут быть расширены (наследованы) другими классами.
Формальное определение интерфейса:
Интерфейс — это ссылочный тип, который может содержать константы, сигнатуры методов, методы по-умолчанию, статические методы и вложенные типы, при этом тела методов могут иметь только методы по-умолчанию и статические методы. Интерфейсы не могут быть созданы (инстанциированы), но могут быть реализованы классами или расширены (наследованы) другими интерфейсами.
Абстрактный класс предоставляет структуру с базовым поведением и состоянием, присущим всем его наследникам. Интерфейс, в свою очередь, можно сравнить с набором навыков, которые могут иметь работники в конкретной области. Конкретные классы работников могут реализовывать этот интерфейс и добавлять свои собственные методы, связанные с особенностями их профессии.
Таким образом, абстрактный класс и интерфейс можно использовать вместе для создания гибких и расширяемых приложений, которые могут адаптироваться к изменяющимся условиям и потребностям в различных областях.
Отличия абстрактных классов и интерфейсов
Для начала давайте рассмотрим классическое определение различий между абстрактными классами и интерфейсами, которые можно чаще всего услышать в большинстве технический статей и дискуссий. В следующих разделах мы рассмотрим продвинутые нюансы, которые обычно упускаются при поверхностном изучении этого вопроса.
Итак, абстрактный класс — это класс, который может иметь абстрактные методы, не может быть создан (инстанциирован), но можем иметь конструктор и состояние. Абстрактные методы — это методы, имеющие сигнатуру (название метода, принимаемые параметры и возвращаемый тип), но не имеющие реализации (тела метода). Абстрактный класс также может иметь неабстрактные методы, которые имеют реализацию (тело метода). Все конкретные классы, наследующиеся от абстрактного класса, должны определять реализацию (тело метода) для всех абстрактных методов. Состояние класса можно определить как переменные экземпляра и неабстрактные методы, которые могут получать доступ к этим переменным и изменять их.
Интерфейс, в свою очередь, — это набор абстрактных методов, которые должны быть реализованы классом. Один класс может реализовать множество интерфейсов, но наследоваться только от одного класса. Это дает возможность использовать интерфейсы для, своего рода, реализации множественного наследования, которое не поддерживается Java-классами. Когда класс реализует интерфейс, он должен предоставить реализацию для всех методов, объявленных в интерфейсе — аналогично абстрактным методам в абстрактном классе.
Главное различие между абстрактным классом и интерфейсом заключается в том, что абстрактный класс может иметь состояние, тогда как интерфейс нет. Отсюда вытекает тот факт, что абстрактный класс может иметь конструктор, тогда как интерфейс нет. Когда создается подкласс, конструктор его супер-класса (включая любой абстрактный класс) вызывается автоматически. Интерфейс не может иметь конструктор, потому что он не может быть создан.
Здесь сразу возникает очевидный вопрос — почему мы не можем создать (инстанциировать) абстрактный класс, если у него есть конструктор? Несмотря на наличие конструктора, абстрактный класс не может быть создан (инстанциирован) напрямую потому что он не является полноценным классом из-за отсутствия деталей имплементации — абстрактных методов. Если бы мы давали возможность напрямую создавать абстрактные классы, то у нас бы возникали исключительные ситуации при попытке вызвать его абстрактные методы из-за отсутствия у них реализации (тела метода), поэтому такое решение просто не имеет смысла.
Когда мы создаем объект класса наследника, конструктор абстрактного класса вызывается неявно для инициализации полей абстрактного класса (его состояния). Следовательно, можно считать, что конструктор абстрактного класса вызывается косвенно, а не напрямую. Это же объясняет и отсутствия конструктора у интерфейса в Java — так как у интерфейса нет состояния в виде переменных экземпляра и методов имеющих к ним доступ, то нет необходимости и в конструкторе, который их инициализирует.
Рассмотрим несколько базовых примеров кода для иллюстрации различий между абстрактным классом и интерфейсом в Java.
Чем интерфейс отличается от абстрактного класса
Один из принципов проектирования гласит, что при создании системы классов надо программировать на уровне интерфейсов, а не их конкретных реализаций. Под интерфейсами в данном случае понимаются не только типы C#, определенные с помощью ключевого слова interface , а определение функционала без его конкретной реализации. То есть под данное определение попадают как собственно интерфейсы, так и абстрактные классы, которые могут иметь абстрактные методы без конкретной реализации.
В этом плане у абстрактных классов и интерфейсов много общего. Нередко при проектировании программ в паттернах мы можем заменять абстрактные классы на интерфейсы и наоборот. Однако все же они имеют некоторые отличия.
Когда следует использовать абстрактные классы:
- Если надо определить общий функционал для родственных объектов
- Если мы проектируем довольно большую функциональную единицу, которая содержит много базового функционала
- Если нужно, чтобы все производные классы на всех уровнях наследования имели некоторую общую реализацию. При использовании абстрактных классов, если мы захотим изменить базовый функционал во всех наследниках, то достаточно поменять его в абстрактном базовом классе. Если же нам вдруг надо будет поменять название или параметры метода интерфейса, то придется вносить изменения и также во всех классы, которые данный интерфейс реализуют.
Когда следует использовать интерфейсы:
- Если нам надо определить функционал для группы разрозненных объектов, которые могут быть никак не связаны между собой.
- Если мы проектируем небольшой функциональный тип
Ключевыми здесь являются первые пункты, которые можно свести к следующему принципу: если классы относятся к единой системе классификации, то выбирается абстрактный класс. Иначе выбирается интерфейс. Посмотрим на примере.
Допустим, у нас есть система транспортных средств: легковой автомобиль, автобус, трамвай, поезд и т.д. Поскольку данные объекты являются родственными, мы можем выделить у них общие признаки, то в данном случае можно использовать абстрактные классы:
public abstract class Vehicle < public abstract void Move(); >public class Car : Vehicle < public override void Move() < Console.WriteLine("Машина едет"); >> public class Bus : Vehicle < public override void Move() < Console.WriteLine("Автобус едет"); >> public class Tram : Vehicle < public override void Move() < Console.WriteLine("Трамвай едет"); >>
Абстрактный класс Vehicle определяет абстрактный метод перемещения Move() , а классы-наследники его реализуют.
Но, предположим, что наша система транспорта не ограничивается вышеперечисленными транспортными средствами. Например, мы можем добавить самолеты, лодки. Возможно, также мы добавим лошадь - животное, которое может также выполнять роль транспортного средства. Также можно добавить дирижабль. Вобщем получается довольно широкий круг объектов, которые связаны только тем, что являются транспортным средством и должны реализовать некоторый метод Move() , выполняющий перемещение.
Так как объекты малосвязанные между собой, то для определения общего для всех них функционала лучше определить интерфейс. Тем более некоторые из этих объектов могут существовать в рамках параллельных систем классификаций. Например, лошадь может быть классом в структуре системы классов животного мира.
Возможная реализация интерфейса могла бы выглядеть следующим образом:
public interface IMovable < void Move(); >public abstract class Vehicle : IMovable < public abstract void Move(); >public class Car : Vehicle < public override void Move() =>Console.WriteLine("Машина едет"); > public class Bus : Vehicle < public override void Move() =>Console.WriteLine("Автобус едет"); > public class Hourse : IMovable < public void Move() =>Console.WriteLine("Лошадь скачет"); > public class Aircraft : IMovable < public void Move() =>Console.WriteLine("Самолет летит"); >
Теперь метод Move() определяется в интерфейсе IMovable, а конкретные классы его реализуют.
Говоря об использовании абстрактных классов и интерфейсов можно привести еще такую аналогию, как состояние и действие. Как правило, абстрактные классы фокусируются на общем состоянии классов-наследников. В то время как интерфейсы строятся вокруг какого-либо общего действия.
Например, солнце, костер, батарея отопления и электрический нагреватель выполняют функцию нагревания или излучения тепла. По большому счету выделение тепла - это единственный общий между ними признак. Можно ли для них создать общий абстрактный класс? Можно, но это не будет оптимальным решением, тем более у нас могут быть какие-то родственные сущности, которые мы, возможно, тоже захотим использовать. Поэтому для каждой вышеперечисленной сущности мы можем определить свою систему классификации. Например, в одной системе классов, которые наследуются от общего астрактного класса, были бы звезды, в том числе и солнце, планеты, астероиды и так далее - то есть все те объекты, которые могут иметь какое-то общее с солнцем состояние. В рамках другой системы классов мы могли бы определить электрические приборы, в том числе электронагреатель. И так, для каждой разноплановой сущности можно было бы составить свою систему классов, исходяющую от определенного абстрактного класса. А для общего действия определить интерфейс, например, IHeatable, в котором бы был метод Heat, и этот интерфейс реализовать во всех необходимых классах.
Таким образом, если разноплановые классы обладают каким-то общим действием, то это действие лучше выносить в интерфейс. А для одноплановых классов, которые имеют общее состояние, лучше определять абстрактный класс.
Сравнение абстрактных классов и интерфейсов в Java
В мире программирования на Java, мы часто сталкиваемся с понятиями абстрактных классов и интерфейсов. Но что это такое и в чем ключевые различия между этими двумя концепциями? Давайте разберемся!
Абстрактные классы в Java
Абстрактный класс — это класс, который содержит один или несколько абстрактных методов. Абстрактный метод — это метод, который объявлен, но не имеет реализации. Абстрактные классы не могут быть инстанцированы, они служат основой для подклассов.
Например, мы можем создать абстрактный класс «Животное» с абстрактным методом «издать звук». Подклассы «Собака» и «Кошка» будут иметь разные реализации этого метода.
abstract class Animal < abstract void makeSound(); >class Dog extends Animal < void makeSound() < System.out.println("woof"); >> class Cat extends Animal < void makeSound() < System.out.println("meow"); >>
Для более подробного изучения абстрактных классов рекомендуется прочесть статью Что такое абстрактные классы в Java и чем они отличаются от интерфейсов. А если вам интересно, могут ли абстрактные классы иметь конструкторы, обратите внимание на эту статью Могут ли абстрактные классы иметь конструкторы в Java?.
Интерфейсы в Java
Интерфейс в Java — это полностью абстрактный класс, который может содержать только абстрактные методы и поля. Интерфейс представляет собой декларацию методов, которые должны быть реализованы в классе, который его реализует.
interface Animal < void makeSound(); >class Dog implements Animal < public void makeSound() < System.out.println("woof"); >> class Cat implements Animal < public void makeSound() < System.out.println("meow"); >>
Подробнее о интерфейсах и использовании методов по умолчанию можно прочитать в статье Использование методов по умолчанию в интерфейсах Java 8+ и абстрактных методов.
Сравнение абстрактных классов и интерфейсов
Существуют ключевые различия между абстрактными классами и интерфейсами в Java:
- Множественная реализация: Класс может реализовать несколько интерфейсов, но может наследоваться только от одного абстрактного класса.
- Определение методов: Интерфейсы могут содержать только объявления методов, а абстрактные классы могут содержать и объявления, и реализации методов.
- Конструкторы и поля: Абстрактные классы могут иметь конструкторы и поля, в то время как интерфейсы не могут.
Заключение
Основная разница между абстрактными классами и интерфейсами заключается в том, что абстрактные классы предоставляют частичную реализацию, которую могут использовать подклассы, в то время как интерфейсы объявляют только методы, которые должны быть реализованы классами.