Перейти к основному содержимому

Что лучше, интерфейс или абстрактный класс в Java?

· 6 мин. чтения

1. Введение

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

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

2. Класс против интерфейса

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

Класс — это определяемый пользователем тип, который действует как план создания объекта. Он может иметь свойства и методы, которые представляют состояния и поведение объекта соответственно.

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

В дополнение к этому, новые функции Java 8 поддерживают статические методы и методы по умолчанию в интерфейсах для поддержки обратной совместимости. Методы в интерфейсе неявно абстрактны, если они не являются статическими или стандартными , и все они общедоступны .

Однако, начиная с Java 9, мы также можем добавлять приватные методы в интерфейсы.

3. Интерфейс против абстрактного класса

Абстрактный класс — это не что иное, как класс, объявленный с использованием ключевого слова abstract . Он также позволяет нам объявлять сигнатуры методов с помощью ключевого слова abstract (абстрактный метод) и заставляет его подклассы реализовывать все объявленные методы. Предположим, что если у класса есть абстрактный метод, то и сам класс должен быть абстрактным.

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

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

Абстрактные классы в некотором роде аналогичны интерфейсам:

  • Мы не можем создать экземпляр ни одного из них. т. е. мы не можем использовать оператор new TypeName() непосредственно для создания экземпляра объекта. Если бы мы использовали вышеупомянутый оператор, мы должны были бы переопределить все методы, используя анонимный класс .
  • Оба они могут содержать набор методов, объявленных и определенных с их реализацией или без нее. т. е. статические методы и методы по умолчанию (определенные) в интерфейсе, методы экземпляра (определенные) в абстрактном классе, абстрактные методы (объявленные) в обоих из них

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

Давайте рассмотрим некоторые сценарии, когда следует использовать интерфейс:

  • Если проблему необходимо решить с помощью множественного наследования и она состоит из разных иерархий классов
  • Когда несвязанные классы реализуют наш интерфейс. Например, Comparable предоставляет метод compareTo() , который можно переопределить для сравнения двух объектов .
  • Когда функциональные возможности приложения должны быть определены как контракт, но не заботиться о том, кто реализует поведение. т.е. сторонние поставщики должны реализовать его полностью

Рассмотрите возможность использования интерфейса, когда наша проблема делает утверждение «A способен [делать это]» . Например, «Clonable может клонировать объект», «Drawable может рисовать фигуру» и т. д.

Давайте рассмотрим пример, использующий интерфейс:

public interface Sender {
void send(File fileToBeSent);
}
public class ImageSender implements Sender {
@Override
public void send(File fileToBeSent) {
// image sending implementation code.
}
}

Здесь Sender — это интерфейс с методом send() . Следовательно, «Отправитель может отправить файл» мы реализовали его как интерфейс. ImageSender реализует интерфейс для отправки изображения цели. Далее мы можем использовать описанный выше интерфейс для реализации VideoSender и DocumentSender для выполнения различных задач.

Рассмотрим модульный тест, в котором используется вышеуказанный интерфейс и его реализованный класс:

@Test
void givenImageUploaded_whenButtonClicked_thenSendImage() {

File imageFile = new File(IMAGE_FILE_PATH);

Sender sender = new ImageSender();
sender.send(imageFile);
}

5. Когда использовать абстрактный класс

Теперь давайте посмотрим на некоторые сценарии, когда следует использовать абстрактный класс:

  • При попытке использовать концепцию наследования в коде (общий код среди многих связанных классов), предоставляя общие методы базового класса, которые подклассы переопределяют.
  • Если у нас есть определенные требования и только частичные детали реализации
  • В то время как классы, которые расширяют абстрактные классы, имеют несколько общих полей или методов (для которых требуются непубличные модификаторы)
  • Если кто-то хочет иметь неокончательные или нестатические методы для изменения состояний объекта

Рассмотрите возможность использования абстрактных классов и наследования, когда наша проблема делает доказательство «А есть Б». Например, «Собака — это животное», «Ламборджини — это автомобиль» и т. д.

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

public abstract class Vehicle {

protected abstract void start();
protected abstract void stop();
protected abstract void drive();
protected abstract void changeGear();
protected abstract void reverse();

// standard getters and setters
}
public class Car extends Vehicle {

@Override
protected void start() {
// code implementation details on starting a car.
}

@Override
protected void stop() {
// code implementation details on stopping a car.
}

@Override
protected void drive() {
// code implementation details on start driving a car.
}

@Override
protected void changeGear() {
// code implementation details on changing the car gear.
}

@Override
protected void reverse() {
// code implementation details on reverse driving a car.
}
}

В приведенном выше коде класс Vehicle был определен как абстрактный вместе с другими абстрактными методами. Он обеспечивает общие операции любого реального транспортного средства, а также имеет несколько общих функций. Класс Car , который расширяет класс Vehicle , переопределяет все методы, предоставляя детали реализации автомобиля («Car is a Vehicle»).

Следовательно, мы определили класс Vehicle как абстрактный, в котором функциональные возможности могут быть реализованы любым отдельным реальным транспортным средством, таким как автомобили и автобусы. Например, в реальном мире запуск автомобиля и автобуса никогда не будет одинаковым (для каждого из них нужны разные детали реализации).

Теперь давайте рассмотрим простой модульный тест, использующий приведенный выше код:

@Test
void givenVehicle_whenNeedToDrive_thenStart() {

Vehicle car = new Car("BMW");

car.start();
car.drive();
car.changeGear();
car.stop();
}

6. Заключение

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

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