Освоюємо Java/Потоки вводу-виводу

Доволі часто необхідно здійснювати читання даних із файлів, різноманітних пристроїв, мережевих ресурсів, тощо, все це здійснюється за допомогою потоків вводу/виводу.

Концепція потоків вводу/виводу ред.

Для читання даних із чогось в програмі створюється вхідний потік, для виводу даних кудись - створюється вихідний потік. З практичної точки зору, при об'єктно орієнтованому програмуванні, вам необхідно створити два об'єкти, які уособлюватимуть ці потоки і вказати в кожному з них з чим вони зв'язані (наприклад, задати шлях до файлу, чи ідентифікатор порту комп'ютера, чи мережеву URL адресу і т.п.). Якщо нам потрібно прийняти дані, то використовується один об'єкт, якщо передати дані — звертаємось до іншого об'єкта.

 

Ієрархія потоків вводу/виводу ред.

Класи потоків в Java формують дві ієрархії класів: символьні потоки (Character Streams) та байтові потоки (Byte Streams). Як зрозуміло з назв, перші потоки орієнтовані на роботу з символами Юнікоду, а інші з послідовностями байт. Практичну будь-яку передачу інформації можна реалізувати за допомогою байтових потоків, проте доволі часто така інформація представляється у вигляді символів, тож для таких даних зручніше використовувати символьні потоки. В java для організації потоків вводу-виводу, програмістам надано біля півсотні класів. Насправді, для створення вводу-виводу достатньо лише кілька класів, проте в залежності від задачі деякі класи реалізовують більш зручні засоби для отримання, передачі і деякої попередньої обробки даних.

(даний пункт незавершений, необхідно зробити схеми ієрарій потоків, то розписати коротко основні класи.)

Робота з файлами ред.

Байтові потоки ред.

Розглянемо приклад роботи з файлами за допомогою байтових потоків. У нас є два файли, необхідно скопіювати один файл у інший. Для цього ми створюємо два байтові потоки: вхідний потік, через який читатиметься наш файл first.txt і створюємо інший вихідний (output) потік, який записуватиме прочитані дані у second.txt. Для цього будемо використовувати два класи FileInputStream та FileOutputStream.

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class CopyBytes {
    public static void main(String[] args) throws IOException {
        
        //створюємо об'єктні змінні, які посилатимуться на наші потоки
        FileInputStream in = null;
        FileOutputStream out = null;

        // При помилках читання/запису можуть генеруватися винятки, тож потрібно перехопити їх
        // Наприклад, помилка може виникнути, при відсутності файлу first.txt у вказаному місці
        try { 
            
            // створюємо вхідний і вихідний потік
            // файл first.txt повинен вже існувати
            // якщо second.txt не буде існувати,
            // то буде створений при спробі запису
            in = new FileInputStream("d:\\first.txt");
            out = new FileOutputStream("d:\\second.txt");
            int c; 

            //Допоки з файлу first.txt не буде прочинато всі байти,
            //читаємо байти з файлу first.txt і записуємо даний байт у second.txt
            //якщо потік не повертає -1(не досягнено кінець файлу),
            //то копіюємо наступний байт
            while ((c = in.read()) != -1) {
                out.write(c);
            }
        } finally { //дії коли не знайдено файли
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }
    }
}

Як бачимо алгоритм роботи з файлами доволі простий. Спочатку створюємо вхідний потік, далі створюємо вихідний потік. Читаємо перший байт, першого файлу і записуємо його у другий файл, далі переходимо до наступного байту і так допоки усі байти першого файлу не будуть прочитані. В разі виникнення виняткової ситуації, закриваємо наші потоки, а разом з ними і наші файли(не варто забувати закривати потоки, задля звільнення ресурсів комп'ютера. Необхідно згадати, що при читанні/записі файлу застосовується своєрідний вказівник на вміст файлу(неявно для нас). В даному випадку після кожного прочитаного байту вказівник неявним для нас чином переміщується по вмісту файлу на один байт і т.д.

В даному випадку ми використали низькорівневі байтові потоки. Вони годяться для копіювання даних з одного місця в інший, проте символи можуть представлятися не одним байтом. Тому коли ми захочемо, наприклад, вивести вміст файлу на екран, ми отримаємо проблеми із виводом символів. Так у нашому випадку, якщо файл буде латиницею, то використавши у циклі інструкцію:

System.out.print(Character.toChars(c));

ми одержимо текст у читабельному виді. Оскільки в Юнікоді для представлення латиниці достатньо одного байту. Якщо ж файл кирилицею, текст буде виведений карлючками, оскільки робота перетворення байт у символи пройшло не зовсім вірно (кириличні літери представляються двома байтами). Дану проблему можна вирішити кількома способами. Найбільш підковані програмісти можуть спробувати власноруч здійснити перетворення байтів у символи з використанням бітових операцій, проте це дещо незручний спосіб і потрібно враховувати кодування символів. Інший спосіб більш легший: замість того, щоб читати по одному байту, ми можемо прочитати зразу ж увесь вміст файлу у масив і перетворити його у текстовий рядок потрібного кодування:

    byte []b=new byte[10000]; // масив для вмісту файлу 
    int k=in.read(b); // читаємо в масив та отримуємо кількість прочитаних байт
    //використовуємо конструктор String, 
    //який перетворює масив у рядок 
    //із відповідним кодуванням символів     
    String s = new String(b, 0, k, "cp1251");
    System.out.println(s);

Якщо необхідно визначити потрібне кодування, то це можна зробити за допомогою читання відповідних системних властивостей:

String encoding= System.getProperty("file.encoding");

І все ж, коли наперед відомо, що ми працюємо із текстовими файлами, то більш елегантним і простішим рішенням буде використання символьних потоків. Тоді Java візьме на себе правильну роботу із кодуванням символів.

Символьні потоки ред.

Наступний приклад вирішує проблему з читанням кирилиці із файлу з наступним її відображенням на екран. На відміну від попереднього прикладу, тепер використовуються символьні потоки, що створюються на основі класів: FileReader, FileWriter.

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class CopyCharacters {
    public static void main(String[] args) throws IOException {

        FileReader inputStream = null;
        FileWriter outputStream = null;

        try {
            inputStream = new FileReader("d:\\first.txt");
            outputStream = new FileWriter("d:\\second.txt");

            int c;
            while ((c = inputStream.read()) != -1) {
                //посимвольно записуємо у файл і виводимо на екран
                outputStream.write(c);
                System.out.print(Character.toChars(c));
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

Як бачимо крім назви класів, на оcнові, яких ми створюємо потоки в коді нічого суттєво більше не змінилося. Нам не потрібно іти на різного роду хитрощі, щоб правильно працювати із символами.

Ось результат по виводу на екран вмісту файлу із прикладу CopyBytes.java(байтові потоки):

?????? ????, ??????????? ?????!

а ось при використанні прикладу із CopyCharacters.java(символьні потоки):

Привіт тобі, божевільний світе!

Буферизовані потоки ред.

Читання вмісту файлу по байтах не дуже хороша ідея, якщо файл доволі великий. Адже це зайве навантаження на обчислювальні ресурси комп'ютера. Тому більш кращим варіантом є читання тексту цілими блоками. Наприклад, рядками. Рядки у файлах прийнято завершувати символом нового рядка("\n") та символом переходу на новий рядок("\r"). Може бути присутній як один з цих символів так і обидва ("\r\n"), в залежності від того хто і яким чином створював файл.

Читання блоків файлу відбувається через так звані буферизовані потоки, що працюють через буфер в пам'яті комп'ютера. При читанні даних з файлу, дані передаються в програму коли буфер буде порожнім, при записі у файл буфер спочатку повинен заповнитись. Для буферизованого вводу/виводу існує чотири класи. Для буферизованих байтових потоків: BufferedInputStream та BufferedOutputStream. Для буферизованих символьних потоків: BufferedReader та BufferedWriter. Інколи корисно вивільнити дані з буфера до повного його заповнення у окремих критичних точках,це можна зробити з допомогою методу flush.

Далі наведено, дещо модифікований вищенаведений приклад, що використовує буферизовані потоки:

import java.io.FileReader;
import java.io.FileWriter;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.IOException;

public class CopyLines {
    public static void main(String[] args) throws IOException {

        BufferedReader inputStream = null;
        PrintWriter outputStream = null;

        try {
            inputStream = new BufferedReader(new FileReader("d:\\first.txt"));
            outputStream = new PrintWriter(new FileWriter("d:\\second.txt"));

            String l;
            //тепер читаємо дані цілими рядками
            while ((l = inputStream.readLine()) != null) {
                outputStream.println(l);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

Як бачимо наші буферизовані потоки обгорнули звичайні небуферизовані потоки:

inputStream = new BufferedReader(new FileReader("d:\\first.txt"));
outputStream = new PrintWriter(new FileWriter("d:\\second.txt"));

Тепер ми можемо використовувати метод readLine і читати дані з файлу цілими рядками тексту:

inputStream.readLine())

та записувати дані у файл також цілими рядками:

outputStream.println(l);

Деякі потоки можуть обгортати інші потоки не лише заради буферизації.

Робота з COM-портом ред.

Інколи постає необхідність роботи з різноманітними пристроями через COM-порт. Для роботи з портом застосовуються звичайні байтові або ж символьні потоки вводу/виводу, проте основна проблема - це необхідність відповідного API пакету Java для роботи з ними. У JDK немає стандартного пакету. Тому необхідно встановити додаткову бібліотеку для роботи з послідовними та паралельними портами. Послідовний (COM-порт) доволі поширений в даний час, багато апаратного обладнання працює на ньому. Навіть в сучасному він використовується доволі часто. Сучасні прилади підключаються до USB, проте в багатьох використовується перехідник USB-COM, що дозволяє розробникам програмного забезпечення працювати з приладом через добре знайомий і легкий інтерфейс RS232 (COM).

Існує декілька java бібліотек для роботи з COM-портом , які побудовані з використанням Native-методів. Свого часу була популярна javax.Comm, її можна використовувати до цих пір, проте в даний час бібліотеку ніхто не підтримує, її розвиток не відбувається. На зміну javax.Comm прийшла інша java бібліотека – RXTX. Методи в RXTX для роботи з COM-портом ідентичні до методів javax.Comm.

Детальніше тут:

Ком'ютерні мережі ред.

Передача даних через мережу здійснюється через сокети (Sockets).

(Ще не написано)

Джерела інформації ред.


Аплети · Узагальнення