CodeGym /Kursy /JAVA 25 SELF /Odczyt i zapis plików binarnych: InputStream, OutputStrea...

Odczyt i zapis plików binarnych: InputStream, OutputStream

JAVA 25 SELF
Poziom 36 , Lekcja 2
Dostępny

1. Wprowadzenie

Zacznijmy od najprostszej rzeczy: czym plik binarny różni się od tekstowego? Plik tekstowy — to taki plik, który można otworzyć w zwykłym edytorze tekstu i zobaczyć litery, cyfry, spacje i inne znaki. Na przykład my_notes.txt lub poem.txt.

Plik binarny — to plik, który zawiera nie tekst, lecz dowolne bajty. Może to być obraz (.jpg, .png), muzyka (.mp3), archiwum (.zip), plik wykonywalny (.exe), wideo (.mp4), plik bazy danych i tak dalej. Jeśli otworzysz taki plik w edytorze tekstu, zobaczysz coś w rodzaju ÿØÿà albo całą ścianę niezrozumiałych znaków. To normalne! Komputer „rozumie” tylko bajty — dla niego i tekst, i obraz, i wideo — to po prostu sekwencja bajtów. W plikach tekstowych te bajty można zinterpretować jako znaki, a w binarnych — to „surowe” dane, które nie są przeznaczone do czytania przez człowieka.

Podstawowe klasy do pracy z plikami binarnymi

W Javie do pracy z plikami binarnymi używa się strumieni bajtowych:

  • InputStream — podstawowa klasa do odczytu bajtów.
  • OutputStream — podstawowa klasa do zapisu bajtów.

Do pracy z plikami istnieją ich konkretne implementacje:

  • FileInputStream — czyta bajty z pliku.
  • FileOutputStream — zapisuje bajty do pliku.

Jeśli słyszałeś o FileReader i FileWriter, pamiętaj: pracują na znakach i nadają się tylko do tekstu. Do plików binarnych używaj wyłącznie InputStream/OutputStream i ich klas pochodnych.

2. Odczyt plików binarnych

Odczyt bajt po bajcie

Najprostszy sposób — czytać plik bajt po bajcie. To jest przejrzyste, ale bardzo wolne.

try (FileInputStream in = new FileInputStream("image.jpg")) {
    int b;
    while ((b = in.read()) != -1) {
        // b to liczba od 0 do 255 (bajt); -1 oznacza koniec pliku
        // Można przetworzyć bajt, np. policzyć sumę wszystkich bajtów
    }
}

Metoda read() zwraca kolejny bajt jako int (od 0 do 255), a gdy plik się skończy — zwraca -1. Zazwyczaj czyta się bajt po bajcie tylko wtedy, gdy potrzebne jest coś bardzo specyficznego (np. analizowanie struktury pliku).

Odczyt blokami (z buforem)

Czytanie bajt po bajcie jest jak chodzenie do sklepu po każde jabłko osobno. Znacznie wydajniej wziąć od razu całą torbę! W Javie służy do tego metoda read(byte[] buffer), która wypełnia tablicę bajtami z pliku.

try (FileInputStream in = new FileInputStream("image.jpg")) {
    byte[] buffer = new byte[4096]; // bufor 4 KB
    int bytesRead;
    while ((bytesRead = in.read(buffer)) != -1) {
        // buffer zawiera bytesRead bajtów z pliku
        // Można przetworzyć te bajty, np. zapisać je gdzieś indziej
    }
}

Metoda read(buffer) zwraca, ile bajtów faktycznie odczytano (może być mniej niż rozmiar bufora, zwłaszcza przy ostatnim odczycie). Taki sposób jest znacznie szybszy, ponieważ odwołań do dysku jest mniej.

Przykład: kopiowanie pliku

Napiszmy prosty program, który kopiuje dowolny plik binarny (na przykład obraz) z jednego miejsca w drugie.

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 KB — optymalny rozmiar dla większości zadań
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
            System.out.println("Kopiowanie zakończone!");
        } catch (IOException e) {
            System.out.println("Błąd podczas kopiowania: " + e.getMessage());
        }
    }
}

To proste: czytamy bloki z pliku źródłowego i od razu zapisujemy je do nowego pliku. Taki sposób działa z dowolnymi plikami: obrazami, archiwami, wideo.

3. Zapis plików binarnych

Zapis tablicy bajtów

Jeśli masz tablicę bajtów (na przykład otrzymaną z sieci albo wygenerowaną w programie), możesz zapisać ją do pliku w ten sposób:

byte[] data = new byte[] {1, 2, 3, 4, 5}; // przykładowa tablica

try (FileOutputStream out = new FileOutputStream("data.bin")) {
    out.write(data); // zapisuje całą tablicę do pliku
}

Metoda write(byte[]) zapisuje wszystkie bajty z tablicy. Można też zapisać tylko część tablicy: out.write(data, offset, length).

Zapis pliku partiami (np. przy kopiowaniu)

Podobnie jak przy odczycie, zwykle używa się bufora:

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);
    }
}

Tutaj wszystko, co odczytujemy z pliku, od razu zapisujemy do innego pliku. Taki kod często spotyka się w programach-archiwizatorach, narzędziach do pobierania, przetwarzaniu obrazów itd.

4. Przydatne szczegóły

Dlaczego nie wolno używać Reader/Writer do plików binarnych?

Reader i Writer pracują na znakach (char), a nie na bajtach. Automatycznie przekształcają bajty w znaki zgodnie z kodowaniem (na przykład UTF-8). To wygodne dla tekstu, ale dla plików binarnych — śmiertelnie niebezpieczne!

Jeśli spróbujesz zapisać obraz przez FileWriter, otrzymasz uszkodzony plik, którego nie da się otworzyć. Zapamiętaj: do wszelkich plików nietekstowych używaj wyłącznie InputStream/OutputStream!

Ważne różnice i niuanse pracy z plikami binarnymi

  • Rozmiar bufora: Zbyt mały bufor spowolni pracę (zbyt wiele odwołań do dysku), zbyt duży — zajmie niepotrzebnie pamięć. 416 KB — zwykle optymalnie.
  • Obsługa błędów: Zawsze obsługuj IOException — plik może nie istnieć, być zablokowany albo może zabraknąć miejsca na dysku.
  • Zamykanie strumieni: Używaj try-with-resources — to gwarantuje zamknięcie plików nawet w razie błędów.
  • Nadpisywanie pliku: Jeśli otwierasz plik przez new FileOutputStream("file.bin"), zostanie nadpisany. Aby dopisać na końcu, użyj konstruktora z parametrem append = true.
  • Uprawnienia: Jeśli program nie może otworzyć pliku, sprawdź prawa do odczytu/zapisu.
  • readAllBytes(): Pozwala odczytać cały plik do tablicy bajtów jednym wywołaniem. Dla dużych plików — nie używaj, żeby nie „pożreć” całej pamięci!

5. Typowe błędy przy pracy z plikami binarnymi

Błąd nr 1: Użycie FileReader/FileWriter do plików binarnych. To prowadzi do uszkodzenia danych, ponieważ te klasy przekształcają bajty w znaki i z powrotem, co dla obrazów, archiwów itd. — katastrofa.

Błąd nr 2: Ignorowanie wartości zwracanej przez read(). Metoda read(byte[]) może odczytać mniej bajtów, niż żądasz, zwłaszcza w ostatnim bloku. Zawsze używaj wartości zwracanej, aby wiedzieć, ile bajtów faktycznie przetworzono.

Błąd nr 3: Zapomniano zamknąć strumień. Jeśli nie zamkniesz pliku, może pozostać zablokowany, a dane nie zostaną zapisane do końca (zwłaszcza przy zapisie!). Używaj try-with-resources.

Błąd nr 4: Próba wczytania całego pliku do pamięci bez uwzględnienia rozmiaru. Dla dużych plików to doprowadzi do OutOfMemoryError. Używaj bufora i czytaj partiami.

Błąd nr 5: Brak obsługi wyjątków. Praca z plikami zawsze może prowadzić do błędów: plik nie znaleziony, brak praw, pełny dysk. Nie zapominaj obsługiwać IOException.

1
Zadanie
JAVA 25 SELF, poziom 36, lekcja 2
Niedostępne
Wyodrębnianie wartościowego fragmentu z binarnego strumienia 🧪
Wyodrębnianie wartościowego fragmentu z binarnego strumienia 🧪
1
Zadanie
JAVA 25 SELF, poziom 36, lekcja 2
Niedostępne
Obliczanie potencjału energetycznego artefaktu ⚛️
Obliczanie potencjału energetycznego artefaktu ⚛️
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION