В объектно-ориентированном программировании есть четыре основополагающих принципа: композиция, полиморфизм, наследование и делегация. Их нужно эффективно реализовывать в Java. Для этого есть специальные инструменты — абстрактные классы, и любому программисту на Java нужно понимать, что это такое и как грамотно использовать эти инструменты.
Абстрактные методы и классы
Абстрактный класс объявляется при описании класса: добавляют оператора abstract перед оператором class. Абстрактные классы Java нельзя инициализировать как объект, но от них можно наследоваться.
Пример объявления абстрактного класса:
public abstract class GraphicObject { // объявление полей // объявление не абстрактных методов abstract void draw(); }
Следующий код выдаст ошибку компиляции:
GraphicObject object = new GraphicObject();
Пример класса, наследующего от абстрактного класса:
public class Circle extends GraphicObject { void draw() { System.out.println(“Нарисовали круг.”); } }
Абстрактный метод Java — это метод, который объявлен, но в нём не описана логика, то есть метод не реализован. В таком случае нет фигурных скобок, а после объявления метода сразу идет точка с запятой.
Например:
abstract void moveTo(double deltaX, double deltaY);
Абстрактный класс необязательно должен содержать абстрактные методы. Но если какие-то методы класса объявлены абстрактными, то и он должен быть таким.
Следующий код выдаст ошибку компиляции:
public class GraphicObject { abstract void draw(); }
Если класс наследуется от абстрактного класса, в нём нужно описать реализацию всех абстрактных методов родительского класса. Если же какие-то из абстрактных методов родителя не реализованы, класс-наследник тоже должен быть абстрактным.
💡 Методы интерфейсов, которые не объявлены статическими (оператор static) или методами по умолчанию (оператор default), тоже абстрактные. Но задается это неявно: чтобы уменьшить количество кода, обычно не пишут оператор abstract у методов интерфейса. Хотя добавить его можно — это не вызовет ошибки компиляции.
Разобраться во всех тонкостях языка программирования Java можно на курсе Skypro «Java-разработчик». За несколько месяцев изучите типы данных и переменные, разберетесь в циклах и массивах. В программе — необходимый минимум теории и много практики. Уже в процессе обучения начнете писать код, тестировать его и исправлять ошибки. А результаты практических заданий сможете положить в портфолио, чтобы быстрее найти работу по новой специальности.
Абстрактные классы и интерфейсы: сравнение
Абстрактные классы очень похожи на интерфейсы. Интерфейсы, как и абстрактные классы, нельзя инициализировать в объект, и они могут содержать описание методов: с реализацией и без нее.
Но есть важные отличия. В интерфейсах все поля по умолчанию публичные, статические и неизменяемые, а методы могут быть только публичными. В абстрактном классе можно объявить нестатическое поле класса изменяемым и непубличным. А методы абстрактного класса могут иметь все допустимые уровни доступа:
- публичный (public);
- приватный (private);
- защищенный (protected);
- на уровне пакета (package).
Абстрактный класс может иметь внутреннее состояние с одинаковым кодом управления для всех его потомков. А правила доступа к логике абстрактного класса можно спроектировать более гибко.
Еще одно важное отличие абстрактного класса от интерфейса: в Java можно наследовать только один абстрактный класс, а интерфейсов реализовать множество.
Когда использовать абстрактный класс, а когда — интерфейс
Используйте абстрактный класс, если к вашей задаче применимо одно или несколько условий:
🔸 Необходимо распределить некоторый общий код между несколькими тесно связанными классами.
🔸 У классов-наследников много общих полей и методов с уровнями доступа, отличными от публичного.
🔸 У объекта класса какое-то внутреннее состояние. Это потребует объявления нестатических, изменяемых полей и методов доступа.
Используйте интерфейс, если к вашей задаче применимы утверждения:
🔹 Нужно объявить только контракт работы с каким-то типом данных без каких-либо указаний на его внутреннее состояние.
🔹 Объявленный контракт будет использоваться не только в связанных классах, но и в типах, которые напрямую не связаны с ним. Или даже в коде за пределами текущего приложения или библиотеки. Например, интерфейсы из стандартной библиотеки Java Comparable и Cloneable реализованы многими сторонними библиотеками.
🔹 Структура типа данных предполагает, что будет участвовать во множественном наследовании.
В качестве примера абстрактного класса из стандартной библиотеки Java приведем класс AbstractMap. У его наследников — HashMap, TreeMap и ConcurrentHashMap — много общих методов. Например, get, put, isEmpty, containsKey и containsValue, которые изначально были объявлены в AbstractMap.
Пример класса, который реализует несколько интерфейсов, — класс HashMap. Он реализует интерфейсы Serializable, Cloneable, и Map<K, V>. Независимо от конкретной реализации этого класса он будет поддерживать клонирование, конвертацию в массив байтов. А еще иметь функции таблицы: предоставлять доступ к внутренним данным по ключу.
Многие библиотеки используют абстрактный класс и интерфейсы одновременно. Например, класс HashMap реализует несколько интерфейсов и наследует класс AbstractMap.
Почему в Java нет множественного наследования классов
В объектно-ориентированных языках программирования множественным наследованием называется возможность описать класс, который наследуется от нескольких классов-родителей.
В отличие от других популярных ООП-языков, таких как C++, Java не поддерживает множественное наследование, так как оно вызывает проблему ромба — или, по-другому, проблему алмаза.
Проблема ромба
Рассмотрим пример наследования, где дерево классов можно представить диаграммой:
Допустим, у нас есть абстрактный класс SuperClass. В нём объявлен метод, который реализуется классами-наследниками ClassA и ClassB:
SuperClass.java
public abstract class SuperClass { public abstract void doSomething(); }
ClassA.java
public class ClassA extends SuperClass{ @Override public void doSomething(){ System.out.println("doSomething implementation of A"); } //ClassA own method</code> public void methodA(){</code> } }
ClassB.java
public class ClassB extends SuperClass{ @Override public void doSomething(){ System.out.println("doSomething implementation of B"); } //ClassB specific method public void methodB(){ } }
Теперь предположим, что при разрешенном множественном наследовании ClassC наследует оба класса: ClassA и ClassB.
ClassC.java
//Это гипотетический пример описания класса //В реальном компиляторе Java данный код вызовет //ошибку компиляции, public class ClassC extends ClassA, ClassB{ public void test(){ //calling super class method doSomething(); } }
Заметьте, что метод test() вызывает родительский метод doSomething(). Это ведет к неопределенности: мы не знаем, какой именно код будет выполнен в итоге, так как метод doSomething() реализован и в ClassA, и в ClassB. Компилятору потребуются дополнительные инструкции, чтобы разрешить эту ситуацию. Это и называется проблемой ромба.
Мы рассмотрели простой пример, но проблема ромба усугубляется, если наследоваться от трех и более классов, которые реализуют один и тот же метод. У разработчиков Java был выбор: простота языка или дополнительные языковые конструкции для поддержки множественного наследования. Они выбрали первое. Поэтому в Java нет множественного наследования, но его можно реализовать самостоятельно. Например, с помощью паттерна программирования «композиция».
Если хотите стать Java-разработчиком, пройдите курс Skypro. Учим с нуля даже тех, у кого вообще нет опыта в IT. Уже через 11 месяцев получите новую профессию, диплом о профпереподготовке, портфолио с проектами и работу. Не просто помогаем с поиском, а устраиваем на работу: гарантию фиксируем в договоре.
Пример абстрактного класса
Рассмотрим пример приложения для рисования. Его создают по принципам объектно-ориентированного программирования. В коде приложения нужно реализовать несколько классов, которые описывают графические объекты. Например, круг, прямоугольник, линию и кривую Безье.
У всех этих объектов должны быть общие атрибуты для описания состояния объекта:
- позиция относительно сетки координат;
- размер;
- ориентация;
- цвета обводки и цвет заполнения.
А еще методы для управления состоянием: передвинуть, повернуть, изменить размер, отрисовать.
Позиция, цвет заполнения и метод «передвинуть» будут одинаковыми для всех графических объектов. Другие придется реализовывать отдельно для каждого объекта. Например, «изменить размер» и «отрисовать».
Все графические объекты должны уметь менять размер и отрисовывать себя, но способы будут разные. Это идеальная ситуация, чтобы использовать абстрактный класс-родитель.
Воспользуемся преимуществами абстрактного класса и реализуем все общие атрибуты и методы в общем предке. Назовем его GraphicObject. А всю различающуюся логику опишем в каждом классе-наследнике отдельно.
Структура дерева классов будет такая:
Сначала объявим класс GraphicObject, чтобы описать общие для всех потомков поля и методы: текущая позиция и метод для ее изменения (moveTo). В классе GraphicObject тоже объявим абстрактные методы с уникальной реализацией для каждого потомка. Например, методы draw or resize для отрисовки и изменения размера объекта соответственно. Код класса GraphicObject будет выглядеть примерно так:
abstract class GraphicObject { int x, y; ... void moveTo(int newX, int newY) { ... abstract void draw(); abstract void resize(); }
Все неабстрактные классы-потомки GraphicObject (Circle и Rectangle) должны содержать реализацию методов draw и resize. Их код будет выглядеть примерно так:
class Circle extends GraphicObject { void draw() { ... } void resize() { ... } }
class Rectangle extends GraphicObject { void draw() { ... } void resize() { ... } }
Если абстрактный класс реализует интерфейс
Когда класс реализует интерфейс, в нём нужно имплементировать все методы интерфейса. Но если объявить такой класс абстрактным, то некоторые или все методы можно не имплементировать. Вместо этого их можно реализовать через классы-наследники. Пример:
interface Y { void methodA(); void methodB(); }
abstract class X implements Y { @Override void methodA() { System.out.println(“Inside method A in class X”); } }
class XX extends X { @Override void methodB() { System.out.println(“Inside method B in class XX”); } }
В этом примере класс Х обязательно нужно объявить абстрактным, потому что он реализует только один из методов интерфейса Y. А класс ХХ, наследуя от класса Х, должен обязательно реализовать methodB().
Члены класса
У абстрактного класса могут быть статические поля и методы. Доступ к таким членам класса получают через ссылку на класс. При этом не создается его экземпляр. Например:
AbstractClass.staticMethod().
Главное про разницу между интерфейсом и абстрактным классом
- Абстрактные классы похожи на интерфейсы, но в отличие от них поддерживают внутреннее состояние. А методы абстрактного класса могут иметь уровень доступа, отличный от публичного.
- Абстрактные классы — важная часть парадигмы ООП, реализованной в Java. Как и абстрактные методы, их задают с помощью оператора abstract.
- Абстрактные классы могут не содержать абстрактные методы и реализовывать интерфейсы.
- Java не поддерживает множественное наследование, чтобы избежать проблемы ромба.
Добавить комментарий