JavaRush /Курси /Java Syntax Zero /Потоки для введення даних

Потоки для введення даних

Java Syntax Zero
Рівень 16 , Лекція 2
Відкрита

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
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 одразу масив байтів. Масив для збереження байтів потрібно передати як параметр. Метод повертає число — кількість реально зчитаних байтів.

Припустимо у вас буфер на 10 кілобайт, і ви читаєте дані із файлу за допомогою класу FileInputStream. Якщо файл містить лише 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]; // 64Kb
   while (reader.ready())
   {
      int real = reader.read(buffer);
      writer.write(buffer, 0, real);
   }
}



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

Буфер, у який будемо зчитувати дані
Поки дані є у потоці

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

Коментарі (25)
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ
Ігор Рівень 51
13 жовтня 2025
Під час роботи з цим Рівнем №24, НЕ ВИРІШУЙТЕ ЗАДАЧІ ОДРАЗУ, прочитайте весь рівень, тут всього 5 лекцій, а лише потім поверніться до практики - майже всі задачі зроблені на матеріалах майбутніх лекцій, тобто віднесіться до задач як до сторінки з практикою на завершення рівня. Збережете купу нервів :)
RMykola Рівень 20
9 березня 2025
Це дуже обурливо. Цілу лекцію даємо матреріал. Але в задачі ви його не можете використовувати. Можна використовувати лише матеріал, який ми ще не проходили. Прекрасно.
Alexander Kisil Рівень 51
26 жовтня 2024
Фейсконтроль задача - це щось!! (в нехорошому сенсі) 1) При використанні replaceAll - "Задача не компілюється на сервері" (хоча я звик, що у мене щось не компвлюється) 2) Якщо в задачі стоіть послідовність точка-кома-пробіл , то тільки в такій послідовності перевірок проходить перевірку. Трохи не зручно, коли такі моменти забирають купу часу.
Кирило Рівень 43
9 серпня 2024
Це точно перша лекція рівня!?)
Андрій Рівень 18
3 квітня 2024
Вирішив не паритись і в останній задачі просто використав метод replaceAll)
Olexandr Рівень 47
21 січня 2024
Ладно hard задача, з якою можно розібратися. Але вимагати застосувати лямбда вирази, які ми навіть близько не вчили - це сильно!🤬 Я розумію і підтримую, коли потрібно самому щось знайти і загуглити. Але для цієі задачі я маю сам вивчити всю Джаву, чи як???
oleh ronin Рівень 47
6 листопада 2023
Напишу це ще раз, люди не розуміють..🙂 Агов народ,те що в лекціях трапляється матеріал який ще не вчили, таким чином JavaRush привчає вас гуглити інформацію яка вам портібна. Так уже сталось що повністю вивчити мову Java неможливо. Правильно поставлене питання по проблемі в гугл - це вже 70% вирішення проблеми. Так що не "плюйтесь" тут що ми такого не вчили, і "JavaRush зкатились", тут нам на благо))
Dmytrii Рівень 28
6 серпня 2024
то ще все прийде з опитом, але нам, нубам, потрібну інфу треба давати в лекціях
Олег Рівень 65
8 січня 2025
Олег, мушу з Вами не погодитись, та поясню чому. я плачу гроші ДжаваРашу не за те, щоб Вони мене в гуглі навчили ставити питання, а за знання, які я отримую. Так що не пудрите мозок наступним учням.
Yurii Kuzniak Рівень 30
7 червня 2023
Перепрошую, але люди добрі, шо це, як це вирішити, шо таке +=2, ви шо здуріли те все мені зараз пояснювати, а дещо навіть не пояснювати, дуже важка тема як на мене, не знаю як вам
Ва Дим Рівень 28
27 квітня 2024
не зрозумів питання .int a += 2; це те ж саме що і int a = a + 2;
Elder_HD Рівень 42 Expert
4 червня 2023
Тільки дякуючи задачам, рівень яких не відповідає викладеному матеріалу, вдалося нормально розібратися в темі (в т.ч. побайтовому читанні вхідного потоку). Задачі не без гуглінга, але все же вдалось вирішити. Про лямба вираз у 3й задачі я взагалі мовчу))) (хоч і алгоритм застосував той самий). Всім, хто зіштовхується з труднощами при виконанні задач, рекомендую читати та аналізувати методи, які використовуються або вимагаються у задачах (read(), read(byte[] bytes), write (byte[] bytes), readAllBytes() і тд). Особливо важливо знати "що саме повертає метод". До речі, в процесі вирішення задач стало цікаво, який з методів побайтвого читання при копіюванні ефективніший: - read() - побайтове копіювання. Зчитали один байт, один байт записали - read(byte[] buffer) - побайтове копіювання обмеженим "буфером". Зчитали певну кількість даних (що помістилася у буфер), скопіювали, зчитали наступну порцію, скопіювали і тд... - readAllBytes() - копіювання цілісним масивом байтів, який був зчитаний з вхідного потоку. Зручно, але викине помилку, якщо не вистачить пам'яті (вхідний файл завеликий).
222.1Balaban Danil Рівень 32
21 квітня 2023
Перед задачою з рівнем Hard, рекомендую прочитати цю лекцію https://javarush.com/groups/posts/2275-files-path
Serhii #2958832 Рівень 51 Expert
7 червня 2023
Дякую. Дійсно, без інформації з даної лекції не зробив би не тільки task1504 (Hard), а task1506 також. Не пам'ятаю, щоб клас Files з його readAllLines раніше проходили.