Интерфейс описывает только поведение. У него нет состояния. А у абстрактного класса состояние есть: он описывает и то, и другое.
Возьмем для примера абстрактный класс
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 появились дефолтные методы интерфейсов, у которых есть реализация. О них ты уже знаешь, поэтому повторяться не будем.
Абстрактный класс связывает между собой и объединяет классы, имеющие очень близкую связь. В то же время, один и тот же интерфейс могут реализовать классы, у которых вообще нет ничего общего.
Вернемся к нашему примеру с птицами.
Наш абстрактный класс
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
’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
:
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
’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
у нас еще будет отдельная лекция, поэтому для первого знакомства информации о них достаточно.
Вот и все! Надеемся, ты хорошо разобрался в отличиях между интерфейсами и абстрактными классами и готов ответить на любой вопрос, даже с подвохом :)
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ