Java files

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

Байтові потоки

Розглянемо приклад роботи з файлами за допомогою байтових потоків. У нас є два файли, необхідно скопіювати один файл у інший. Для цього ми створюємо два байтові потоки: вхідний потік, через який читатиметься наш файл 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);

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

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