Java IO

Матеріал з Вікі ЦДУ
Перейти до: навігація, пошук

Система вводу/виводу. Потоки даних (stream)

Переважна більшість програм обмінюються даними із зовнішнім світом. Це, безумовно, роблять будь-які мережеві застосування – вони передають і отримують інформацію від інших комп'ютерів і спеціальних пристроїв, підключених до мережі. Виявляється, можна таким самим чином представляти обмін даними між пристроями усередині однієї машини. Так, наприклад, програма може прочитувати дані з клавіатури і записувати їх у файл, або ж навпаки - прочитувати дані з файлу і виводити їх на екран. Таким чином, пристрої, звідки може проводитися прочитування інформації, можуть бути найрізноманітнішими – файл, клавіатура, вхідне мережеве з'єднання і так далі То ж стосується і пристроїв виводу – це може бути файл, екран монітора, принтер, витікаюче мережеве з'єднання і тому подібне Кінець кінцем, всі дані в комп'ютерній системі в процесі обробки передаються від пристроїв введення до пристроїв виводу.

Зазвичай частина обчислювальної платформи, яка відповідає за обмін даними, так і називається – система вводу/виводу. У Java вона представлена пакетом java.io (input/output). Реалізація системи вводу/виводу ускладнюється не тільки широким спектром джерел і одержувачів даних, але ще і різними форматами передачі інформації. Нею можна обмінюватися в двійковому уявленні, символьному або текстовому, із застосуванням деякого кодування (тільки для російської мови їх налічується більше 4 штук), або передавати числа в різних уявленнях. Доступ до даних може потрібно як послідовний (наприклад, прочитування HTML-сторінки), так і довільний (складна робота з декількома частинами одного файлу). Часто для підвищення продуктивності застосовується буферизація.

У Java для опису роботи по вводу/виводу використовується спеціальне поняття потік даних (stream). Потік даних пов'язаний з деяким джерелом, або приймачем, даних, здатним отримувати або надавати інформацію. Відповідно, потоки діляться на вхідні – такі, що читають дані і дані, що виходять – передають (що записують). Введення концепції stream дозволяє відокремити основну логіку програми, інформацією, що обмінюється, з будь-якими пристроями однаковим чином, від низькорівневих операцій з такими пристроями вводу/виводу.

У Java потоки природним чином представляються об'єктами. Класи, що описують їх, якраз і складають основну частину пакету java.io. Вони досить різноманітні і відповідають за різну функціональність. Всі класи розділені на дві частини – одні здійснюють введення даних, інші – вивід.

Існуючі стандартні класи допомагають вирішити більшість типових завдань. Мінімальною "порцією" інформації є, як відомо, битий, що набуває значення 0 або 1 (це поняття також зручно застосовувати на найнижчому рівні, де дані передаються електричним сигналом; умовно кажучи, 1 представляється проходженням імпульсу, 0 – його відсутністю). Традиційно використовується крупніша одиниця вимірювання – байт, об'єднуюча 8 битий. Таким чином, значення, представлене одним байтом, знаходиться в діапазоні від 0 до 28-1=255, або, якщо використовувати знак, – від -128 до +127.

Примітивний тип byte в Java в точності відповідає останньому – знаковому діапазону. Базові, найбільш універсальні, класи дозволяють прочитувати і записувати інформацію саме у вигляді набору байт. Щоб їх було зручно застосовувати в різних завданнях, java.io містить також класи, що перетворюють будь-які дані в набір байт. Наприклад, якщо потрібно зберегти результати обчислень – набір значень типу double – у файл, то їх можна спочатку перетворити на набір байт, а потім ці байти записати у файл. Аналогічні дії здійснюються і за ситуації, коли потрібно зберегти об'єкт (тобто його стан) – перетворення в набір байт і подальша їх запис у файл. Зрозуміло, що при відновленні даних в обох розглянутих випадках проробляються зворотні дії – спочатку прочитується послідовність байт, а потім вона перетвориться в потрібний формат.

Класи InputStream і OutputStream

InputStream – це базовий клас для потоків введення, тобто читання. Відповідно, він описує базові методи для роботи з байтовими потоками даних. Ці методи необхідні всім класам, які успадковуються отInputStream.

Проста операція представлена методом read() (без аргументів). Він є абстрактним і, відповідно, має бути визначений в класах-спадкоємцях. Цей метод призначений для прочитування рівно одного байта із потоку, проте повертає при цьому значення типу int. В тому випадку, якщо прочитування відбулося успішно, значення, що повертається, лежить в діапазоні від 0 до 255 і є отриманим байтом (значення int має 4 байти і виходить простим доповненням нулями в двійковому уявленні). Звернете увагу, що отриманий таким чином байт не володіє знаком і не знаходиться в діапазоні від -128 до +127, як примітивний тип byte в Java.

Якщо досягнутий кінець потоку, тобто в нім більше немає інформації для читання, то значення, що повертається, рівне -1.

Якщо ж рахувати з потоку дані не вдається із-за якихось помилок, або збоїв, буде кинуто виключення java.io.IOException. Цей клас успадковується від Exception, тобто його завжди необхідно обробляти явно. Річ у тому, що канали передачі інформації, будь то Internet або, наприклад, жорсткий диск, можуть давати збої незалежно від того, наскільки добре написана програма. А це означає, що потрібно бути готовим до них, щоб користувач не втратив потрібні дані.

Метод read() – це абстрактний метод, але саме з дотриманням всіх вказаних умов він має бути реалізований в класах-наслідниках.

На практиці зазвичай доводиться прочитувати не один, а відразу декілька байт – тобто масив байт. Для цього використовується метод read(), де як параметри передається масив byte[]. При виконанні цього методу в циклі проводиться виклик абстрактного методу read() (визначеного без параметрів) і результатами заповнюється переданий масив. Кількість байт, яке прочитане таким чином, дорівнює довжині переданого масиву. Але при цьому може так вийти, що дані в потоці закінчаться ще до того, як буде заповнений весь масив. Тобто можлива ситуація, коли в потоці даних (байт) міститься менше, ніж довжина масиву. Тому метод повертає значення int, яке вказує, скільки байт було реально пораховано. Зрозуміло, що це значення може бути від 0 до величини довжини переданого масиву.

Якщо ж ми спочатку хочемо заповнити не весь масив, а тільки його частина, то для цих цілей використовується метод read(), якому, окрім масиву byte[], передаються ще два int значення. Перше – це позиція в масиві, з якою слід почати заповнення, друге, – кількість байт, яке потрібно рахувати. Такий підхід, коли для отримання даних передається масив і два int числа – offset (зсув) і length (довжина), є досить поширеним і часто зустрічається не тільки в пакеті java.io.

При виклику методів read() можливе виникнення такої ситуації, коли запрошувані дані ще не готові до прочитування. Наприклад, якщо ми прочитуємо дані, що поступають з мережі, і вони ще просто не прийшли. У такому разі не можна сказати, що даних більше немає, але і вважати теж нічого - виконання зупиняється на виклику методу read() і виходить "зависання".

Щоб дізнатися, скільки байт в потоці готово до прочитування, застосовується метод available(). Цей метод повертає значення типу int, яке показує, скільки байт в потоці готово до прочитування. При цьому не варто плутати кількість байт, готових до прочитування, з тією кількістю байт, які взагалі можна буде рахувати з цього потоку.

Метод available() повертає число – кількість байт, саме на даний момент готових до прочитування.

Коли робота з вхідним потоком даних закінчена, його слід закрити. Для цього викликається метод close(). Цим викликом будуть звільнені всі системні ресурси, пов'язані з потоком.

Точно так, як і InputStream – це базовий клас для потоків введення, клас OutputStream – це базовий клас для потоків виводу.

У класі OutputStream аналогічним чином визначаються три методи write() – що один приймає як параметр int, другий, – byte[] і третій – byte[], плюс два int-числа. Всі ці методи нічого не повертають (void).

Метод write(int) є абстрактним і має бути реалізований в класах-наслідниках. Цей метод приймає як параметр int, але реально записує в потік тільки byte – молодші 8 битий в двійковому уявленні. Останні 24 біта будуть проігноровані. У разі виникнення помилки цей метод кидає java.io.IOException, як, втім, і більшість методів, пов'язаних з вводом- виводом.

Для запису в потік відразу деякої кількості байт методу write() передається масив байт. Або, якщо ми хочемо записати тільки частину масиву, то передаємо масив byte[] і два int-числа – відступ і кількість байт для запису. Зрозуміло, що якщо вказати невірні параметри – наприклад, негативний відступ, негативна кількість байт для запису, або якщо сума відступ плюс довжина буде більше довжини масиву, – у всіх цих випадках кидається виключення IndexOutOfBoundsException.

Реалізація потоку може бути такій, що дані записуються не відразу, а зберігаються якийсь час в пам'яті. Наприклад, ми хочемо записати у файл якісь дані, які отримуємо порціями по 10 байт, і так 200 разів підряд. У такому разі замість 200 звернень до файлу зручніше буде скопити всі ці дані в пам'яті, а потім одним заходом записати все 2000 байт.

Тобто клас вихідного потоку може використовувати деякий внутрішній механізм для буферизації (тимчасового зберігання перед відправкою) даних. Щоб переконатися, що дані записані в потік, а не зберігаються в буфері, викликається метод flush(), визначений в OutputStream. У цьому класі його реалізація порожня, але якщо який-небудь із спадкоємців використовує буферизацію даних, то цей метод має бути в нім перевизначений.

Коли робота з потоком закінчена, його слід закрити. Для цього викликається метод close(). Цей метод спочатку звільняє буфер (викликом методу flush), після чого потік закривається і звільняються всі пов'язані з ним системні ресурси. Закритий потік не може виконувати операції виводу і не може бути відкритий наново. У классеOutputStream реалізація методу close() не проводить ніяких дій.

На початок курсу