JavaRush /Курси /JAVA 25 SELF /BufferedReader, BufferedWriter: буферизація, переваги

BufferedReader, BufferedWriter: буферизація, переваги

JAVA 25 SELF
Рівень 36 , Лекція 0
Відкрита

1. Вступ

Згадайте, як працюють FileReader і FileWriter. Щоразу, коли ви викликаєте в них метод read() або write(), здійснюється звернення до файлової системи — комп’ютер іде на диск, щоб прочитати чи записати один символ. Якщо файл великий, а ви читаєте чи пишете по одному символу за раз, це перетворюється на марафонську дистанцію з тисяч або навіть мільйонів звернень до диска. А диск, як відомо, — не найшвидший друг програміста.

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

Ось як це відбувається:

// Читання файлу посимвольно (по "чайній ложці"!)
try (FileReader reader = new FileReader("big.txt")) {
    int c;
    while ((c = reader.read()) != -1) {
        // обробляємо символ (наприклад, просто рахуємо)
    }
}

Якщо файл великий, ви помітите, що програма працює дуже повільно.

Буферизація: що це і навіщо потрібна

Буфер — це спеціальна область пам’яті (зазвичай масив), куди дані завантажуються або записуються не по одному символу, а відразу великими шматками (наприклад, по 8 КБ або більше). Простіше кажучи, буфер — це шматок пам’яті, щось на кшталт проміжного відра, куди дані читаються або записуються не по одному символу, а відразу великими порціями.

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

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

2. BufferedReader і BufferedWriter: синтаксис і приклади

У Java для буферизації читання та запису текстових файлів використовуються два класи:

  • BufferedReader — для читання текстових файлів.
  • BufferedWriter — для запису текстових файлів.

Вони працюють поверх звичайних Reader/Writer (наприклад, FileReader/FileWriter), додаючи буферизацію.

Читання файлу по рядках за допомогою BufferedReader

Найпоширеніший сценарій — читати файл по рядках. Метод readLine() повертає рядок до символу нового рядка ("\n" або "\r\n").

import java.io.*;

public class BufferedReaderExample {
    public static void main(String[] args) {
        try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line); // Виводимо рядок на екран
            }
        } catch (IOException e) {
            System.out.println("Помилка під час читання файлу: " + e.getMessage());
        }
    }
}

Що тут відбувається?

Ми створюємо FileReader, який уміє читати файл символ за символом, і обгортаємо його в BufferedReader. Буферизований читач бере дані не по одному символу, а відразу великими шматками, складає їх у пам’ять і роздає нам по рядках за допомогою методу readLine(). У підсумку ви просто пишете цикл while, отримуєте рядки одну за одною і не думаєте про те, наскільки великий файл: читання все одно буде швидким і ощадним.

Запис файлу за допомогою BufferedWriter

Записувати рядки у файл також можна ефективно:

import java.io.*;

public class BufferedWriterExample {
    public static void main(String[] args) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
            writer.write("Hello, world!");
            writer.newLine(); // Перехід на новий рядок (залежить від ОС)
            writer.write("Це другий рядок.");
        } catch (IOException e) {
            System.out.println("Помилка під час запису файлу: " + e.getMessage());
        }
    }
}

Що тут відбувається?

Спочатку ми створюємо FileWriter, а потім обгортаємо його в BufferedWriter. Коли ви викликаєте write() або newLine(), дані не біжать відразу на диск. Вони складаються в буфер — спеціальну проміжну пам’ять. Лише коли буфер заповниться або ви закриєте потік (чи явно викличете flush()), увесь накопичений текст разом записується у файл. Такий підхід значно пришвидшує запис і заощаджує звернення до диска.

Як це виглядає в єдиному застосунку?

Припустімо, ми пишемо просту програму-«щоденник», яка зберігає записи у файл і виводить їх на екран.

import java.io.*;
import java.util.Scanner;

public class DiaryApp {
    public static void main(String[] args) {
        String fileName = "diary.txt";
        Scanner scanner = new Scanner(System.in);

        // Додавання нового запису
        System.out.print("Введіть новий запис до щоденника: ");
        String entry = scanner.nextLine();

        try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName, true))) {
            writer.write(entry);
            writer.newLine();
            System.out.println("Запис збережено!");
        } catch (IOException e) {
            System.out.println("Помилка під час запису: " + e.getMessage());
        }

        // Читання усіх записів
        System.out.println("\nВаш щоденник:");
        try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.out.println("Помилка під час читання: " + e.getMessage());
        }
    }
}

Ми беремо ім’я файлу "diary.txt" і пропонуємо користувачеві ввести новий запис. Для збереження використовуємо FileWriter у режимі append (передаємо true), тож старі записи не стираються — кожен новий рядок акуратно додається в кінець файлу. Обгортка у вигляді BufferedWriter робить запис швидким і ощадним: дані спочатку накопичуються в пам’яті, а потім одним блоком йдуть на диск.

Після цього ми відкриваємо той самий файл на читання через BufferedReader. Він бере вміст великими блоками, а вам видає рядки по одній. У циклі програма просто виводить їх на екран, і в результаті ви бачите весь свій «щоденник» від початку до кінця.

3. Переваги BufferedReader і BufferedWriter

Відчутне прискорення роботи

Коли ви читаєте або пишете файл за допомогою буферизованих потоків, програма робить у десятки, а інколи й у сотні разів менше звернень до диска. Це особливо помітно на великих файлах.

Зручні методи

  • BufferedReader.readLine() — дозволяє читати файл по рядках, що дуже зручно для обробки текстових файлів (наприклад, логів, CSV, конфігів).
  • BufferedWriter.newLine() — додає перехід на новий рядок, коректно підставляючи потрібний символ залежно від ОС.

Простота використання

  • Класи легко комбінуються з іншими потоками (наприклад, можна обгорнути InputStreamReader усередині BufferedReader для читання файлів із різним кодуванням).
  • Автоматично закривають усі ресурси під час використання try-with-resources.

Гнучкість

  • Можна задати розмір буфера явно, якщо типовий (зазвичай 8 КБ) вам замалий або завеликий:
BufferedReader reader = new BufferedReader(new FileReader("big.txt"), 16384); // буфер на 16 КБ

4. Коли використовувати BufferedReader і BufferedWriter

Використовуйте їх:

  • Якщо працюєте з текстовими файлами (логи, CSV, великі текстові дані).
  • Коли потрібно читати або писати файл по рядках.
  • Для прискорення роботи з великими файлами, коли швидкість важлива.
  • Якщо потрібно обробляти потік даних із мережі або іншого джерела, що підтримує Reader/Writer.

Не використовуйте їх:

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

5. Корисні нюанси

Комбінування з різними кодуваннями

Якщо вам потрібно читати/писати файли у певному кодуванні (наприклад, "UTF-8", "Windows-1251"), використовуйте InputStreamReader/OutputStreamWriter у зв’язці з буферизованими потоками:

BufferedReader reader = new BufferedReader(
    new InputStreamReader(new FileInputStream("input.txt"), "UTF-8")
);

BufferedWriter writer = new BufferedWriter(
    new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")
);

Явне скидання буфера

Іноді важливо, щоб дані точно потрапили на диск (наприклад, якщо пишете лог або чек). Для цього викликайте writer.flush(). Зазвичай це не потрібно, адже закриття потоку автоматично скидає буфер.

Розмір буфера

Типовий розмір буфера — близько 8 КБ. Можна задати інший розмір, якщо ви точно знаєте, що це дасть приріст продуктивності (наприклад, під час обробки гігантських файлів).

Порівняння: FileReader/FileWriter vs BufferedReader/BufferedWriter

Клас Швидкість на великих файлах Зручність читання по рядках Зручність запису по рядках Гнучкість щодо кодувань
FileReader/FileWriter Повільно Ні (лише посимвольно) Ні (лише посимвольно) Лише за замовчуванням
BufferedReader/BufferedWriter Швидко Так (readLine()) Так (newLine()) Так (через InputStreamReader/OutputStreamWriter)

6. Типові помилки при роботі з BufferedReader і BufferedWriter

Помилка № 1: Забули закрити потік. Якщо не використовувати try-with-resources або не викликати close(), файл може залишитися заблокованим, а дані — не записаними на диск. Завжди використовуйте try-with-resources!

Помилка № 2: Плутають роботу з текстовими та бінарними файлами. Спроба відкрити бінарний файл (".jpg", ".zip") через BufferedReader призведе до «абракадабри» і, швидше за все, до помилок. Для бінарних файлів використовуйте InputStream/OutputStream.

Помилка № 3: Не використовують буферизацію за великих обсягів даних. Якщо читати або писати посимвольно, програма працюватиме повільно. Завжди використовуйте буферизацію для великих файлів.

Помилка № 4: Не викликають flush() за необхідності. Якщо потрібно, щоб дані з’явилися на диску негайно (наприклад, для логування), викликайте writer.flush(). Але зазвичай закриття потоку все зробить за вас.

Помилка № 5: Не враховують кодування. Якщо відкрити файл із неправильним кодуванням, текст може відображатися некоректно (символи перетворюються на «?», ієрогліфи тощо). Завжди явно вказуйте потрібне кодування, якщо воно відрізняється від системного (наприклад, "UTF-8").

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ