JavaRush /Курси /JAVA 25 SELF /Читання і запис двійкових файлів: InputStream, OutputStre...

Читання і запис двійкових файлів: InputStream, OutputStream

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

1. Вступ

Почнімо з найпростішого: чим двійковий файл відрізняється від текстового? Текстовий файл — це такий файл, який можна відкрити у звичайному блокноті й побачити літери, цифри, пробіли та інші символи. Наприклад, my_notes.txt або poem.txt.

Двійковий файл — це файл, що містить не текст, а довільні байти. Це може бути зображення (.jpg, .png), музика (.mp3), архів (.zip), виконуваний файл (.exe), відео (.mp4), файл бази даних тощо. Якщо ви відкриєте такий файл у блокноті, побачите щось на кшталт ÿØÿà або цілу стіну незрозумілих символів. Це нормально! Комп’ютер «розуміє» лише байти — для нього і текст, і зображення, і відео — просто послідовність байтів. У текстових файлах ці байти можна інтерпретувати як символи, а у двійкових — це «сирі» дані, не призначені для читання людиною.

Основні класи для роботи з двійковими файлами

У Java для роботи з двійковими файлами використовуються потоки байтів:

  • InputStream — базовий клас для зчитування байтів.
  • OutputStream — базовий клас для запису байтів.

Для роботи з файлами є їхні конкретні реалізації:

  • FileInputStream — зчитує байти з файлу.
  • FileOutputStream — записує байти у файл.

Якщо ви чули про FileReader і FileWriter, то знайте: вони працюють із символами і підходять лише для тексту. Для двійкових файлів використовуйте тільки InputStream/OutputStream та їхніх нащадків.

2. Читання двійкових файлів

Читання по одному байту

Найпростіший спосіб — зчитувати файл по одному байту. Це наочно, але дуже повільно.

try (FileInputStream in = new FileInputStream("image.jpg")) {
    int b;
    while ((b = in.read()) != -1) {
        // b — це число від 0 до 255 (байт), -1 означає кінець файлу
        // Можна обробити байт, наприклад, підрахувати суму всіх байтів
    }
}

Метод read() повертає наступний байт як int (від 0 до 255), а коли файл закінчився — повертає -1. Зазвичай по одному байту читають лише тоді, коли потрібно щось дуже специфічне (наприклад, аналізувати структуру файлу).

Читання блоками (із буфером)

Читати по одному байту — це як ходити до магазину за кожним яблуком окремо. Набагато ефективніше брати відразу цілий пакет! У Java для цього є метод read(byte[] buffer), який заповнює масив байтами з файлу.

try (FileInputStream in = new FileInputStream("image.jpg")) {
    byte[] buffer = new byte[4096]; // буфер на 4 КБ
    int bytesRead;
    while ((bytesRead = in.read(buffer)) != -1) {
        // buffer містить bytesRead байтів із файлу
        // Можна обробити ці байти, наприклад, зберегти їх кудись іще
    }
}

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

Приклад: копіювання файлу

Напишемо просту програму, яка копіює будь-який двійковий файл (наприклад, зображення) з одного місця в інше.

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

public class BinaryCopyExample {
    public static void main(String[] args) {
        String source = "cat.jpg";
        String dest = "cat_copy.jpg";

        try (FileInputStream in = new FileInputStream(source);
             FileOutputStream out = new FileOutputStream(dest)) {

            byte[] buffer = new byte[8192]; // 8 КБ — оптимальний розмір для більшості задач
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
            System.out.println("Копіювання завершено!");
        } catch (IOException e) {
            System.out.println("Помилка під час копіювання: " + e.getMessage());
        }
    }
}

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

3. Запис двійкових файлів

Запис масиву байтів

Якщо у вас є масив байтів (наприклад, ви отримали його з мережі або згенерували в програмі), ви можете записати його у файл ось так:

byte[] data = new byte[] {1, 2, 3, 4, 5}; // приклад масиву

try (FileOutputStream out = new FileOutputStream("data.bin")) {
    out.write(data); // записує весь масив у файл
}

Метод write(byte[]) записує всі байти з масиву. Також можна записати лише частину масиву: out.write(data, offset, length).

Запис файлу частинами (наприклад, під час копіювання)

Як і під час читання, зазвичай використовують буфер:

try (FileInputStream in = new FileInputStream("source.bin");
     FileOutputStream out = new FileOutputStream("dest.bin")) {

    byte[] buffer = new byte[4096];
    int bytesRead;
    while ((bytesRead = in.read(buffer)) != -1) {
        out.write(buffer, 0, bytesRead);
    }
}

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

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

Чому не можна використовувати Reader/Writer для двійкових файлів?

Reader і Writer працюють із символами (char), а не з байтами. Вони автоматично перетворюють байти на символи відповідно до кодування (наприклад, UTF-8). Це зручно для тексту, але для двійкових файлів — смертельно небезпечно!

Якщо ви спробуєте записати зображення через FileWriter, отримаєте зіпсований файл, який неможливо відкрити. Запам’ятайте: для будь-яких нетекстових файлів використовуйте лише InputStream/OutputStream!

Важливі відмінності та нюанси роботи з двійковими файлами

  • Розмір буфера: Надто малий буфер уповільнить роботу (надто багато звернень до диска), надто великий — займе зайву пам’ять. 416 КБ — зазвичай оптимально.
  • Обробка помилок: Завжди обробляйте IOException — файл може не існувати, бути заблокованим або може закінчитися місце на диску.
  • Закриття потоків: Використовуйте try-with-resources — це гарантує закриття файлів навіть у разі помилок.
  • Перезапис файлу: Якщо ви відкриваєте файл через new FileOutputStream("file.bin"), його буде перезаписано. Щоб дописати в кінець, використовуйте конструктор із параметром append = true.
  • Права доступу: Якщо програма не може відкрити файл, перевірте права на читання/запис.
  • readAllBytes(): Дозволяє прочитати весь файл у масив байтів одним викликом. Для великих файлів — не використовуйте, щоб не «з’їсти» всю пам’ять!

5. Типові помилки під час роботи з двійковими файлами

Помилка № 1: Використання FileReader/FileWriter для двійкових файлів. Це призведе до пошкодження даних, адже ці класи перетворюють байти на символи і навпаки, що для зображень, архівів тощо — катастрофа.

Помилка № 2: Ігнорування значення, що повертає read(). Метод read(byte[]) може зчитати менше байтів, ніж ви просите, особливо в останньому блоці. Завжди використовуйте повернене значення, щоб знати, скільки байтів насправді опрацьовано.

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

Помилка № 4: Спроба зчитати файл повністю в пам’ять без урахування розміру. Для великих файлів це призведе до OutOfMemoryError. Використовуйте буфер і читайте частинами.

Помилка № 5: Не обробляються винятки. Робота з файлами завжди може призвести до помилок: файл не знайдено, немає прав, диск переповнено. Не забувайте обробляти IOException.

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