JavaRush /Java блог /Random UA /Різниця між абстрактними класами та інтерфейсами

Різниця між абстрактними класами та інтерфейсами

Стаття з групи Random UA
Вітання! У цій лекції поговоримо про те, чим абстрактні класи відрізняються від інтерфейсів та розглянемо приклади з поширеними абстрактними класами. Різниця між абстрактними класами та інтерфейсами - 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ми ще матимемо окрему лекцію, тому для першого знайомства інформації про них достатньо. От і все! Сподіваємося, ти добре розібрався у відмінностях між інтерфейсами та абстрактними класами і готовий відповісти на будь-яке питання, навіть із каверзою :)
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ