1. Потоки даних
Будь-яка програма рідко існує сама по собі. Зазвичай вона якось взаємодіє із «зовнішнім світом». Це може бути зчитування даних з клавіатури, відправка повідомлень, завантаження сторінок з інтернету або, навпаки, завантаження файлів на віддалений сервер.
Усі ці речі ми можемо назвати одним словом — процес обміну даними між програмою та зовнішнім світом. Хоча це вже не одне слово.
Сам процес обміну даними можна поділити на два типи: отримання даних і відправка даних. Наприклад, ви зчитуєте дані з клавіатури за допомогою об'єкта Scanner — це отримання даних. І виводите дані на екран за допомогою команди System.out.println() — це відправка даних.
Для опису процесу обміну даними в програмуванні використовується термін потік. Звідки взагалі взялася така назва?
У реальному житті це може бути потік води або потік людей (людський потік). У програмуванні ж під потоком мається на увазі потік даних.
Потоки — це універсальний інструмент. Вони дозволяють програмі отримувати дані звідки завгодно (вхідні потоки) і відправляти дані куди завгодно (вихідні потоки). Діляться на два види:
- Вхідний потік (Input): використовується для отримання даних
- Вихідний потік (Output): використовується для відправки даних
Щоб потоки можна було «помацати руками», розробники Java написали два класи: InputStream та OutputStream.
У класу InputStream є метод read(), який дозволяє читати з нього дані. А у класу OutputStream є метод write(), який дозволяє записувати в нього дані. У них є й інші методи, але про це згодом.
Байтові потоки
Що ж це за дані і в якому вигляді їх можна читати? Іншими словами, які типи даних підтримуються цими класами?
О, це універсальні класи, і тому вони підтримують найпоширеніший тип даних — byte. У OutputStream можна записувати байти (і масиви байт), а з об'єкта InputStream можна читати байти (або масиви байт). Усе — жодні інші типи даних вони не підтримують.
Тому такі потоки ще називають байтовими потоками.
Особливість потоків у тому, що дані з них можна читати (писати) лише послідовно. Ви не можете прочитати дані з середини потоку, не прочитавши всі дані перед ними.
Саме так працює зчитування з клавіатури через клас Scanner: ви читаєте дані з клавіатури послідовно: рядок за рядком. Прочитали рядок, прочитали наступний рядок, прочитали ще один рядок і т.д. Тому метод зчитування рядка і називається nextLine() (дослівно — «наступний рядок»).
Запис даних у потік OutputStream також відбувається послідовно. Гарний приклад — виведення на екран. Ви виводите рядок, за нею ще одну і ще одну. Це послідовний вивід. Ви не можете вивести 1-й рядок, потім 10-й, а потім другий. Усі дані записуються в потік виводу тільки послідовно.
Символьні потоки
Нещодавно ви дізналися, що строки — другий за популярністю тип даних, і це дійсно так. Дуже багато інформації передається у вигляді символів і цілих рядків. Комп'ютер чудово передавав би все у вигляді байт, але люди не настільки ідеальні.
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 одразу масив байтів. Масив для збереження байтів потрібно передати як параметр. Метод повертає число — кількість реально зчитаних байтів.
Припустимо у вас буфер на 10 кілобайт, і ви читаєте дані із файлу за допомогою класу FileInputStream. Якщо файл містить лише 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 для запису у файлБуфер, у який будемо зчитувати дані Поки дані є у потоці Зчитуємо дані у буфер Записуємо дані з буфера у другий потік |
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ