Что такое абстрактный класс

от admin

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Интерфейсы

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Отличия абстрактного класса от интерфейса (abstract class and interface)

Каковы отличия между абстрактным классом и интерфейсом?

13 ответов 13

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

Интерфейс — это абстрактный класс, у которого ни один метод не реализован, все они публичные и нет переменных класса.

Интерфейс нужен обычно когда описывается только интерфейс (тавтология). Например, один класс хочет дать другому возможность доступа к некоторым своим методам, но не хочет себя «раскрывать». Поэтому он просто реализует интерфейс.

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

В некоторых языках (С++) специального ключевого слова для обозначения интерфейсов нет.

Можно считать, что любой интерфейс — это уже абстрактный класс, но не наоборот.

Arhadthedev's user avatar

tl;dr: Абстрактный класс — средство разработки классов на нижнем уровне, средство для повторного использования кода; интерфейс — средство выражения семантики класса. Таким образом, это совершенно разные, мало связанные между собой понятия.

Думайте об этом по-другому.

Абстрактный класс — это «заготовка» класса: реализовано большинство методов (включая внутренние), кроме нескольких. Эти несколько нереализованных методов вполне могут быть внутренними методами класса, они лишь уточняют детали имплементации. Абстрактный класс — средство для повторного использования кода, средство, чтобы указать, какой метод обязан быть перекрыт для завершения написания класса.

Интерфейс же — это своего рода контракт: интерфейсы используются в определениях чтобы указать, что объект, который будет использован на самом деле, должен реализовывать (для входных параметров) или будет гарантированно реализовывать (для выходных параметров) набор методов и (что намного важнее!) иметь определённую семантику. Интерфейс вполне может быть и пустым, тем не менее, имплементировать интерфейс означает поддерживать данную семантику.

Абстрактные классы идеологически схожи с шаблонами C++: и те, и другие являются заготовками классов, но шаблону для получения класса нужно специфицировать шаблонные типы, а абстрактному классу — абстрактные методы.

Интерфейсы идеологически схожи с заголовочными файлами C++: они раскрывают методы и скрывают конкретную реализацию.

Вопрос о том, является ли интерфейс или абстрактный класс собственно классом — техническая подробность реализации, зависящая от конкретного языка программирования. Например, в C++ интерфейсы отсутствуют вовсе, и их приходится эмулировать классами без данных. Абстрактный класс в C++ как таковой также отсутствует, но им можно считать любой класс с абстрактными методами. (Отсюда ограничение C++: как минимум 1 абстрактный метод в абстрактном классе.) Также в C++ можно (непрямо) инстанциировать абстрактный класс, вызвать абстрактный метод и (возможно) получить ошибку времени выполнения. В C# интерфейсы и абстрактные классы встроены в язык.

Пример (на C#, конкретный язык значения не имеет):

VladD's user avatar

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

Абстрактные классы и интерфейсы имеют определенное отношение к наследованию, точнее к моделированию мира. С их помощью мы хотим выразить, что у определенной группы вещей в нашей системе есть что-то общее: некоторое общее поведение, которое отличает эту группу штуковин от всех остальных.

Допустим, для примера, мы хотим смоделировать кнопки в интерфейсе пользователя. Поскольку мы говорим об ООП, то мы выделим некоторый тип Кнопки с некоторым набором операций (которые определяют поведение) и скрытого состояния, на которое опирается поведение (да, скрытого состояния может и не быть). При этом мы можем выделить три вида операции:

Конкретная фиксированная операция, которая должна быть абсолютно стабильно для всех типов кнопок.

Конкретная операция с поведением по умолчанию (т.е. операция, чье поведение подходит для многих типов кнопок, но могут быть кнопки с другим поведением).

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

Другими словами, тип Кнопки может содержать невиртуальные методы (non-virtual methods), виртуальные методы (virtual methods) и абстрактные методы (abstract methods).

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

После того, как мы определили базовый тип, пришло время определить произвольные типы. И тут начинаются вопросы. Точнее, вопросов никаких не возникает, когда у типа есть лишь один непосредственный базовый тип или все базовые типы содержат лишь декларации операций. Не проблема, унаследовать «Кнопку меню» от «Кнопки» и переопределить метод «Нажать на кнопку». Но что, если наш тип «Кнопка меню» будет отнаследован от двух типов с одной и той же виртуальной операцией? Как переопределить лишь одну, а оставить другую? А как быть клиенту нового типа и различить, какую операцию вызвать? А что если у двух базовых типов есть поле с одним именем? А что если у одного базового типа метод «Нажать кнопку» реализован, а у другого – лишь описан в виде декларации?

Нет, все эти проблемы решаемы, и в С++, и Eiffel, и других языках программирования вы можете довольно гибко контролировать, что и как переопределять, что прятать, что выставлять наружу и как вызвать метод определенного базового типа. Но для авторов некоторых языков программирования подобная сложность показалась излишней, и они пошли на хитрость и отделили типы, которые содержат лишь декларации методов в отдельную категорию, и так появились интерфейсы.

Читать:
Чем процедура отличается от функции

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

Интерфейс – описывает некоторое семейство типов и содержит лишь декларации операций (да, я осознанно пишу слово «декларация», а не использую слово «контракт», которое в ООП имеет вполне определенное значение).

Абстрактный базовый класс описывает некоторое семейство типов, но помимо декларации операций может содержать реализации по умолчанию (виртуальные методы) и фиксированные операции (невиртуальные методы).

Конкретный класс описывает некоторое семейство типов, которое готово для использования клиентами. Такой класс не может содержать декларации операций и все его операции должны быть либо фиксированными (невиртуальные методы) или содержать реализацию по умолчанию (виртуальные методы). Есть еще один подвид конкретных классов – запечатанный (sealed) класс – это разновидность конкретного класса отнаследоваться от которого невозможно, а значит он может содержать лишь конкретные операции.

Выделение интерфейсов в отдельную категорию полезно не только с точки зрения упрощения реализации языков программирования, но и для выделения разных подходов к моделированию. Так, например, наследование классов моделирует отношение «Является» («Кнопка меню» ЯВЛЯЕТСЯ «Кнопкой»), а базовые классы обычно содержат определенный функционал, тесно связанный с функционалом производного класса. Базовые классы не просто моделируют группу типов, но и позволяют использовать повторно существующий функционал.

Интерфейсы же, по своей природе обладают меньшей связностью (low coupling), поскольку не обладают конкретным поведением, которое может осложнить жизнь класса-наследника. Интерфейсы также могут моделировать отношение «Является» («Кнопка меню» ЯВЛЯЕТСЯ «IКнопкой»), но могут определять и менее жесткое отношение «Может выполнять роль» (CAN DO). Например, интерфейс IEquatable из BCL определяет «дополнительное» поведение, которое говорит о возможности типов сравнивать значения объектов.

Введение в ООП с примерами на C#. Часть четвёртая. Абстрактные классы

В прошлых статьях серии «Введение в ООП» мы рассматривали полиморфизм (а также его нюансы на практике) и наследование. В этой мы поговорим о самой захватывающей части ООП-парадигмы — об абстрактных классах. В целом концепция абстрактных классов в C# ничем не отличается от таковой в других языках, но в C# работать с ней приходится несколько иначе.

Что такое абстрактные классы

В плане терминологии давайте доверимся MSDN:

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

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

Итак, попробуем создать абстрактный класс:

Попытаемся скомпилировать этот код:

Compile time error: Cannot create an instance of the abstract class or interface ‘InheritanceAndPolymorphism.ClassA’

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

Описание методов в абстрактном классе

Попробуем добавить в наш абстрактный класс немного кода:

С кодом класса никаких проблем нет, но скомпилировать снова не получается, потому что нельзя создавать экземпляры абстрактных классов.

Использование абстрактного класса в качестве базового

Давайте попробуем создать ещё один класс:

Вау. Теперь всё спокойно компилируется.

Что нужно запомнить: Мы можем унаследовать обычный класс от абстрактного.

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

Декларация методов в абстрактном классе

Теперь попробуем сделать вот так:

И… мы получаем ошибку компиляции:

Compile time error: ‘InheritanceAndPolymorphism.ClassA.YYY()’
must declare a body because it is not marked abstract, extern, or partial

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

…и снова получим ошибку компиляции:

Compiler error: ‘InheritanceAndPolymorphism.ClassB’ does not implement
inherited abstract member ‘InheritanceAndPolymorphism.ClassA.YYY()’

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

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

Реализация абстрактного метода в производном классе

Так, давайте тогда попробуем реализовать метод YYY() в классе ClassB :

На первый взгляд всё отлично, правда? Но на этот раз мы получим сразу две ошибки компиляции:

Compile time error: ‘InheritanceAndPolymorphism.ClassB’ does not implement
inherited abstract member ‘InheritanceAndPolymorphism.ClassA.YYY()’

Compile time warning: ‘InheritanceAndPolymorphism.ClassB.YYY()’ hides
inherited member ‘InheritanceAndPolymorphism.ClassA.YYY()’.

Дело в том, что в C# нужно явно объявить, что мы реализуем абстрактный метод класса-родителя с помощью ключевого слова override :

Ура! ^_^ У нас наконец-то нет никаких ошибок!

Абстрактный метод базового класса и метод с override класса-наследника должны быть одинаковы

Это значит, что мы не можем менять тип возвращаемого значения или аргументы, которые передаются в метод. Например, если мы напишем такое:

То в консоли увидим следующую ошибку:

Compile time error: ‘InheritanceAndPolymorphism.ClassB.YYY()’: return type must be ‘void’
to match overridden member ‘InheritanceAndPolymorphism.ClassA.YYY()’

Инициализация переменных в абстрактных классах

В примерах выше, переменная int a будет обладать значением по умолчанию (0). Мы можем изменить его на нужное нам прямо в абстрактном классе — с этим не связано никаких особенностей.

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

Такой код не скомпилируется:

Compiler error: ‘InheritanceAndPolymorphism.ClassA.YYY()’ is abstract
but it is contained in non-abstract class ‘InheritanceAndPolymorphism.ClassA’

Что нужно запомнить: Абстрактные методы могут быть объявлены только в абстрактных классах.

Вызов абстрактного метода родителя

Compile time error : Cannot call an abstract base member:
‘InheritanceAndPolymorphism.ClassA.YYY()’

Разумеется, мы не можем исполнить код, которого не существует.

Абстрактный класс, который наследуется от другого абстрактного класса

Почему вывод именно такой, вы должны понимать из материалов третьей статьи этой серии.

Может ли абстрактный класс быть sealed

И получим ошибку:

Compile time error: ‘InheritanceAndPolymorphism.ClassA’:
an abstract class cannot be sealed or static

Разумеется, абстрактный класс не может быть sealed , т.к. он для того и создан, чтобы от него создавались производные классы.

Что нужно запомнить: Абстрактный класс не может иметь модификатор sealed .

Что нужно запомнить: Абстрактный класс не может иметь модификатор static .

Abstract Methods and Classes

An abstract class is a class that is declared abstract —it may or may not include abstract methods. Abstract classes cannot be instantiated, but they can be subclassed.

An abstract method is a method that is declared without an implementation (without braces, and followed by a semicolon), like this:

If a class includes abstract methods, then the class itself must be declared abstract , as in:

When an abstract class is subclassed, the subclass usually provides implementations for all of the abstract methods in its parent class. However, if it does not, then the subclass must also be declared abstract .

Abstract Classes Compared to Interfaces

Abstract classes are similar to interfaces. You cannot instantiate them, and they may contain a mix of methods declared with or without an implementation. However, with abstract classes, you can declare fields that are not static and final, and define public, protected, and private concrete methods. With interfaces, all fields are automatically public, static, and final, and all methods that you declare or define (as default methods) are public. In addition, you can extend only one class, whether or not it is abstract, whereas you can implement any number of interfaces.

Which should you use, abstract classes or interfaces?

  • Consider using abstract classes if any of these statements apply to your situation:
    • You want to share code among several closely related classes.
    • You expect that classes that extend your abstract class have many common methods or fields, or require access modifiers other than public (such as protected and private).
    • You want to declare non-static or non-final fields. This enables you to define methods that can access and modify the state of the object to which they belong.
    • You expect that unrelated classes would implement your interface. For example, the interfaces Comparable and Cloneable are implemented by many unrelated classes.
    • You want to specify the behavior of a particular data type, but not concerned about who implements its behavior.
    • You want to take advantage of multiple inheritance of type.

    An example of an abstract class in the JDK is AbstractMap , which is part of the Collections Framework. Its subclasses (which include HashMap , TreeMap , and ConcurrentHashMap ) share many methods (including get , put , isEmpty , containsKey , and containsValue ) that AbstractMap defines.

    An example of a class in the JDK that implements several interfaces is HashMap , which implements the interfaces Serializable , Cloneable , and Map<K, V> . By reading this list of interfaces, you can infer that an instance of HashMap (regardless of the developer or company who implemented the class) can be cloned, is serializable (which means that it can be converted into a byte stream; see the section Serializable Objects), and has the functionality of a map. In addition, the Map<K, V> interface has been enhanced with many default methods such as merge and forEach that older classes that have implemented this interface do not have to define.

    Note that many software libraries use both abstract classes and interfaces; the HashMap class implements several interfaces and also extends the abstract class AbstractMap .

    An Abstract Class Example

    In an object-oriented drawing application, you can draw circles, rectangles, lines, Bezier curves, and many other graphic objects. These objects all have certain states (for example: position, orientation, line color, fill color) and behaviors (for example: moveTo, rotate, resize, draw) in common. Some of these states and behaviors are the same for all graphic objects (for example: position, fill color, and moveTo). Others require different implementations (for example, resize or draw). All GraphicObject s must be able to draw or resize themselves; they just differ in how they do it. This is a perfect situation for an abstract superclass. You can take advantage of the similarities and declare all the graphic objects to inherit from the same abstract parent object (for example, GraphicObject ) as shown in the following figure.

    Classes Rectangle, Line, Bezier, and Circle Inherit from GraphicObject

    First, you declare an abstract class, GraphicObject , to provide member variables and methods that are wholly shared by all subclasses, such as the current position and the moveTo method. GraphicObject also declares abstract methods for methods, such as draw or resize , that need to be implemented by all subclasses but must be implemented in different ways. The GraphicObject class can look something like this:

    Each nonabstract subclass of GraphicObject , such as Circle and Rectangle , must provide implementations for the draw and resize methods:

    When an Abstract Class Implements an Interface

    In the section on Interfaces , it was noted that a class that implements an interface must implement all of the interface's methods. It is possible, however, to define a class that does not implement all of the interface’s methods, provided that the class is declared to be abstract . For example,

    In this case, class X must be abstract because it does not fully implement Y , but class XX does, in fact, implement Y .

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