User Professor Hans Noodles
Professor Hans Noodles
41 уровень

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

Статья из группы 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 у нас еще будет отдельная лекция, поэтому для первого знакомства информации о них достаточно. Вот и все! Надеемся, ты хорошо разобрался в отличиях между интерфейсами и абстрактными классами и готов ответить на любой вопрос, даже с подвохом :)
Комментарии (199)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ СДЕЛАТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
KOTNinja Уровень 19, Санкт-Петербург, Россия
15 октября 2021
Как только увидел InputStream/OutputStream - сразу статью пролистал вниз🤣🤣
Игорь Евгеньевич Уровень 28, Хабаровск, Россия
30 сентября 2021
Error: JavaFX runtime components are missing, and are required to run this application Вот что пишет IDEA на любую попытку поиграть с реальными файлами.
Карбофос Огарин Уровень 14, Санкт-Петербург
12 апреля 2021
Подскажите болезному - а в абстрактном классе обязательно объявлять геттеры и сеттеры? Без них в классах наследниках не будет доступа к ним?
Артём Уровень 25, Санкт-Петербург
18 февраля 2021
Отличная статья. Спасибо
Anonymous #2504305 Уровень 28, London
9 февраля 2021
“ Вот и все! Надеемся, ты хорошо разобрался в отличиях между интерфейсами и абстрактными классами и готов ответить на любой вопрос, даже с подвохом :) ” Да, конечно, бро!
Михаил Гараздюк Уровень 22, Prague, Чехия
7 февраля 2021
Я хотел прочитать статью о разнице между абстрактным классом и интерфейсом. Однако, здесь больше половины текста написано про стандартные интерфейсы InputStream & OutputStream, про их разновидности и методы. В свою очередь эти интерфейсы вовсе не являются интерфейсами, как оказалось: "Вообще, никакие это не интерфейсы, а самые настоящие абстрактные классы. " В общем, данная статья ни о чём. Лучше удалите. Не позорьтесь!
Alexey Khmelev Уровень 22, Владивосток
17 января 2021
Как правильно во входящем потоке указать путь файла? D:\Java\JavaRush\Files\Hello World.txt или D:/Java/JavaRush/Files/Hello World.txt
Oleg Viktorovich Уровень 26
10 января 2021
Подскажите, решил попробовать входной поток, набрал пример, но выдает не символы из файла а кракозябру (наблюдал такое когда менял в выходном потоке из другого примера (bufferedStream.write(buffer,0, buffer.length( <-- тут вручную уменьшал число байтов на 1));)) Затем попробывал занести 1 символ в файл, но получил еще дополнительные байты перед символом который я занес в файл (этот символ '1').

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

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

        try {
            inputStream = new FileInputStream("F:/ДЗ/TestFile/TestOneFile.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();
        }
    }
}
Вячеслав Уровень 25, Москва, Россия
5 января 2021
У меня одного не запускается код "Ты можешь скопировать этот код и запустить его на каком-то настоящем файле, который хранится на твоем компьютере"??? Выдает ошибки: "Error: Could not find or load main class BufferedInputExample Caused by: java.lang.ClassNotFoundException: BufferedInputExample" Подскажите, умные люди :)
🦔 Виктор Уровень 20, Москва, Россия Expert
26 декабря 2020
Начали за разницу между абстрактными классами и интерфейсами, а закончили стримами... Начало статьи потрясающее, очень наглядно и доходчиво всё объяснили, смогли донести в чём разница. Но потом какая-то каша из примеров со стримами, либо я сюда ранова-то попал (12 уровень). Заметил ссылку на Александра Климова, впечатляет его авторитет : ) Спасибо за труды. Выжимка: 1. Интерфейс описывает только поведение. У него нет состояния. А у абстрактного класса состояние есть: он описывает и то, и другое. 2. Абстрактный класс связывает между собой и объединяет классы, имеющие очень близкую связь. В то же время, один и тот же интерфейс могут реализовать классы, у которых вообще нет ничего общего.