Вебинары Разобраться в IT Реферальная программа
Программирование Аналитика Дизайн Маркетинг Управление проектами
05 Май 2023
8 мин
4038

Что такое абстрактные классы в Java и чем они отличаются от интерфейсов

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

В объектно-ориентированном программировании есть четыре основополагающих принципа: композиция, полиморфизм, наследование и делегация. Их нужно эффективно реализовывать в 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 не поддерживает множественное наследование, чтобы избежать проблемы ромба.
Проверь как ты усвоил материалы статьи
Пройди тест и узнай насколько ты лучше других читателей

Добавить комментарий