JavaRush /Java блог /Java Developer /Разница между абстрактными классами и интерфейсами
Автор
Aditi Nawghare
Инженер-программист в Siemens

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

Статья из группы Java Developer
Привет! В этой лекции поговорим о том, чем абстрактные классы отличаются от интерфейсов и рассмотрим примеры с распространенными абстрактными классами. Разница между абстрактными классами и интерфейсами - 1Мы посвятили отличиям абстрактного класса от интерфейса отдельную лекцию, так как тема очень важная. О разнице между этими понятиями тебя спросят на 90% будущих собеседований. Поэтому обязательно разберись с прочитанным, а если что-то поймешь не до конца, почитай дополнительные источники. Итак, мы знаем, что такое абстрактный класс и что такое интерфейс. Теперь пройдемся по их отличиям.
  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 нужен, чтобы на его основе создавать птиц. Только птиц и никого больше! Конечно, они будут разными.

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

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

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

    Эти 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 у нас еще будет отдельная лекция, поэтому для первого знакомства информации о них достаточно. Вот и все! Надеемся, ты хорошо разобрался в отличиях между интерфейсами и абстрактными классами и готов ответить на любой вопрос, даже с подвохом :)
Комментарии (288)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
JavaRusher853 Уровень 23
20 марта 2024
Ребята, поддержите меня лайком пожалуйста, I/O для меня больная тема(
Миша Рич Уровень 33
22 февраля 2024
Благодаря статье, наконец разобрался с I/O.
Алексей Барищук Уровень 26 Expert
18 февраля 2024
начали за здравие, закончили за упокой)
SobolenkoE Уровень 30
23 января 2024
Говорили, что будет понятно, почему InputStream и OutputStream - это по сути интерфейсы, но все-же абстрактные классы. Обманули?
Alex Уровень 28
2 января 2024
И каждый раз, когда я уходил от тебя, ты снова возвращалась 🚶
Максим Li Уровень 36
12 ноября 2023
Ок!
ElenaN Уровень 37
28 октября 2023
Вроде статья начиналась как статья про абстрактные классы и интерфейсы, а потом вдруг - а, кстати, помните пару уровней назад вы ничего не поняли про I/O? вот, держите!
Василь Уровень 28
25 октября 2023
мені одному здалося що стаття більше про ріадери, а не про абстрактні класи та інтерфейси?
Александра ;) Уровень 32
6 сентября 2023
Всем привет!! Изучая Java, в очередной раз осознала нехватку живого общения с единомышленниками =). Вот и решила создать группу в телеграмм, где Java - студенты смогут не только обсуждать вопросы по изучению Java, но и организовывать живые встречи. Кому интересно, вступайте =) https://t.me/JavaCommunityMoscow
Dmitry Vidonov Уровень 29 Expert
4 сентября 2023
Молодец Aditi! Продуктивная бабца!