Ми присвятимо відмінностям абстрактного класу та інтерфейсу окрему лекцію, оскільки ця тема є дуже важливою. Про різницю між цими поняттями тебе спитають на 90% майбутніх співбесід. Тому обов'язково розберися з прочитаним, а якщо щось зрозумієш не до кінця, пошукай додаткові джерела. Отже, ми знаємо, що таке абстрактний клас і що таке інтерфейс. Тепер пройдемося за їхніми відмінностями.
Інтерфейс описує лише поведінку. Він не має стану. А в абстрактного класу стан є: він описує і те, й інше.
Візьмемо для прикладу абстрактний клас
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-модифікатор створили, щоб приховувати реалізацію від користувача. А всередині інтерфейсу реалізації немає: там і приховувати нема чого.
Інтерфейс лише описує поведінку. Відповідно, ми не зможемо реалізувати всередині інтерфейсу гетери та сеттери. Така природа інтерфейсу: він необхідний для роботи з поведінкою, а не зі станом.
У Java 8 з'явилися дефолтні методи інтерфейсів, які мають реалізацію. Про них ти вже знаєш, тож повторюватися не будемо.
Абстрактний клас пов'язує між собою та поєднує класи, що мають дуже близький зв'язок. У той самий час той самий інтерфейс можуть реалізувати класи, які взагалі не мають нічого спільного.
Повернемося до нашого прикладу з птахами.
Наш абстрактний клас
Birdпотрібний, щоб на його основі створювати птахів. Тільки птахів і нікого більше! Звісно, вони будуть різними.
З інтерфейсом
Flyableвсе по-іншому. Він лише описує поведінку, що відповідає його назві, – «літаючий». Під визначення «літаючий», «здатний літати» потрапляє багато об'єктів, які не пов'язані між собою.
Ці 4 сутності між собою ніяк не пов'язані. Що вже там казати, не всі з них навіть одухотворені. Тим не менш, усі вони
Flyable– здатні літати.Ми не змогли б описати їх за допомогою абстрактного класу. Вони не мають спільного стану, однакових полів. Щоб дати характеристику літаку, нам, напевно, знадобляться поля «модель», «рік випуску» та «максимальна кількість пасажирів». Для Карлсона – поля для всіх солодощів, які він сьогодні з'їв, і список ігор, у які він гратиме з Малим. Для комара... е-е-е... навіть не знаємо... Може, рівень докучливості? :)
Головне, що за допомогою абстрактного класу описати їх ми не можемо. Вони надто різні. Але є спільна поведінка: вони можуть літати. Інтерфейс ідеально підійде для опису всього на світі, що вміє літати, плавати, стрибати або має якусь іншу поведінку.
-
Класи можуть реалізовувати скільки завгодно інтерфейсів, але успадковуватися можна лише від одного класу.
Про це ми вже говорили неодноразово. Множинного успадкування в 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байтів введення, повертаючи кількість пропущених байтів.
FileInputStream: найпоширеніший видInputStream. Використовується для читання інформації з файлу;StringBufferInputStream: ще один корисний видInputStream. Він перетворює рядок на вхідний потік данихInputStream;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. Він відповідає не за те, звідки зчитувати дані, а за те, куди їх відправити. Як і InputStream, цей абстрактний клас надає всім нащадкам групу методів для зручної роботи:
void close()закриває вихідний потік;void flush()очищує всі буфери виведення;abstract void write(int oneByte)записує 1 байт у вихідний потік;void write(byte[] buffer)записує масив байтів у вихідний потік;void write(byte[] buffer, int offset, int count)записує діапазон ізcountбайтів з масиву, починаючи з позиціїoffset.
OutputStream:
-
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()тощо. -
Клас
FileOutputStream. Реалізує механізм надсилання даних у файл на диску. Ми, до речі, вже використовували його в минулому прикладі, звернув увагу? Ми передали його всерединуDataOutputStream, який виступав у ролі «обгортки». -
BufferedOutputStream. Буферизований вихідний потік. Теж нічого складного, суть та сама, що й уBufferedInputStream(або уBufferedReader). Замість звичайного послідовного запису даних використовується запис через спеціальний «накопичувальний» буфер. Завдяки буферу можна скоротити кількість звернень до місця призначення даних і за рахунок цього підвищити ефективність.```java 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 у нас ще буде окрема лекція, тому для першого знайомства інформації про них цілком достатньо.
Ось і все! Сподіваємося, ти добре розібрався у відмінностях між інтерфейсами й абстрактними класами та готовий відповісти на будь-яке запитання, навіть із підступом!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ