Чем интерфейс отличается от абстрактного класса

от admin

Зачем нужны интерфейсы и абстрактные классы

До недавнего времени мне было трудно понять, зачем же нужны абстрактные классы и интерфейсы и в чём между ними разница. Однако теперь на реальных примерах мне все же удалось это понять, и я готов поделиться своим видением с читателями. В качестве примера мы рассмотрим TypeScript.

Интерфейсы

Давайте представим, что у нас имеется автомастерская WorkShop. Она может ремонтировать 3 вида автомобилей BMW, Mercedes и Porsche. В этом случае наш код будет выглядеть следующим образом:

void необходимо указывать в случаях, когда наш метод ничего не возвращает

Мы создали 3 класса для каждого автомобиля. У каждого класса имеются методы fixEngine и ChangeWheels. Каждый метод в каждом классе реализован по-разному для разных марок автомобилей. Основной класс WorkShop на вход принимает vehicle, который может быть 3 разных типов, поэтому мы указываем их через знак |.

В данном случае мы конкретно указываем тип автомобилей в конструкторе, которые принимает Workshop. Если бы мы этого не сделали, тогда бы мы не были уверены в том, что объект в переменной vehicle имеет методы fixEngine и changeWheels, что привело бы к ошибкам.

Но теперь возникает проблема — что будет, если наша мастерская через год начнет обслуживать дополнительно 10 типов автомобилей? В этом случае наш конструктор будет выглядеть как-то так:

Согласитесь, вариант не очень привлекательный. Однако решение есть! Мы можем использовать интерфейс Vehicle.

Интерфейс — это элемент, в котором можно объявить методы, которые должны быть реализованы в объекте.

Мы можем как имплементить(implements) интерфейс, так и указать его в качестве типа в аргументах конструктора Workshop. В случае с динамическими языками типа PHP единственным вариантом является использование implements(например, class BMW implements Vehicle; class Mercedes implements Vehicle и так далее). В JavaScript, например, интерфейсов нет вообще.

Теперь WorkShop может принимать любые объекты, у которых есть методы fixEngine и ChangeWheels. Ура! В этом заключается основная суть интерфейсов.

Абстрактный класс

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

Абстрактные классы — это такие классы, которые не могут создавать объекты и нужны лишь для того, чтобы дополнить работу основных классов и избавить нас от повторяющегося кода.

Абстрактными могут быть еще и свойства и методы. Если в классе реализован хотя бы один абстрактный метод или свойство, то класс необходимо обозначить как абстрактный.

Представим, что у нас есть два класса — принтер и сканер. В обоих случаях мы имеем повторяющийся код — одинаковые методы turnOn и Loading.

Как решить проблему? Можно создать класс Device, в котором реализуем методы turnOn и Loading, а зачем через наследование будем обращаться к нашим методам. Но в таком случае нам каждый раз нужно будет создавать сначала объект типа Device, а уже потом Printer и Scaner — неудобно. Можно использовать абстрактный класс Device.

Таким образом мы избавились от повторений в коде, и при этом нет необходимости создавать объект родительского класса Device (let device = new Device());

Абстрактные методы, например, полезны в случаях, если у нас в методе Loading, например, идет обращение к методу класса Scaner или Printer.

В примере в методе Loading мы вызываем метод cleanRAM, который в каждом случае имеет различную реализацию. Без указания абстрактного метода в классе Device TypeScript не понимает, что это за метод такой cleanRAM.

Таким образом, абстрактные классы и интерфейсы позволяют эффективно избавляться от повторений в коде. Делитесь мнением в комментариях и не забывайте подписываться на меня в Twitter.

Kotlin. Абстрактные классы и интерфейсы.

Абстрактные классы и интерфейсы объединены мной в одну тему, так как по своей сути они очень похожи. И те и другие имеют отношение к “моделированию” классов. С их помощью мы можем показать, что у определённой группы классов есть что-то общее: то, что их отличает от всех остальных. Ключевая разница между ними лишь в том, как их применять.

Абстрактные классы

Абстрактный класс — это класс, представляющий из себя “заготовку” для целого семейства классов, который описывает для них общий шаблон поведения. Такой класс не может быть создан. Т.е. нельзя создать его экземпляр. Он используются исключительно в качестве суперкласса, а его цель — моделирование поведения своего семейства, а также предоставление функционала для повторного использования. Другими словами абстрактный класс — это средство, для повторного использования кода.

Например, вы разрабатываете приложение, которое предоставляет информацию о деревьях. Тогда класс Tree должен быть абстрактным и объединять в себе свойства и функции, характерные для всех деревьев. А каждый наследник класса Tree будет по-своему их реализовывать.

Объявляется абстрактный класс при помощи ключевого слова abstract .

Наследование от такого класса осуществляется с помощью оператора : . При этом абстрактному классу не нужен модификатор open , потому что он “открыт” для наследования по умолчанию.

В теле класса можно объявлять абстрактные свойства и функции. Это полезно, когда часть поведения класса не имеет смысла без реализации в более конкретном подклассе.

Каждый наследник обязан переопределять их все.

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

Так как этот компонент класса уже не будет абстрактным, наследники не смогут его переопределить.

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

У абстрактного класса может быть конструктор.

Тогда каждый наследник должен предоставить для него значения.

Интерфейсы

Интерфейс — это совокупность методов и правил, которые определяют поведение класса или общее поведение для группы независимых друг от друга классов. Интерфейсы похожи на абстрактные классы тем, что нельзя создать их экземпляры и они могут определять абстрактные или конкретные функции и свойства. Отличие в том, что интерфейсу не важна связь “родитель-наследник”, он задаёт лишь правила поведения.

К примеру, в нашем приложении уже есть целое семейство деревьев, информацию о которых можно с лёгкостью получить — их название и краткое описание. Но помимо этого деревья могут цвести и производить кислород. Такие методы можно добавить в абстрактный класс Tree , но что если мы решим помимо деревьев предоставлять информацию о, скажем, водорослях. У них тоже есть период цветения и они тоже производят кислород, но они не являются деревьями, а значит мы не можем наследовать их от класса Tree . Таким образом мы получили группу независимых друг от друга классов с одинаковым поведением, которое будет реализовываться через интерфейс Cultivable .

Объявляется интерфейс ключевым словом interface .

Реализация интерфейса осуществляется аналогично наследованию: после имени класса ставится оператор : и название интерфейса.

В теле интерфейса можно определять абстрактные свойства и функции. Для этого не требуется использовать ключевое слово abstract , так как Kotlin способен сам понять, что свойство и функция без реализации должны быть абстрактными. Также обратите внимание, что единственный способ определить свойство — это определить его в теле интерфейса, так как у интерфейса не бывает конструкторов.

Класс должен реализовывать все абстрактные свойства и функции, определённые в интерфейсе.

При этом если интерфейс реализовывается в абстрактном классе, то свойства и функции интерфейса могут быть в нём опущены. Тогда все наследники абстрактного класса должны будут их переопределять.

В интерфейсе можно определять свойства и функции с конкретной реализацией (по умолчанию). Классы, реализующие этот интерфейс, могут использовать реализацию по умолчанию или определить свою. При этом реализация свойств осуществляется с помощью метода доступа get() .

Один интерфейс может реализовать другой интерфейс, при этом будет иметь доступ к его свойствам и функциям.

Каждый класс, реализующий интерфейс Cultivable может использовать свойства и функции интерфейса Fruitable , если в этом есть необходимость.

Шпаргалка: абстрактный класс или интерфейс?

  • У вас есть семейство классов, из которых можно выделить общую сущность? Определите эту сущность в качестве абстрактного класса и она будет “заготовкой” для всего семейства.
  • Вам нужно создать более конкретную версию класса? Создайте подкласс этого класса и добавьте недостающее поведение.
  • Требует определить общее поведение для группы независимых друг от друга классов? Создайте интерфейс и реализуйте его теми классами, которым необходимо это поведение.

Ключевые моменты

  • Абстрактный класс — это “заготовка” для целого семейства классов.
  • Нельзя создать экземпляр абстрактного класса.
  • Абстрактный класс может содержать как абстрактные, так и конкретные реализации свойств и функций.
  • Класс, который содержит абстрактное свойство или функцию, должен быть объявлен абстрактным.
  • Абстрактный класс может быть без единого абстрактного свойства или функции.
  • У класса может быть только один суперкласс.
  • Наследники абстрактного класса должны переопределять все его абстрактные свойства и функции.
  • Чтобы наследники могли переопределять конкретные реализации свойств и функций, для них в абстрактном классе должен быть явно указан модификатор open .
  • У абстрактного класса может быть конструктор.
  • Интерфейс определяет поведение класса или общее поведение для группы независимых друг от друга классов.
  • Нельзя создать экземпляр интерфейса.
  • Интерфейс может содержать как абстрактные, так и конкретные реализации функций.
  • Свойства интерфейсов могут быть абстрактными, а могут иметь get() методы.
  • Класс может реализовывать несколько интерфейсов.
  • Класс должен реализовывать все абстрактные свойства и функции, определённые в интерфейсе.
  • Если интерфейс реализовывается абстрактным классом, то переопределение его абстрактных свойств и функций может быть передана наследникам абстрактного класса.
  • Интерфейс может реализовывать другой интерфейс.

Полезные ссылки

Abstract classes — официальная документация.
Абстрактные классы — перевод на русский (об абстрактных классах в самом низу статьи).
Interfaces — официальная документация.
Интерфейсы — перевод статьи про интерфейсы на русский.

Абстрактные классы и интерфейсы в Java

Абстрактные классы и интерфейсы встречаются повсюду как в Java-приложениях, так и в самом Java Development Kit (JDK). Каждый из них служит своей цели:

Интерфейс — это контракт, который должен быть реализован конкретным классом.

Абстрактный класс похож на обычный, но отличается тем, что может содержать абстрактные методы — методы без реализации, и нельзя создать экземпляр абстрактного класса.

Многие разработчики не видят разницы между интерфейсами и абстрактными классами, но на самом деле между ними есть весьма существенное различие.

Интерфейсы

Интерфейс — это контракт, который реализуется в некотором классе. У интерфейса не может быть состояния, поэтому в нем нельзя использовать изменяемые поля экземпляра. В интерфейсе могут быть только неизменяемые final-поля.

Когда использовать интерфейсы

Интерфейсы очень полезны для уменьшения связанности (coupling) кода и реализации полиморфизма. Для примера давайте взглянем на интерфейс List из JDK:

Как вы, вероятно, заметили, код весьма краток и лаконичен. Здесь мы видим сигнатуры методов, которые будут реализованы в конкретном классе, реализующем этот интерфейс.

Контракт интерфейса List реализуется классами ArrayList , Vector , LinkedList и другими.

При использовании полиморфизма тип переменной объявляем как List , и присваиваем ей любую из доступных реализаций. Например:

В этом случае в каждом классе присутствует своя реализация методов. И это отличный пример использования интерфейсов. Если вы заметили, что ряд ваших классов содержит одинаковые методы, но с разными реализациями, то стоит использовать интерфейс.

Переопределение метода интерфейса

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

Читать:
Как из моно сделать стерео схема

Рассмотрим следующий пример:

Результат будет следующий:

Обратите внимание еще раз, что методы интерфейса неявно абстрактны и их не нужно явно объявлять как abstract.

Неизменяемые переменные

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

Обратите внимание, что обе переменные неявно final и static . Это означает, что они являются константами, не зависят от экземпляра и не могут быть изменены.

При попытке изменить поля в интерфейсе Challenger , например, следующим образом:

будет ошибка компиляции:

Default-методы

После появления в Java 8 методов по умолчанию, некоторые разработчики решили, что интерфейсы стали абстрактными классами. Однако это не так, поскольку у интерфейсов не может быть состояния.

У методов по умолчанию может быть реализация, а у абстрактных методов — нет. Методы по умолчанию — результат появления лямбда-выражений и Stream API, но использовать их нужно с осторожностью.

В качестве примера default-метода из JDK можно привести метод forEach() из интерфейса Iterable . Вместо копирования кода этого метода во все реализации Iterable , мы можем переиспользовать метод forEach :

Любая реализация Iterable может использовать метод forEach() без необходимости реализации этого нового метода.

Давайте рассмотрим пример с методом по умолчанию:

Важно отметить, что у default-метода должна быть реализация и default-метод не может быть статическим.

Абстрактные классы

У абстрактных классов может быть состояние в виде изменяемых полей экземпляра. Например:

Абстрактные методы в абстрактных классах

Аналогично интерфейсам в абстрактных классах могут быть абстрактные методы. Абстрактный метод — это метод без тела (без реализации). Но в отличие от интерфейсов, абстрактные методы в абстрактных классах должны быть явно объявлены как абстрактные.

Попытка объявить метод без реализации и без ключевого слова 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).

Могут быть любые методы.

Задачка

Давайте изучим основные различия между интерфейсами и абстрактными классами с помощью небольшой задачки. Вы также можете посмотреть данный материал в формате видео (англ.).

В приведенном ниже коде объявлены интерфейс, абстрактный класс и используются лямбда-выражения.

Как вы думаете, какой будет вывод, когда мы запустим этот код? Выберите один из следующих вариантов:

Вариант 1

Вариант 2

Вариант 3

Вариант 5

Разбор задачи

Эта задачка демонстрирует понятия об интерфейсах, абстрактных методах и о некоторых других вещах. Давайте разберем код строка за строкой.

В первой строке main() присутствует лямбда-выражение для интерфейса Zombie. Обратите внимание, что в этой лямбде мы инкрементируем статическое поле. Здесь также можно было использовать поле экземпляра, но не локальную переменную, объявленную вне лямбда-выражения. То есть код компилируется без ошибок. Также обратите внимание, что это лямбда-выражение еще не выполняется, оно только объявлено, и поле nemesisRaids не будет увеличено.

Далее мы выводим значение поля nemesisRaids , которое еще не увеличено. Следовательно, вывод будет:

Еще один интересный момент заключается в том, что мы используем анонимный внутренний класс. Мы создаем не экземпляр абстрактного класса Nemesis , но экземпляр анонимного класса, расширяющего Nemesis . Также обратите внимание, что первый конкретный класс в иерархии наследования всегда будет обязан реализовать абстрактные методы.

В интерфейсе Zombie есть поле с типом интерфейса Zombie , объявленное с помощью лямбда-выражения. Поэтому, когда мы вызываем метод Zombie.zombie.shoot() , получим следующий вывод:

В следующей строке вызывается лямбда-выражение, которое мы создали в начале. Следовательно, переменная nemesisRaids будет увеличена. Однако, поскольку мы используем оператор постинкремента, она будет увеличена только после этого выражения. Следующий вывод будет:

Далее вызовем метод shoot для nemesis , который изменяет поле экземпляра shoots на 23. Обратите внимание, что как раз здесь мы видим основную разницу между интерфейсом и абстрактным классом.

Наконец, мы выводим значение nemesis.shoots и nemesisRaids .

Правильный ответ — вариант 3:

Материал подготовлен в преддверии старта специализации Java-разработчик.

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

Разница между абстрактными классами и интерфейсами

Возьмем для примера абстрактный класс Bird и интерфейс Flyable :

Давай создадим класс птицы Mockingjay (сойка-пересмешница) и унаследуем его от Bird :

Как видишь, мы легко получаем доступ к состоянию абстрактного класса — к его переменным species (вид) и age (возраст).

Но если мы попытаемся сделать это же с интерфейсом, картина будет другой. Можем попробовать добавить в него переменные:

У нас даже не получится создать внутри интерфейса private-переменные. Почему? Потому что private-модификатор создали, чтобы скрывать реализацию от пользователя. А внутри интерфейса реализации нет: там и скрывать нечего.

Интерфейс только описывает поведение. Соответственно, мы не сможем реализовать внутри интерфейса геттеры и сеттеры. Такова природа интерфейса: он нужен для работы с поведением, а не состоянием.

В Java8 появились дефолтные методы интерфейсов, у которых есть реализация. О них ты уже знаешь, поэтому повторяться не будем.

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

Вернемся к нашему примеру с птицами.

Наш абстрактный класс Bird нужен, чтобы на его основе создавать птиц. Только птиц и никого больше! Конечно, они будут разными.

С интерфейсом Flyable все обстоит по-другому. Он только описывает поведение, соответствующее его названию, — «летающий». Под определение «летающий», «способный летать» попадает много объектов, не связанных между собой.

Разница между абстрактными классами и интерфейсами - 3

Эти 4 сущности между собой никак не связаны. Что уж там говорить, не все из них даже являются одушевленными. Тем не менее, все они — Flyable , способные летать.

Мы бы не смогли описать их с помощью абстрактного класса. У них нет общего состояния, одинаковых полей. Чтобы дать характеристику самолету, нам, наверное, понадобятся поля «модель», «год выпуска» и «максимальное количество пассажиров». Для Карлсона — поля для всех сладостей, которые он сегодня съел, и список игр, в которые он будет играть с Малышом. Для комара. э-э-э. даже не знаем… Может, «уровень надоедливости»? 🙂

Главное, что с помощью абстрактного класса описать их мы не можем. Они слишком разные. Но есть общее поведение: они могут летать. Интерфейс идеально подойдет для описания всего на свете, что умеет летать, плавать, прыгать или обладает каким-то другим поведением.

Классы могут реализовывать сколько угодно интерфейсов, но наследоваться можно только от одного класса.

Об этом мы уже говорили не раз. Множественного наследования в Java нет, а множественная реализация есть. Отчасти этот пункт вытекает из предыдущего: интерфейс связывает между собой множество разных классов, у которых часто нет ничего общего, а абстрактный класс создается для группы очень близких друг другу классов. Поэтому логично, что наследоваться можно только от одного такого класса. Абстрактный класс описывает отношения «is a».

Стандартные интерфейсы InputStream & OutputStream

  • int available() возвращает число байт, доступных для чтения;
  • close() закрывает источник ввода;
  • int read() возвращает целочисленное представление следующего доступного байта в потоке. Если достигнут конец потока, будет возвращено число -1;
  • int read(byte[] buffer) пытается читать байты в буфер, возвращая количество прочитанных байтов. Когда он достигает конца файла, возвращает значение -1;
  • int read(byte[] buffer, int byteOffset, int byteCount) считывает часть блока байт. Используется, когда есть вероятность, что блок данных был заполнен не целиком. Когда он достигает конца файла, возвращает -1;
  • long skip(long byteCount) пропускает byteCount , байт ввода, возвращая количество проигнорированных байтов.
  1. FileInputStream : самый распространенный вид InputStream . Используется для чтения информации из файла;
  2. StringBufferInputStream : еще один полезный вид InputStream . Он превращает строку во входной поток данных InputStream ;
  3. BufferedInputStream : буферизированный входной поток. Чаще всего он используется для повышения эффективности.
  • int close() закрывает выходной поток;
  • void flush() очищает все буферы вывода;
  • abstract void write (int oneByte) записывает 1 байт в выходной поток;
  • void write (byte[] buffer) записывает массив байтов в выходной поток;
  • void write (byte[] buffer, int offset, int count) записывает диапазон из count байт из массива, начиная с позиции offset.

DataOutputStream . Выходной поток, включающий методы для записи стандартных типов данных Java.

Очень простой класс для записи примитивных типов Java и строк. Наверняка ты поймешь написанный код даже без объяснений:

У него есть отдельные методы для каждого типа — writeDouble() , writeLong() , writeShort() и так далее.

Класс FileOutputStream . Реализует механизм отправки данных в файл на диске. Мы, кстати, уже использовали его в прошлом примере, обратил внимание? Мы передали его внутрь DataOutputStream, который выступал в роли «обертки».

BufferedOutputStream . Буферизованный выходной поток. Тоже ничего сложного, суть та же, что и у BufferedInputStream (или у BufferedReader ’a). Вместо обычной последовательной записи данных используется запись через специальный «накопительный» буфер. Благодаря буферу можно сократить количество обращений к месту назначения данных и за счет этого повысить эффективность.

Опять же, ты можешь самостоятельно «поиграть» с этим кодом и проверить, как он будет работать на реальных файлах твоего компьютера.

Похожие статьи