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 у нас еще будет отдельная лекция, поэтому для первого знакомства информации о них достаточно. Вот и все! Надеемся, ты хорошо разобрался в отличиях между интерфейсами и абстрактными классами и готов ответить на любой вопрос, даже с подвохом :)
Комментарии (289)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
JavaRusher853
Уровень 25
20 марта, 21:05
Ребята, поддержите меня лайком пожалуйста, I/O для меня больная тема(
Миша Рич
Уровень 33
22 февраля, 08:22
Благодаря статье, наконец разобрался с I/O.
Алексей Барищук
Уровень 34
Expert
18 февраля, 13:32
начали за здравие, закончили за упокой)
SobolenkoE Python Developer
23 января, 18:03
Говорили, что будет понятно, почему InputStream и OutputStream - это по сути интерфейсы, но все-же абстрактные классы. Обманули?
Alex
Уровень 28
2 января, 15:00
И каждый раз, когда я уходил от тебя, ты снова возвращалась 🚶
Максим Li Java Developer
12 ноября 2023, 23:38
Ок!
Denis Gritsay
Уровень 37
20 ноября 2023, 23:09
Ок!
Andrew Martelis
Уровень 26
27 ноября 2023, 15:42
Ок!
Anonymous cat
Уровень 30
Expert
14 декабря 2023, 17:05
Ok!
pulsarmn
Уровень 38
20 января, 13:40
Ок!
Sypher Team Lead в Бравлике Expert
27 января, 11:35
Ок!
Olexandr
Уровень 29
4 февраля, 17:17
Ок!
Кирилл
Уровень 28
20 февраля, 16:24
Ок!
Алексей
Уровень 26
21 марта, 11:36
Ок!
ElenaN
Уровень 37
28 октября 2023, 20:09
Вроде статья начиналась как статья про абстрактные классы и интерфейсы, а потом вдруг - а, кстати, помните пару уровней назад вы ничего не поняли про I/O? вот, держите!
Denis Gritsay
Уровень 37
20 ноября 2023, 23:36
поток мыслей)
24 декабря 2023, 11:10
😄
Olexandr
Уровень 29
4 февраля, 17:18
Прочитали поток моих мыслей)))
Василь trainee в Kindgeek
25 октября 2023, 13:59
мені одному здалося що стаття більше про ріадери, а не про абстрактні класи та інтерфейси?
Александра ;)
Уровень 32
6 сентября 2023, 13:07
Всем привет!! Изучая Java, в очередной раз осознала нехватку живого общения с единомышленниками =). Вот и решила создать группу в телеграмм, где Java - студенты смогут не только обсуждать вопросы по изучению Java, но и организовывать живые встречи. Кому интересно, вступайте =) https://t.me/JavaCommunityMoscow
Dmitry Vidonov
Уровень 29
Expert
4 сентября 2023, 04:49
Молодец Aditi! Продуктивная бабца!