Источник: InfoWorld Сегодня вы узнаете, в каких случаях разработчику стоит использовать абстрактный класс, а в каких — интерфейс. Также мы определим разницу между этими элементами языка Java и выясним, как использовать их в программах. Кофе-брейк #140. Абстрактные классы и интерфейсы в Java - 1Абстрактные классы и интерфейсы достаточно распространены в коде Java и даже в самом Java Development Kit (JDK). Каждый из этих элементов служит своей цели:
  • Интерфейс — это конструкция в языке Java, которая помогает реализовать абстрактные методы и статические константы.
  • Абстрактные классы похожи на обычные классы, с тем отличием, что они могут включать абстрактные методы, то есть методы без тела. Абстрактные классы не могут быть созданы.
Многие разработчики считают, что интерфейсы и абстрактные классы похожи, но на самом деле это не совсем так. Давайте рассмотрим основные различия между ними.

Что такое интерфейс

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

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

Интерфейсы очень полезны для разделения кода и реализации полиморфизма. Мы можем это увидеть на примере в JDK с интерфейсом List:

public interface List<E> extends Collection<E> {

    int size();
    boolean isEmpty();
    boolean add(E e);
    E remove(int index);
    void clear();
}
Как вы, вероятно, заметили, этот код хотя и краткий, но весьма описательный. Мы можем легко увидеть сигнатуру метода, которая будет использоваться для реализации методов в интерфейсе с использованием конкретного класса. Интерфейс List содержит контракт, который можно реализовать классами ArrayList, Vector, LinkedList и другими. Чтобы использовать полиморфизм, мы можем просто объявить тип нашей переменной с помощью List, а затем выбрать любой из доступных экземпляров. Вот еще один пример:

List list = new ArrayList();
System.out.println(list.getClass());

 List list = new LinkedList();
 System.out.println(list.getClass());
На выводе получаем:
class java.util.ArrayList class java.util.LinkedList
В данном случае методы реализации для ArrayList, LinkedList и Vector различаются, что является отличным сценарием для использования интерфейса. Если вы заметили, что многие классы принадлежат родительскому классу с одними и теми же действиями методов, но с разным поведением. В таких ситуациях рекомендуется использовать интерфейс. Далее давайте рассмотрим несколько вариантов применения интерфейсов.

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

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

public class OverridingDemo {
  public static void main(String[] args) {
    Challenger challenger = new JavaChallenger();
    challenger.doChallenge();
  }
}

interface Challenger {
  void doChallenge();
}

class JavaChallenger implements Challenger {
  @Override
  public void doChallenge() {
    System.out.println("Challenge done!");
  }
}
Вывод:
Challenge done!
Обратите внимание, что методы интерфейса неявно абстрактные. Это означает, что нам не нужно явно объявлять их абстрактными.

Постоянные переменные

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

public class Challenger {
  
  int number = 7;
  String name = "Java Challenger";

}
Тут обе переменные являются неявными final и static. Это означает, что они являются константами, не зависят от экземпляра и не могут быть изменены. А теперь мы попытаемся изменить переменные в интерфейсе Challenger, скажем, вот так:

Challenger.number = 8;
Challenger.name = "Another Challenger";
Это вызовет ошибку компиляции:
Cannot assign a value to final variable 'number' Cannot assign a value to final variable 'name'

Методы по умолчанию

Когда в Java 8 появились методы по умолчанию, некоторые разработчики думали, что они будут такими же, как абстрактные классы. Однако это не так, потому что интерфейсы не могут иметь состояние. Метод по умолчанию может иметь реализацию, а абстрактные методы — нет. Методы по умолчанию являются результатом инноваций с лямбда-выражениями и потоками, но мы должны использовать их осторожно. Метод в JDK, использующий метод по умолчанию, — это forEach(), который является частью интерфейса Iterable. Вместо того, чтобы копировать код в каждую реализацию Iterable, мы можем просто повторно использовать метод forEach:

default void forEach(Consumer<? super T> action) { 
  // Code implementation here...
Любая реализация Iterable может использовать метод forEach(), не требуя новой реализации метода. Затем мы можем повторно использовать код с методом по умолчанию. Давайте создадим наш собственный метод по умолчанию:

public class DefaultMethodExample {

  public static void main(String[] args) {
    Challenger challenger = new JavaChallenger();
    challenger.doChallenge();
  }

}

class JavaChallenger implements Challenger { }

interface Challenger {

  default void doChallenge() {
    System.out.println("Challenger doing a challenge!");
  }
}
Результат:
Challenger doing a challenge!
В отношении методов по умолчанию важно отметить, что каждый такой метод нуждается в реализации. Метод по умолчанию не может быть статическим. А теперь перейдем к абстрактным классам.

Суть абстрактного класса

Абстрактные классы могут иметь состояние с переменными экземпляра. Это означает, что переменная экземпляра может использоваться и изменяться. Вот пример:

public abstract class AbstractClassMutation {

  private String name = "challenger";

  public static void main(String[] args) {
    AbstractClassMutation abstractClassMutation = new AbstractClassImpl();
    abstractClassMutation.name = "mutated challenger";
    System.out.println(abstractClassMutation.name);
  }

}

class AbstractClassImpl extends AbstractClassMutation { }
Вывод:
mutated challenger

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

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

public abstract class AbstractMethods {

  abstract void doSomething();

}
А вот попытка объявить метод без реализации и без ключевого слова abstract:

public abstract class AbstractMethods {
   void doSomethingElse();
}
К сожалению, она приводит к ошибке компиляции:
Missing method body, or declare abstract

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

Абстрактный класс рекомендуется использовать, когда вам нужно реализовать изменяемое состояние. Например, Java Collections Framework включает класс AbstractList, который использует состояние переменных. В тех случаях, когда вам не нужно поддерживать состояние класса, обычно лучше использовать интерфейс.

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

С точки зрения объектно-ориентированного программирования основное различие между интерфейсом и абстрактным классом заключается в том, что интерфейс не может иметь состояние, тогда как абстрактный класс может иметь состояние с переменными экземпляра. Другое ключевое отличие состоит в том, что классы могут реализовывать более одного интерфейса, но они могут расширять только один абстрактный класс. Это решение основано на том факте, что множественное наследование (расширение более чем одного класса) может привести к взаимоблокировке кода. Разработчики языка Java решили этого избежать. Еще одно отличие состоит в том, что интерфейсы могут быть реализованы классами или расширены интерфейсами, а классы могут быть только расширены. Важно отметить, что лямбда-выражения могут использоваться только с функциональным интерфейсом (имеется в виду интерфейс только с одним методом), в то время как абстрактные классы только с одним абстрактным методом не могут использовать лямбда-выражения. Вот еще несколько различий между абстрактными классами и интерфейсами. Интерфейс:
  • Может иметь только конечные статические переменные. Интерфейс никогда не может изменить свое собственное состояние.
  • Класс может реализовывать несколько интерфейсов.
  • Может быть реализован с помощью ключевого слова implements. Интерфейс может расширять другой интерфейс.
  • Для методов можно использовать только статические конечные поля, параметры или локальные переменные.
  • Только функциональные интерфейсы могут использовать функцию лямбда в Java.
  • Не может иметь конструктор.
  • Может иметь абстрактные методы.
  • Может иметь методы по умолчанию и статические (представлены в Java 8).
  • Может иметь частные методы с реализацией (представлено в Java 9).
Абстрактные классы:
  • Могут иметь любые экземпляры или статические переменные, изменяемые или неизменяемые.
  • Класс может расширять только один абстрактный класс.
  • Могут иметь экземпляр изменяемых полей, параметров или локальных переменных.
  • Абстрактные классы только с одним абстрактным методом не могут использовать лямбда-выражения.
  • Могут иметь конструктор.
  • Могут иметь любые методы.