1. Po co są potrzebne DataInputStream i DataOutputStream?
Gdy pracujesz z plikami, czasem trzeba przechowywać nie tylko tekst, ale i dane strukturalne: liczby, wartości boolowskie, tablice prymitywów. Na przykład wyobraź sobie, że piszesz prostą grę i chcesz zapisać postęp użytkownika: liczbę punktów (int), bieżący poziom (int), czas gry (double), status zwycięzcy (boolean). Oczywiście można zapisać to w postaci tekstowej:
12345
5
67.5
true
Ale to niewygodne i niebezpieczne: napisy trzeba parsować, format może „rozjechać się”, a liczby zajmują więcej miejsca.
Pomysł polega na tym, by zapisywać dane do pliku w „surowym” (binarnym) formacie, bez konwersji do tekstu. Do tego w Javie są dwa znakomite klasy:
- DataOutputStream — potrafi zapisywać typy prymitywne do strumienia.
- DataInputStream — potrafi odczytywać typy prymitywne ze strumienia.
Działają one nad zwykłymi strumieniami bajtowymi (OutputStream/InputStream). To znaczy, są to takie „nakładki”, które potrafią nie tylko pisać bajty, lecz wiedzą, jak z tych bajtów złożyć int, double, boolean i nawet String.
Jak to działa?
Zwykły FileOutputStream można sobie wyobrazić jako taśmę, na którą ręcznie kładziesz bajty jeden po drugim. Jeśli chcesz zapisać liczbę całkowitą albo napis, musisz sam pilnować, ile bajtów zajmuje każdy element.
DataOutputStream ułatwia życie: działa jak robot na tej taśmie. Mówisz mu „zapisz liczbę” albo „zapisz napis”, a on sam pakuje dane w odpowiednią liczbę bajtów i wysyła na dysk. Po drugiej stronie taśmy taki sam robot — DataInputStream — potrafi złożyć te bajty z powrotem w obiekty wyjściowe.
Dlaczego to wygodne? Bo nie musisz myśleć o liczbie bajtów dla int, double czy boolean. Dane są przechowywane kompaktowo, szybko się je odczytuje i zapisuje, a przy tym nie ma ryzyka błędów parsowania ani problemów z formatem.
2. Przykład: zapis i odczyt prymitywów
Załóżmy, że chcemy zapisać wyniki naszej (umownej) gry: imię gracza (String), liczbę punktów (int), rekordowy czas (double), czy gracz wygrał (boolean).
Zapis danych do pliku
import java.io.*;
public class SaveGameData {
public static void main(String[] args) {
String fileName = "savegame.bin";
String playerName = "Alice";
int score = 12345;
double recordTime = 67.5;
boolean isWinner = true;
try (DataOutputStream dos = new DataOutputStream(
new FileOutputStream(fileName))) {
dos.writeUTF(playerName); // Zapisujemy napis (UTF-8)
dos.writeInt(score); // Zapisujemy int (4 bajty)
dos.writeDouble(recordTime); // Zapisujemy double (8 bajtów)
dos.writeBoolean(isWinner); // Zapisujemy boolean (1 bajt)
System.out.println("Dane zostały pomyślnie zapisane do pliku!");
} catch (IOException e) {
System.out.println("Błąd zapisu: " + e.getMessage());
}
}
}
- writeUTF(String) — zapisuje napis w formacie UTF-8 (z długością na początku).
- writeInt(int) — zapisuje 4 bajty.
- writeDouble(double) — zapisuje 8 bajtów.
- writeBoolean(boolean) — zapisuje 1 bajt (1 lub 0).
- Wszystkie metody automatycznie „pakują” dane do wymaganego formatu.
Odczyt danych z pliku
import java.io.*;
public class LoadGameData {
public static void main(String[] args) {
String fileName = "savegame.bin";
try (DataInputStream dis = new DataInputStream(
new FileInputStream(fileName))) {
String playerName = dis.readUTF(); // Odczytujemy napis
int score = dis.readInt(); // Odczytujemy int
double recordTime = dis.readDouble(); // Odczytujemy double
boolean isWinner = dis.readBoolean(); // Odczytujemy boolean
System.out.println("Imię gracza: " + playerName);
System.out.println("Punkty: " + score);
System.out.println("Czas: " + recordTime);
System.out.println("Zwycięzca: " + isWinner);
} catch (IOException e) {
System.out.println("Błąd odczytu: " + e.getMessage());
}
}
}
Ważna uwaga:
Kolejność odczytu musi odpowiadać kolejności zapisu! Jeśli najpierw zapisano napis, potem int, potem double — w takiej samej kolejności trzeba odczytywać. W przeciwnym razie dostaniesz błąd albo „śmieci” z danych.
3. Jakie typy są obsługiwane?
DataOutputStream i DataInputStream obsługują wszystkie podstawowe typy prymitywne Javy:
| Metoda zapisu | Metoda odczytu | Typ danych | Rozmiar (bajtów) |
|---|---|---|---|
|
|
boolean | 1 |
|
|
byte | 1 |
|
|
short | 2 |
|
|
char | 2 |
|
|
int | 4 |
|
|
long | 8 |
|
|
float | 4 |
|
|
double | 8 |
|
|
String (UTF) | zmienny |
Uwagi:
- Dla napisów najczęściej używa się writeUTF/readUTF (zapisuje się długość napisu i same bajty w UTF-8).
- Jeśli chcesz zapisać tablicę, najpierw zapisz jej długość, a następnie elementy po kolei.
4. Zaawansowany przykład: zapisujemy tablice prymitywów
Zapis tablicy
int[] scores = {100, 200, 300, 400, 500};
try (DataOutputStream dos = new DataOutputStream(
new FileOutputStream("scores.bin"))) {
dos.writeInt(scores.length); // Najpierw zapisujemy długość tablicy
for (int score : scores) {
dos.writeInt(score); // Następnie każdy element
}
}
Odczyt tablicy
try (DataInputStream dis = new DataInputStream(
new FileInputStream("scores.bin"))) {
int length = dis.readInt(); // Odczytujemy długość
int[] scores = new int[length];
for (int i = 0; i < length; i++) {
scores[i] = dis.readInt(); // Odczytujemy elementy
}
// Wypisujemy tablicę
for (int score : scores) {
System.out.println(score);
}
}
Dlaczego najpierw długość?
Ponieważ podczas odczytu nie wiemy, ile liczb zapisano. Zapisując długość na początku, sprawiamy, że format pliku jest samodokumentujący się.
5. Ważne niuanse i szczegóły
Kiedy warto używać DataInputStream/DataOutputStream?
- Kiedy trzeba zapisać/wczytać dane strukturalne złożone z prymitywów.
- Do wymiany danych binarnych między programami w Javie (a nawet w różnych językach, jeśli znasz format).
- Gdy liczą się kompaktowość i szybkość (np. logi, wyniki obliczeń, duże tablice liczb).
Kiedy nie warto:
- Jeśli potrzebny jest format czytelny dla człowieka (CSV, JSON, XML) — użyj formatów tekstowych.
- Dla złożonych obiektów z zagnieżdżeniami — lepiej zastosować serializację przez ObjectOutputStream/ObjectInputStream (osobny temat).
Buforowanie
DataOutputStream i DataInputStream same z siebie nie buforują danych. Jeśli chcesz zwiększyć wydajność przy pracy z dużymi plikami, owiń je w BufferedOutputStream/BufferedInputStream:
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream("data.bin")))) {
// ...
}
Kodowanie łańcuchów
Metody writeUTF/readUTF używają specjalnego formatu: najpierw zapisywana jest długość napisu (w bajtach), a następnie zawartość w UTF-8. Nie myl tego z prostym zapisem tablicy bajtów!
Wyjątki
Operacje odczytu/zapisu mogą rzucać IOException, jeśli plik jest niedostępny, uszkodzony albo skończył się wcześniej. Przy próbie odczytu większej ilości danych, niż zapisano, często pojawia się EOFException. Zawsze używaj konstrukcji try-with-resources lub obsługuj wyjątek try-catch.
Kolejność odczytu i zapisu
Najczęstszy błąd — niezgodność kolejności odczytu i zapisu. Jeśli zapisałeś: int, double, boolean, a odczytujesz jako double, int, boolean, otrzymasz nieprawidłowe dane lub wyjątek.
6. Typowe błędy
Błąd nr 1: Niezachowanie kolejności zapisu i odczytu. Jeśli zmienisz kolejność metod, dane zostaną odczytane niepoprawnie albo zostanie rzucony wyjątek. Na przykład jeśli najpierw zapisałeś napis, a potem liczbę, lecz przy odczycie próbujesz najpierw odczytać liczbę — dostaniesz błąd formatu.
Błąd nr 2: Brak zapisu długości tablicy. Jeśli zapisujesz tablicę prymitywów, ale nie zapisałeś jej długości, przy odczycie nie będziesz wiedzieć, ile elementów należy odczytać. To prowadzi albo do błędu, albo do „zbędnych” danych na końcu.
Błąd nr 3: Próba odczytu po końcu pliku. Jeśli odczytasz więcej danych, niż zapisano, otrzymasz EOFException (end of file).
Błąd nr 4: Używanie DataInputStream/DataOutputStream do plików tekstowych. Te klasy nie są przeznaczone do odczytu zwykłych plików tekstowych, utworzonych na przykład w Notatniku. Jeśli spróbujesz odczytać taki plik przez readInt() — dostaniesz bezsensowne dane albo błąd.
Błąd nr 5: Nie zamknięto strumienia. Jeśli nie użyjesz try-with-resources albo nie zamkniesz strumieni ręcznie, plik może pozostać niedostępny dla innych programów albo nie zapisać się do końca.
GO TO FULL VERSION