Запись файлов в Java
Запись текстовых и бинарных файлов в Java с FileWriter, BufferedWriter, PrintWriter и Files.writeString.
Запись файла в Java означает преобразование данных из памяти — String, List строк или byte[] — в байты на диске. В этой главе рассматриваются пять классов записи, к которым вы будете обращаться на практике, ситуации, когда каждый из них подходит, флаги StandardOpenOption, определяющие режим перезаписи или дополнения, и самая распространённая ошибка при записи: данные «не сохранились», потому что writer так и не был закрыт.
Запись следует той же схеме, что и чтение из предыдущей главы — современные однострочники поверх Files, классические декораторы поверх FileWriter и небольшой набор опций, определяющих поведение при наличии или отсутствии целевого файла.
Files.writeString(path, text) — весь файл за один вызов
Аналог Files.readString. Добавлен в Java 11.
Files.writeString(Path.of("notes.txt"), "hello world\n", StandardCharsets.UTF_8);Параметры открытия по умолчанию: CREATE, WRITE, TRUNCATE_EXISTING — то есть «создать, если отсутствует; перезаписать, если существует». Это поведение по умолчанию удивляет тех, кто ожидает режима дополнения; чтобы его включить, нужно указать явно:
Files.writeString(path, "another line\n", StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);Возвращает переданный Path (удобно для цепочек вызовов). Используйте, когда: у вас небольшой объём текста и нужен один вызов. Те же ограничения по памяти, что и у readString — не стоит собирать строку размером 4 ГБ в памяти лишь для того, чтобы её записать.
Files.write(path, lines) и Files.write(path, bytes)
Два перегруженных варианта одного Files.write:
Files.write(Path.of("hosts.txt"), List.of("alpha", "beta", "gamma"), StandardCharsets.UTF_8);
Files.write(Path.of("photo.png"), pngBytes);Перегрузка Iterable<? extends CharSequence> записывает каждый элемент на отдельной строке с разделителем \n. Перегрузка byte[] записывает сырые байты — это лучший выбор для бинарных данных, когда байты уже находятся в памяти.
Files.newBufferedWriter(path) — современная фабрика writer'ов
Потоковый аналог Files.newBufferedReader, работающий с дескрипторами файлов.
try (BufferedWriter w = Files.newBufferedWriter(
Path.of("out.txt"), StandardCharsets.UTF_8, StandardOpenOption.CREATE)) {
w.write("first line");
w.newLine();
w.write("second line");
w.newLine();
}Используйте, когда: вы записываете много небольших фрагментов (цикл по записям, потоковое преобразование, запись в лог) и не хотите сначала материализовывать всё содержимое в виде строки. Буфер группирует операции записи, поэтому ОС видит небольшое число крупных системных вызовов вместо множества мелких.
FileWriter и BufferedWriter — классический стек
Устаревший вариант «построй сам»:
try (BufferedWriter w = new BufferedWriter(new FileWriter("out.txt", StandardCharsets.UTF_8))) {
for (String line : lines) {
w.write(line);
w.newLine();
}
}Три слоя, снизу вверх: FileWriter записывает необработанные символы, используя указанную кодировку (или кодировку платформы по умолчанию — так делать не следует); BufferedWriter оборачивает его в буфер в памяти и предоставляет переносимый метод newLine(). Та же структура, но больше кода, чем в варианте с Files.newBufferedWriter. В новом коде предпочтителен современный фабричный метод; этот стек вы будете встречать в устаревшем коде.
Второй аргумент конструктора FileWriter — это append:
new FileWriter("out.txt", true); // append mode (boolean)
new FileWriter("out.txt", StandardCharsets.UTF_8); // overwrite, UTF-8
new FileWriter("out.txt", StandardCharsets.UTF_8, true); // append, UTF-8Конструктор (String, boolean) появился раньше версий с поддержкой кодировки. Смешивать их в одной кодовой базе — один из тех рисков при сопровождении legacy-кода, когда у одного класса два конкурирующих порядка аргументов.
PrintWriter — форматированный вывод
PrintWriter добавляет print, println и printf поверх любого Writer. Это тот же API, которым вы пользовались через System.out (который сам является PrintStream, байт-ориентированным собратом).
try (PrintWriter w = new PrintWriter(Files.newBufferedWriter(Path.of("report.txt")))) {
w.println("Report generated");
w.printf("user = %-10s total = %d%n", "alice", 42);
w.printf("user = %-10s total = %d%n", "bob", 17);
}Два важных момента:
printfиспользует%nв качестве разделителя строк для текущей платформы.\n— это жёстко заданный LF, который обычно нужен для лог-файлов и данных, читаемых машинами.PrintWriterпоглощаетIOException. Методыprint,printlnиprintfне бросают исключений — они устанавливают внутренний флаг ошибки, который можно проверить черезcheckError(). Это намеренное решение дляSystem.out(ошибки записи в консоль не должны завершать CLI-инструмент), но для файловых writer'ов это источник трудноуловимых ошибок. Если надёжная обработка ошибок важна, используйтеBufferedWriterдля записи, аPrintWriter— только для вспомогательных методов форматирования, или проверяйтеcheckError()после записи.
Флаги StandardOpenOption
Каждый современный writer принимает varargs OpenOption..., изменяющие семантику открытия:
| Опция | Значение |
|---|---|
CREATE | Создать файл, если он не существует; в противном случае открыть существующий. |
CREATE_NEW | Создать; выбросить FileAlreadyExistsException, если файл существует. Атомарная операция. |
TRUNCATE_EXISTING | Если файл существует, очистить его при открытии. |
APPEND | Записывать в конец файла без усечения. Атомарная операция на большинстве ОС. |
WRITE | Открыть для записи. Всегда подразумевается для writer'ов. |
SYNC / DSYNC | Блокировать каждую запись до подтверждения ОС о сохранении на диск. Медленно; обеспечивает долговечность при сбоях. |
DELETE_ON_CLOSE | Удалить файл при закрытии потока. |
Наиболее важные комбинации:
- Перезапись (по умолчанию):
CREATE, TRUNCATE_EXISTING. Это поведениеFiles.writeStringиFiles.newBufferedWriterпо умолчанию. - Дополнение:
CREATE, APPEND. Шаблон для лог-файлов. - Создать или завершиться с ошибкой:
CREATE_NEW. Шаблон для lock-файлов или защиты от перезаписи.
APPEND является атомарной операцией на уровне ОС в Unix: два процесса, дополняющих один файл, получают перемежающиеся блоки, но без разрывов записей внутри одного буферизованного фрагмента. Именно этот контракт делает APPEND стандартным шаблоном для логирования.
Ловушка «writer ничего не записал»
Это ошибка, с которой рано или поздно сталкивается каждая Java-кодовая база:
// WRONG — the writer is never closed
BufferedWriter w = Files.newBufferedWriter(path);
w.write("important data");
return; // tail buffer is still in memory; nothing reached the diskBufferedWriter (и PrintWriter) накапливает записи в буфере в памяти. Байты не попадают на диск до тех пор, пока буфер не заполнится или не выполнится close(). Без конструкции try-with-resources вы пропускаете закрытие, и ваши «сохранённые» данные исчезают.
// CORRECT
try (BufferedWriter w = Files.newBufferedWriter(path)) {
w.write("important data");
} // close() runs here; tail buffer is flushedЕсли данные нужны на диске до закрытия — например, утилита слежения за файлом должна видеть каждую строку лога — вызовите flush() явно. Files.newBufferedWriter не выполняет автоматический сброс после каждой записи; это и есть цена буферизации.
Какой writer выбрать
| Сценарий | Выбор |
|---|---|
| Небольшая строка, одна операция | Files.writeString |
| Список строк или массив байтов | Files.write |
| Потоковая запись множества строк | Files.newBufferedWriter |
Нужно форматирование printf | PrintWriter поверх буферизованного writer'а |
| Только legacy-код | BufferedWriter(new FileWriter(...)) |
По умолчанию используйте Files.writeString, когда текст уже готов, и Files.newBufferedWriter, когда строите контент построчно. К PrintWriter обращайтесь только когда нужен printf.
Практический пример: все writer'ы рядом
Приведённая ниже программа записывает одно и то же содержимое тремя разными способами — современным одноразовым, построчным потоком через BufferedWriter и форматированным через PrintWriter с использованием printf — затем демонстрирует режимы APPEND и TRUNCATE_EXISTING по умолчанию, а также сценарий «забыл закрыть». Все записи выполняются во временный файл, поэтому пример запускается в любой среде.
Что следует извлечь из этого примера:
Files.writeStringиFiles.write(List)— правильный выбор, когда всё содержимое уже готово. Оба перезаписывали файл каждый раз, потому что их параметры по умолчанию включаютTRUNCATE_EXISTING.BufferedWriterиPrintWriterработали внутриtry-with-resources. Только это гарантирует, что хвостовой буфер попадёт на диск — пропустите это, и получите ошибку «writer ничего не записал».- Последовательность APPEND/TRUNCATE записала
base, дополнилаappended, затем усекла и записалаtruncated. Итоговый файл содержал толькоtruncated\n— в этом и заключается ловушка: режим по умолчанию у каждого современного writer'а — перезапись, а не дополнение. Режим дополнения нужно указывать явно. CREATE_NEWдля существующего пути выбросилFileAlreadyExistsException. Это семантика «не перезаписывать» — полезна для lock-файлов и атомарных маркеров «выполнялось ли это раньше?».- Размер файла «протекающего» writer'а до вызова
flush()был равен 0. Байты находились в памяти, а не на диске; без ручногоflush()(или правильногоclose()) они были бы потеряны.
Что дальше
В следующей главе, Удаление файлов в Java, завершается цикл глав о высокоуровневых операциях с файлами: рассматриваются три метода удаления — File.delete(), Files.delete() и Files.deleteIfExists() — а также способ удаления дерева каталогов без написания рекурсии вручную.