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 для запису у файлБуфер, в який зчитуються дані Доки в потоці є дані Зчитуємо дані в буфер Записуємо дані з буфера в другий потік |
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ