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
Reader
Запис даних
OutputStream
Writer

Практичне застосування

Класи InputStream, OutputStream, Reader і Writer в явному вигляді ніхто не використовує: їх не приєднано до зовнішніх об'єктів, з яких можна читати дані або в які можна писати дані. Проте ці чотири класи мають чимало класів-спадкоємців, які дуже багато вміють.


2. Клас InputStream

Клас InputStream цікавий тим, що є батьківським класом для сотень класів-спадкоємців. У ньому самому немає ніяких даних, проте він має методи, які передає всім своїм класам-спадкоємцям.

Об'єкти-потоки взагалі рідко зберігають в собі дані. Потік — це інструмент для читання або запису даних, а не для їх зберігання. Проте бувають і винятки.

Методи класу InputStream і всіх його класів-спадкоємців:

Методи Опис
int read()
Читає один байт із потоку
int read(byte[] buffer)
Читає масив байтів із потоку
byte[] readAllBytes()
Читає всі байти з потоку
long skip(long n)
Пропускає n байтів у потоці (читає та відкидає)
int available()
Перевіряє, скільки ще байтів залишилося в потоці
void close()
Закриває потік

Побіжно перегляньмо ці методи:

Метод 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(). Приклад:

Код Примітка
String src = "c:\\projects\\log.txt";
String dest = "c:\\projects\\copy.txt";

try(FileInputStream input = new FileInputStream(src);
FileOutputStream output = new FileOutputStream(dest))
{
   byte[] buffer = new byte[65536]; // 64Kb
   while (input.available() > 0)
   {
      int real = input.read(buffer);
      output.write(buffer, 0, real);
   }
}



InputStream для читання з файлу
OutputStream для запису у файл

Буфер, в який зчитуються дані
Доки в потоці є дані

Зчитуємо дані в буфер
Записуємо дані з буфера в другий потік

У цьому прикладі ми використали два класи: для читання даних з файлу — клас FileInputStream, спадкоємець класу InputStream, а для запису даних у файл — клас FileOutputStream, спадкоємець класу OutputStream. Про другий клас розповімо трохи згодом.

Ще одна цікавинка — це змінна real. Коли з файлу буде прочитано останній блок даних, може виявитися, що його довжина менша за 64 Кб. Тому в output потрібно теж записати не весь буфер, а тільки його частину: перші real байтів. Саме це й робить метод write().



3. Клас Reader

Клас Reader — це повний аналог класу InputStream, тільки він працює не з байтами, а із символами — char. Клас Reader, так само, як і клас InputStream, сам по собі ніде не використовується: він є батьківським класом для сотень класів-спадкоємців, яким передає всі спільні методи.

Методи класу Reader (і всіх його класів-спадкоємців):

Методи Опис
int read()
Читає один символ char з потоку
int read(char[] buffer)
Читає масив символів char з потоку
long skip(long n)
Пропускає n символів char в потоці (читає та відкидає)
boolean ready()
Перевіряє, чи залишилися ще символи в потоці
void close()
Закриває потік

Методи дуже схожі на методи класу InputStream, хоча є незначні відмінності.

Метод int read()

Цей метод читає з потоку один символ char і повертає його. Тип char розширюється до типу int, але перші два байти результату завжди нульові.

Метод int read(char[] buffer)

Це ще одна модифікація методу read(). Цей метод дає змогу зчитати з Reader відразу масив символів. Масив для збереження символів потрібно передати як параметр. Метод повертає число — кількість реально прочитаних символів.

Метод skip(long n)

Цей метод дає змогу пропустити n перших символів з об'єкта Reader. Він працює так само, як аналогічний метод класу InputStream. Повертає число символів, які були реально пропущені.

Метод boolean ready()

Повертає true, якщо в потоці ще є непрочитані символи.

Метод void close()

Метод close() закриває потік даних і вивільняє пов'язані з ним зовнішні ресурси. Після закриття потоку читати з нього дані більше не можна.

Для порівняння напишімо програму, що копіює текстовий файл:

Код Примітка
String src = "c:\\projects\\log.txt";
String dest = "c:\\projects\\copy.txt";

try(FileReader reader = new FileReader(src);
FileWriter writer = new FileWriter(dest))
{
   char[] buffer = new char[65536]; // 128Kb
   while (reader.ready())
   {
      int real = reader.read(buffer);
      writer.write(buffer, 0, real);
   }
}



Reader для читання з файлу
Writer для запису у файл

Буфер, в який зчитуються дані
Доки в потоці є дані

Зчитуємо дані в буфер
Записуємо дані з буфера в другий потік