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 у нас еще будет отдельная лекция, поэтому для первого знакомства информации о них достаточно. Вот и все! Надеемся, ты хорошо разобрался в отличиях между интерфейсами и абстрактными классами и готов ответить на любой вопрос, даже с подвохом :)
Комментарии (215)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Darya Hom Уровень 25, Москва
5 января 2022
У меня из файла вместо русского текста читает ерунду какую-то 🤔: Был прочитан символ Ð Был прочитан символ Ÿ Был прочитан символ Ñ Был прочитан символ € Был прочитан символ Ð Был прочитан символ ¸ Был прочитан символ Ð Был прочитан символ ² Был прочитан символ Ð Был прочитан символ µ Был прочитан символ Ñ Был прочитан символ ‚
Evgeniy Уровень 17, Санкт-Петербург, Россия
13 декабря 2021
hey, пытаюсь записать текст в файл txt, пишет успешно, но при этом удаляет весь старый текст. как сохранить изначальный текст в файле и просто добавить строку из программы, допустим, с нового абзаца?
Aleksei Reinsalu Уровень 19, Таллинн, Эстония
6 декабря 2021
"IS A" , реализованное в виде родительских классов, абстрактных классов и интерфейсов.. само по себе многозначно. Если разобрать его значения более точно: 1) IS A == is variety of морская вода в стакане есть разновидность неизвестной воды в стакане Программная модель - родительский класс. 2) IS A == can be named "Летающие П" - серия гоночных яхт, все названия которых на букву "П" Но если мы увидим одну из них, мы не поймем, что это одна из ЛП. Название серии это абстрактная условность. Программная модель - абстрактный класс. 3) IS A == is able to || is capable of Умеет что-то, в определенных условиях делает что-то. Название конкретно, происхождение неизвестно. Программная модель - интерфейс.
Darya Hom Уровень 25, Москва
4 декабря 2021
🤯
Владимир Уровень 18, Москва, Россия
29 ноября 2021
Для тех, кто попадает сюда из темы про интерфейсы - просто прочитайте не надо задаваться вопросом "Почему тут пол темы про входные/выходные потоки". Принимайте к сведению и всё. Чуть позже попадёте сюда снова с темы про входные/выходные потоки и всё встанет на свои места.
Никита Уровень 28, Минск, Belarus
5 ноября 2021
что за жесть началась с перескакиванием с темы на тему?
BeetleGold Уровень 17, Россия
19 октября 2021

int close() закрывает выходной поток;
А с каких пор, метод close() - возвращает целочисленное значение?😂
KOTNinja Уровень 28, Санкт-Петербург, Россия
15 октября 2021
Как только увидел InputStream/OutputStream - сразу статью пролистал вниз🤣🤣
Игорь Евгеньевич Уровень 38, Хабаровск, Россия
30 сентября 2021
Error: JavaFX runtime components are missing, and are required to run this application Вот что пишет IDEA на любую попытку поиграть с реальными файлами.
Карбофос Огарин Уровень 14, Санкт-Петербург
12 апреля 2021
Подскажите болезному - а в абстрактном классе обязательно объявлять геттеры и сеттеры? Без них в классах наследниках не будет доступа к ним?