1. Потоки даних
Програми рідко працюють самі по собі. Зазвичай вони так чи інакше взаємодіють із «зовнішнім світом». Це може бути зчитування даних із клавіатури, надсилання повідомлень, завантаження сторінок з інтернету або передавання файлів на віддалений сервер.
Така взаємодія називається процесом обміну даними між програмою та зовнішнім світом.
Процеси обміну даними можна розділити на два типи: отримання даних і надсилання даних. Наприклад, ви зчитуєте дані з клавіатури за допомогою об'єкта Scanner
— це отримання даних. А потім виводите дані на екран за допомогою команди System.out.println()
— це надсилання даних.
Для опису процесу обміну даними в програмуванні використовується термін «потік». Чому саме така назва?
У реальному житті може бути потік води або потік людей (людський потік). А потік у програмуванні — це потік даних.
Потоки — це універсальний інструмент. Вони дають програмі змогу отримувати дані звідки завгодно (вхідні потоки) і надсилати дані куди завгодно (вихідні потоки). Потоки поділяються на два види:
- Вхідний потік (Input): використовується для отримання даних
- Вихідний потік (Output): використовується для надсилання даних
Щоб потоки можна було «помацати руками», розробники Java створили два класи: InputStream
і OutputStream
.
Клас InputStream
має метод read()
, який дає змогу читати дані з вхідного потоку. А клас OutputStream
має метод write()
, який дає змогу записувати дані у вихідний потік. Ці класи мають також інші методи, але про них поговоримо трохи згодом.
Байтові потоки
Що ж це за дані та в якому вигляді їх можна читати? Інакше кажучи, які типи даних підтримуються цими класами?
Ці класи універсальні й тому підтримують найпоширеніший тип даних — byte
. В об'єкт OutputStream
можна записувати байти (і масиви байтів), а з об'єкта InputStream
можна читати байти (або масиви байтів). Це й усе — ніякі інші типи даних не підтримуються.
Тому такі потоки ще називають байтовими потоками.
Потоки мають таку особливість, що дані з них можна читати (або в них писати) тільки послідовно. Ви не можете прочитати дані із середини потоку, не прочитавши всі попередні дані.
Саме так працює читання з клавіатури з використанням класу Scanner
: ви читаєте дані з клавіатури послідовно — рядок за рядком. Прочитали один рядок, прочитали наступний рядок, прочитали наступний рядок і т. д. Саме тому метод читання рядка називається nextLine()
(дослівно — «наступний рядок»).
Запис даних у потік OutputStream
теж відбувається послідовно. Наочним прикладом є виведення на екран. Ви виводите один рядок, за ним іще один, потім іще один — це послідовне виведення. Ви не можете вивести перший рядок, потім десятий, а потім другий. Усі дані записуються в потік виводу тільки послідовно.
Символьні потоки
На попередніх рівнях ви дізналися, що рядки — це другий за популярністю тип даних, і це справді так. Дуже багато інформації передається у вигляді символів і цілих рядків. Комп'ютер чудово передавав би все у вигляді байтів, але людям це не дуже зручно.
Java-програмісти врахували цей факт і написали ще два класи: Reader
і Writer
. Клас Reader
— це аналог класу InputStream
, тільки його метод read()
читає не байти, а символи — char
. Клас Writer
відповідає класу OutputStream
і так само, як і клас Reader
, працює із символами (char
), а не з байтами.
Якщо порівняти ці чотири класи, отримаємо таку картину:
Байти (byte) | Символи (char) | |
---|---|---|
Читання даних |
|
|
Запис даних |
|
|
Практичне застосування
Класи InputStream
, OutputStream
, Reader
і Writer
в явному вигляді ніхто не використовує: їх не приєднано до зовнішніх об'єктів, з яких можна читати дані або в які можна писати дані. Проте ці чотири класи мають чимало класів-спадкоємців, які дуже багато вміють.
2. Клас InputStream
Клас InputStream
цікавий тим, що є батьківським класом для сотень класів-спадкоємців. У ньому самому немає ніяких даних, проте він має методи, які передає всім своїм класам-спадкоємцям.
Об'єкти-потоки взагалі рідко зберігають в собі дані. Потік — це інструмент для читання або запису даних, а не для їх зберігання. Проте бувають і винятки.
Методи класу InputStream
і всіх його класів-спадкоємців:
Методи | Опис |
---|---|
|
Читає один байт із потоку |
|
Читає масив байтів із потоку |
|
Читає всі байти з потоку |
|
Пропускає n байтів у потоці (читає та відкидає) |
|
Перевіряє, скільки ще байтів залишилося в потоці |
|
Закриває потік |
Побіжно перегляньмо ці методи:
Метод read()
Метод read()
читає один байт із потоку та повертає його. Вас може спантеличити тип результату — int
, проте так було зроблено тому, що тип int
є стандартним типом усіх цілих чисел. Три перші байти чисел типу int
дорівнюватимуть нулю.
Метод read(byte[] buffer)
Це ще одна модифікація методу read()
. Цей метод дає змогу зчитати з InputStream
одразу масив байтів. Масив для збереження байтів потрібно передати як параметр. Метод повертає число — кількість реально прочитаних байтів.
Припустімо, ви читаєте дані з файлу за допомогою класу FileInputStream
у буфер розміром 10 кілобайт. Якщо довжина файлу лише 2 кілобайти, всі дані буде записано в масив-буфер, а метод поверне число 2048 (2 кілобайти).
Метод readAllBytes()
Це дуже хороший метод. Він просто читає всі дані з InputStream
, доки вони не закінчаться, і повертає їх у вигляді цілісного масиву байтів. Дуже зручний для читання невеликих файлів. Великі файли можуть фізично не поміститися в пам'яті, і метод кине виняток.
Метод skip(long n)
Цей метод дає змогу пропустити n
перших байтів з об'єкта InputStream
. Оскільки дані читаються строго послідовно, цей метод просто читає n перших байтів із потоку і відкидає їх.
Повертає число байтів, які було реально пропущено (якщо потік закінчився раніше, ніж було перебрано n
байтів).
Метод int available()
Метод повертає решту байтів у потоці.
Метод void close()
Метод close()
закриває потік даних і вивільняє пов'язані з ним зовнішні ресурси. Після закриття потоку читати з нього дані більше не можна.
Напишімо для прикладу програму, що копіює дуже великий файл. Його неможливо цілком прочитати в пам'ять за допомогою методу readAllBytes()
. Приклад:
Код | Примітка |
---|---|
|
InputStream для читання з файлуOutputStream для запису у файлБуфер, в який зчитуються дані Доки в потоці є дані Зчитуємо дані в буфер Записуємо дані з буфера в другий потік |
У цьому прикладі ми використали два класи: для читання даних з файлу — клас FileInputStream
, спадкоємець класу InputStream
, а для запису даних у файл — клас FileOutputStream
, спадкоємець класу OutputStream
. Про другий клас розповімо трохи згодом.
Ще одна цікавинка — це змінна real
. Коли з файлу буде прочитано останній блок даних, може виявитися, що його довжина менша за 64 Кб. Тому в output
потрібно теж записати не весь буфер, а тільки його частину: перші real
байтів. Саме це й робить метод write()
.
3. Клас Reader
Клас Reader
— це повний аналог класу InputStream
, тільки він працює не з байтами, а із символами — char
. Клас Reader
, так само, як і клас InputStream
, сам по собі ніде не використовується: він є батьківським класом для сотень класів-спадкоємців, яким передає всі спільні методи.
Методи класу Reader
(і всіх його класів-спадкоємців):
Методи | Опис |
---|---|
|
Читає один символ char з потоку |
|
Читає масив символів char з потоку |
|
Пропускає n символів char в потоці (читає та відкидає) |
|
Перевіряє, чи залишилися ще символи в потоці |
|
Закриває потік |
Методи дуже схожі на методи класу InputStream
, хоча є незначні відмінності.
Метод int read()
Цей метод читає з потоку один символ char
і повертає його. Тип char
розширюється до типу int
, але перші два байти результату завжди нульові.
Метод int read(char[] buffer)
Це ще одна модифікація методу read()
. Цей метод дає змогу зчитати з Reader
відразу масив символів. Масив для збереження символів потрібно передати як параметр. Метод повертає число — кількість реально прочитаних символів.
Метод skip(long n)
Цей метод дає змогу пропустити n
перших символів з об'єкта Reader
. Він працює так само, як аналогічний метод класу InputStream
. Повертає число символів, які були реально пропущені.
Метод boolean ready()
Повертає true
, якщо в потоці ще є непрочитані символи.
Метод void close()
Метод close()
закриває потік даних і вивільняє пов'язані з ним зовнішні ресурси. Після закриття потоку читати з нього дані більше не можна.
Для порівняння напишімо програму, що копіює текстовий файл:
Код | Примітка |
---|---|
|
Reader для читання з файлуWriter для запису у файлБуфер, в який зчитуються дані Доки в потоці є дані Зчитуємо дані в буфер Записуємо дані з буфера в другий потік |