Главная разница между абстрактным классом и интерфейсом в Java: абстрактный класс может хранить состояние (поля) и содержать готовую реализацию методов, а интерфейс описывает только поведение. От абстрактного класса можно унаследоваться лишь одного, а интерфейсов класс может реализовать сколько угодно. Абстрактный класс объединяет близкие по смыслу классы (отношение «is a»), а интерфейс задает общее поведение даже совершенно разным классам. Привет! В этой лекции поговорим о том, чем абстрактные классы отличаются от интерфейсов и рассмотрим примеры с распространенными абстрактными классами. Мы посвятили отличиям абстрактного класса от интерфейса отдельную лекцию, так как тема очень важная. О разнице между этими понятиями тебя спросят на 90% будущих собеседований. Поэтому обязательно разберись с прочитанным, а если что-то поймешь не до конца, почитай дополнительные источники.

Кратко

  • Абстрактный класс может хранить состояние (поля), интерфейс только описывает поведение.
  • От абстрактного класса можно унаследоваться только одного; интерфейсов класс реализует сколько угодно.
  • Абстрактный класс объединяет близкие по смыслу классы (отношение «is a»), интерфейс объединяет разные классы с общим поведением.
  • В интерфейсе нельзя объявить private-поля и написать геттеры-сеттеры: там нечего скрывать.
  • С Java 8 у интерфейсов есть default-методы с реализацией.
  • InputStream и OutputStream в Java это не интерфейсы, а абстрактные классы.
Абстрактный класс и интерфейс в Java: в чем разница

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

Итак, мы знаем, что такое абстрактный класс и что такое интерфейс. Теперь пройдемся по их отличиям. Кратко разницу удобно свести в таблицу:
КритерийАбстрактный классИнтерфейс
Состояние (поля)есть, может хранить состояниенет, только константы
Что описываети состояние, и поведениетолько поведение
Множественностьнаследуется только одинреализовать можно сколько угодно
Тип связиобъединяет близкие классы (отношение «is a»)объединяет разные классы по общему поведению
Методы с реализациейда, обычные методытолько default-методы (с Java 8)
Ключевое словоextendsimplements
  1. Интерфейс описывает только поведение. У него нет состояния. А у абстрактного класса состояние есть: он описывает и то, и другое.

    Возьмем для примера абстрактный класс Bird и интерфейс Flyable:

    public abstract class Bird {
       private String species;
       private int age;
    
       public abstract void fly();
    
       public String getSpecies() {
           return species;
       }
    
       public void setSpecies(String species) {
           this.species = species;
       }
    
       public int getAge() {
           return age;
       }
    
       public void setAge(int age) {
           this.age = age;
       }
    }

    Давай создадим класс птицы Mockingjay (сойка-пересмешница) и унаследуем его от Bird:

    public class Mockingjay extends Bird {
    
       @Override
       public void fly() {
           System.out.println("Лети, птичка!");
       }
    
       public static void main(String[] args) {
    
           Mockingjay someBird = new Mockingjay();
           someBird.setAge(19);
           System.out.println(someBird.getAge());
       }
    }

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

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

    public interface Flyable {
    
       String species = new String();
       int age = 10;
    
       public void fly();
    }
    
    public interface Flyable {
    
       private String species = new String(); // ошибка
       private int age = 10; // тоже ошибка
    
       public void fly();
    }

    У нас даже не получится создать внутри интерфейса private-переменные. Почему? Потому что private-модификатор создали, чтобы скрывать реализацию от пользователя. А внутри интерфейса реализации нет: там и скрывать нечего.

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

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

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

    Вернемся к нашему примеру с птицами.

    Наш абстрактный класс Bird нужен, чтобы на его основе создавать птиц. Только птиц и никого больше! Конечно, они будут разными.

    Абстрактный класс Bird объединяет только близкие классы, разных птиц

    С интерфейсом Flyable все обстоит по-другому. Он только описывает поведение, соответствующее его названию, «летающий». Под определение «летающий», «способный летать» попадает много объектов, не связанных между собой.

    Интерфейс Flyable объединяет разные не связанные объекты по общему поведению, умению летать

    Эти 4 сущности между собой никак не связаны. Что уж там говорить, не все из них даже являются одушевленными. Тем не менее, все они Flyable, способные летать.

    Мы бы не смогли описать их с помощью абстрактного класса. У них нет общего состояния, одинаковых полей. Чтобы дать характеристику самолету, нам, наверное, понадобятся поля «модель», «год выпуска» и «максимальное количество пассажиров». Для Карлсона поля для всех сладостей, которые он сегодня съел, и список игр, в которые он будет играть с Малышом. Для комара...э-э-э...даже не знаем… Может, «уровень надоедливости»? :)

    Главное, что с помощью абстрактного класса описать их мы не можем. Они слишком разные. Но есть общее поведение: они могут летать. Интерфейс идеально подойдет для описания всего на свете, что умеет летать, плавать, прыгать или обладает каким-то другим поведением.

  3. Классы могут реализовывать сколько угодно интерфейсов, но наследоваться можно только от одного класса.

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

Стандартные интерфейсы InputStream & OutputStream

Мы уже проходили различные классы, отвечающие за потоковый ввод и вывод. Давай рассмотрим InputStream и OutputStream. Вообще, никакие это не интерфейсы, а самые настоящие абстрактные классы. Теперь ты знаешь, что это такое, поэтому работать с ними будет намного проще :) InputStream это абстрактный класс, который отвечает за байтовый ввод. В Java есть серия классов, наследующих InputStream. Каждый из них настроен таким образом, чтобы получать данные из разных источников. Поскольку InputStream является родителем, он предоставляет несколько методов для удобной работы с потоками данных. Эти методы есть у каждого потомка InputStream:
  • int available() возвращает число байт, доступных для чтения;
  • close() закрывает источник ввода;
  • int read() возвращает целочисленное представление следующего доступного байта в потоке. Если достигнут конец потока, будет возвращено число -1;
  • int read(byte[] buffer) пытается читать байты в буфер, возвращая количество прочитанных байтов. Когда он достигает конца файла, возвращает значение -1;
  • int read(byte[] buffer, int byteOffset, int byteCount) считывает часть блока байт. Используется, когда есть вероятность, что блок данных был заполнен не целиком. Когда он достигает конца файла, возвращает -1;
  • long skip(long byteCount) пропускает byteCount, байт ввода, возвращая количество проигнорированных байтов.
Советую изучить полный перечень методов. Классов-наследников на самом деле больше десятка. Для примера приведем несколько:
  1. FileInputStream: самый распространенный вид InputStream. Используется для чтения информации из файла;
  2. StringBufferInputStream: еще один полезный вид InputStream. Он превращает строку во входной поток данных InputStream;
  3. BufferedInputStream: буферизированный входной поток. Чаще всего он используется для повышения эффективности.
Помнишь, мы проходили BufferedReader и говорили, что его можно не использовать? Когда мы пишем:
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))
…использовать BufferedReader не обязательно: InputStreamReader справится с задачей. Но BufferedReader делает это эффективнее и, к тому же, умеет читать данные целыми строками, а не отдельными символами. С BufferedInputStream все так же! Класс накапливает вводимые данные в специальном буфере без постоянного обращения к устройству ввода. Рассмотрим пример:
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;

public class BufferedInputExample {

   public static void main(String[] args) throws Exception {
       InputStream inputStream = null;
       BufferedInputStream buffer = null;

       try {

           inputStream = new FileInputStream("D:/Users/UserName/someFile.txt");

           buffer = new BufferedInputStream(inputStream);

           while(buffer.available()>0) {

               char c = (char)buffer.read();

               System.out.println("Был прочитан символ " + c);
           }
       } catch(Exception e) {

           e.printStackTrace();

       } finally {

           inputStream.close();
           buffer.close();
       }
   }
}
В этом примере мы читаем данные из файла, который находится на компьютере по адресу «D:/Users/UserName/someFile.txt». Создаем 2 объекта, FileInputStream и BufferedInputStream в качестве его «обертки». После этого мы считываем байты из файла и преобразуем их в символы. И так до тех пор, пока файл не закончится. Как видишь, ничего сложного здесь нет. Ты можешь скопировать этот код и запустить его на каком-то настоящем файле, который хранится на твоем компьютере :) Класс OutputStream это абстрактный класс, определяющий потоковый байтовый вывод. Как ты уже понял, это антипод InputStream’a. Он отвечает не за то, откуда считывать данные, а за то, куда их отправить. Как и InputStream, этот абстрактный класс предоставляет всем потомкам группу методов для удобной работы:
  • int close() закрывает выходной поток;
  • void flush() очищает все буферы вывода;
  • abstract void write (int oneByte) записывает 1 байт в выходной поток;
  • void write (byte[] buffer) записывает массив байтов в выходной поток;
  • void write (byte[] buffer, int offset, int count) записывает диапазон из count байт из массива, начиная с позиции offset.
Вот некоторые из потомков класса OutputStream:
  1. DataOutputStream. Выходной поток, включающий методы для записи стандартных типов данных Java.

    Очень простой класс для записи примитивных типов Java и строк. Наверняка ты поймешь написанный код даже без объяснений:

    import java.io.*;
    
    public class DataOutputStreamExample {
    
       public static void main(String[] args) throws IOException {
    
           DataOutputStream dos = new DataOutputStream(new FileOutputStream("testFile.txt"));
    
           dos.writeUTF("SomeString");
           dos.writeInt(22);
           dos.writeDouble(1.21323);
           dos.writeBoolean(true);
    
       }
    }

    У него есть отдельные методы для каждого типа: writeDouble(), writeLong(), writeShort() и так далее.

  2. Класс FileOutputStream. Реализует механизм отправки данных в файл на диске. Мы, кстати, уже использовали его в прошлом примере, обратил внимание? Мы передали его внутрь DataOutputStream, который выступал в роли «обертки».

  3. BufferedOutputStream. Буферизованный выходной поток. Тоже ничего сложного, суть та же, что и у BufferedInputStream (или у BufferedReader’a). Вместо обычной последовательной записи данных используется запись через специальный «накопительный» буфер. Благодаря буферу можно сократить количество обращений к месту назначения данных и за счет этого повысить эффективность.

    import java.io.*;
    
    public class DataOutputStreamExample {
    
       public static void main(String[] args) throws IOException {
    
           FileOutputStream outputStream = new FileOutputStream("D:/Users/Username/someFile.txt");
           BufferedOutputStream bufferedStream = new BufferedOutputStream(outputStream);
    
           String text = "I love Java!"; // эту строку мы преобразуем в массив байтов и запишем в файл
    
           byte[] buffer = text.getBytes();
    
           bufferedStream.write(buffer, 0, buffer.length);
           bufferedStream.close();
       }
    }

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

Также об InputStream и OutputStream и наследниках можно почитать в материале «Система ввода/вывода». О FileInputStream, FileOutputStream и BufferedInputStream у нас еще будет отдельная лекция, поэтому для первого знакомства информации о них достаточно. Вот и все! Надеемся, ты хорошо разобрался в отличиях между интерфейсами и абстрактными классами и готов ответить на любой вопрос, даже с подвохом :) Больше об абстрактных классах и методах можно почитать в документации Oracle (Abstract Methods and Classes).

Читайте также

Вопросы и ответы

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

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

Может ли класс реализовать несколько интерфейсов?

Да, сколько угодно. А вот унаследоваться в Java можно только от одного класса, в том числе абстрактного.

Может ли интерфейс иметь поля (состояние)?

Обычных полей-состояния у интерфейса нет. Переменные в нем это по сути константы, а private-поля и геттеры-сеттеры объявить нельзя.

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

Абстрактный класс, когда нужно объединить близкие классы с общим состоянием (отношение «is a»). Интерфейс, когда нужно задать общее поведение разным, не связанным между собой классам.

Могут ли методы интерфейса иметь реализацию?

С Java 8 да: это default-методы. Обычные методы интерфейса реализации не имеют, ее пишет класс, реализующий интерфейс.

InputStream и OutputStream это интерфейсы?

Нет, это абстрактные классы стандартной библиотеки Java. InputStream отвечает за байтовый ввод, OutputStream за байтовый вывод.