1. Навіщо потрібні DataInputStream і DataOutputStream?
Коли ви працюєте з файлами, інколи потрібно зберігати не просто текст, а структуровані дані: числа, булеві значення, масиви примітивів. Наприклад, уявіть, що ви пишете просту гру й хочете зберегти прогрес користувача: кількість балів (int), поточний рівень (int), час гри (double), статус переможця (boolean). Звісно, можна записати це у текстовому вигляді:
12345
5
67.5
true
Але це незручно й небезпечно: рядки потрібно розбирати, формат може «поплисти», а числа займають більше місця.
Ідея полягає в тому, щоб записувати дані у файл у «сирому» (бінарному) вигляді, без перетворення на текст. Для цього в Java є два чудових класи:
- DataOutputStream — уміє записувати примітивні типи у потік.
- DataInputStream — уміє читати примітивні типи з потоку.
Вони працюють поверх звичайних байтових потоків (OutputStream/InputStream). Тобто це такі «надбудови», які вміють не просто писати байти, а знають, як із цих байтів зібрати int, double, boolean і навіть String.
Як це працює?
Звичайний FileOutputStream можна уявити як конвеєр, на який ви вручну кладете байти один за одним. Якщо ви хочете записати ціле число або рядок, доводиться самостійно стежити, скільки байтів займає кожен елемент.
DataOutputStream полегшує життя: він діє як робот на цьому конвеєрі. Ви кажете йому «запиши число» або «запиши рядок», і він сам пакує дані у потрібну кількість байтів і відправляє на диск. На іншому кінці конвеєра такий самий робот — DataInputStream — уміє зібрати ці байти назад у вихідні об’єкти.
Чому це зручно? Тому що вам не потрібно думати про кількість байтів для int, double або boolean. Дані зберігаються компактно, швидко читаються та записуються, і при цьому немає ризику помилок розбору або проблем із форматом.
2. Приклад: запис і читання примітивів
Припустімо, ми хочемо зберегти результати нашої (умовної) гри: ім’я гравця (String), кількість балів (int), рекордний час (double), чи переміг гравець (boolean).
Запис даних у файл
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); // Записуємо рядок (модифікований UTF-8)
dos.writeInt(score); // Записуємо int (4 байти)
dos.writeDouble(recordTime); // Записуємо double (8 байт)
dos.writeBoolean(isWinner); // Записуємо boolean (1 байт)
System.out.println("Дані успішно записано у файл!");
} catch (IOException e) {
System.out.println("Помилка запису: " + e.getMessage());
}
}
}
- writeUTF(String) — записує рядок у модифікованому UTF-8 (із довжиною на початку).
- writeInt(int) — пише 4 байти.
- writeDouble(double) — пише 8 байт.
- writeBoolean(boolean) — пише 1 байт (1 або 0).
- Усі методи автоматично «пакують» дані у потрібному форматі.
Читання даних із файлу
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(); // Читаємо рядок
int score = dis.readInt(); // Читаємо int
double recordTime = dis.readDouble(); // Читаємо double
boolean isWinner = dis.readBoolean(); // Читаємо boolean
System.out.println("Ім’я гравця: " + playerName);
System.out.println("Бали: " + score);
System.out.println("Час: " + recordTime);
System.out.println("Переможець: " + isWinner);
} catch (IOException e) {
System.out.println("Помилка читання: " + e.getMessage());
}
}
}
Важливий момент:
Порядок читання має збігатися з порядком запису! Якщо спочатку записали рядок, потім int, потім double — у такому самому порядку потрібно читати. Інакше отримаєте помилку або «кашу» з даних.
3. Які типи підтримуються?
DataOutputStream і DataInputStream підтримують усі основні примітивні типи Java:
| Метод запису | Метод читання | Тип даних | Розмір (байт) |
|---|---|---|---|
|
|
boolean | 1 |
|
|
byte | 1 |
|
|
short | 2 |
|
|
char | 2 |
|
|
int | 4 |
|
|
long | 8 |
|
|
float | 4 |
|
|
double | 8 |
|
|
String (UTF) | змінний |
Примітки:
- Для рядків зазвичай використовують writeUTF/readUTF (записують довжину рядка та самі байти у модифікованому UTF-8).
- Якщо потрібно записати масив, спочатку запишіть його довжину, а потім елементи по одному.
4. Розширений приклад: зберігаємо масиви примітивів
Запис масиву
int[] scores = {100, 200, 300, 400, 500};
try (DataOutputStream dos = new DataOutputStream(
new FileOutputStream("scores.bin"))) {
dos.writeInt(scores.length); // Спочатку пишемо довжину масиву
for (int score : scores) {
dos.writeInt(score); // Потім кожен елемент
}
}
Читання масиву
try (DataInputStream dis = new DataInputStream(
new FileInputStream("scores.bin"))) {
int length = dis.readInt(); // Зчитуємо довжину
int[] scores = new int[length];
for (int i = 0; i < length; i++) {
scores[i] = dis.readInt(); // Зчитуємо елементи
}
// Виводимо масив
for (int score : scores) {
System.out.println(score);
}
}
Чому спочатку довжина?
Тому що під час читання ми не знаємо, скільки чисел записано. Записавши довжину на початку, ми робимо формат файлу самодокументованим.
5. Важливі нюанси та особливості
Коли варто використовувати DataInputStream/DataOutputStream?
- Коли потрібно зберегти/завантажити структуровані дані, що складаються з примітивів.
- Для обміну бінарними даними між програмами на Java (або навіть різними мовами, якщо ви знаєте формат).
- Коли важлива компактність і швидкість (наприклад, журнали, результати обчислень, великі масиви чисел).
Коли не варто:
- Якщо потрібен людиночитний формат (CSV, JSON, XML) — використовуйте текстові формати.
- Для складних об’єктів із вкладеністю — краще застосувати серіалізацію через ObjectOutputStream/ObjectInputStream (окрема тема).
Буферизація
DataOutputStream і DataInputStream не буферизують дані самі собою. Якщо ви хочете підвищити продуктивність під час роботи з великими файлами, обгортайте їх у BufferedOutputStream/BufferedInputStream:
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream("data.bin")))) {
// ...
}
Кодування рядків
Методи writeUTF/readUTF використовують спеціальний формат: спочатку записується довжина рядка (у байтах), потім вміст у модифікованому UTF-8. Не плутайте з просто записом масиву байтів!
Винятки
Операції читання/запису можуть кинути IOException, якщо файл недоступний, пошкоджений або закінчився раніше, ніж очікувалося. При спробі читати більше, ніж записано, часто виникає EOFException. Завжди використовуйте конструкцію try-with-resources або обробляйте виняток у try-catch.
Порядок читання та запису
Найпоширеніша помилка — невідповідність порядку читання та запису. Якщо ви записали: int, double, boolean, а читаєте як double, int, boolean, отримаєте некоректні дані або виняток.
6. Типові помилки
Помилка № 1: Порушення порядку запису та читання. Якщо ви зміните порядок методів, дані буде прочитано неправильно або буде кинуто виняток. Наприклад, якщо спочатку записали рядок, а потім число, але під час читання намагаєтеся спочатку прочитати число — отримаєте помилку формату.
Помилка № 2: Забули записати довжину масиву. Якщо ви пишете масив примітивів, але не записали його довжину, під час читання ви не зрозумієте, скільки елементів потрібно прочитати. Це призводить або до помилки, або до «зайвих» даних у кінці.
Помилка № 3: Спроба читати після кінця файлу. Якщо ви читаєте більше даних, ніж було записано, отримаєте EOFException (end of file).
Помилка № 4: Використання DataInputStream/DataOutputStream для текстових файлів. Ці класи не призначені для читання звичайних текстових файлів, створених, наприклад, у Блокноті. Якщо ви спробуєте прочитати такий файл через readInt() — отримаєте безглузді дані або помилку.
Помилка № 5: Не закрили потік. Якщо не використовувати try-with-resources або не закривати потоки вручну, файл може залишатися недоступним для інших програм або не зберегтися до кінця.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ