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 или не закрывать потоки вручную, файл может остаться недоступным для других программ или не сохраниться до конца.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ