W3docs

Запись файлов в 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 disk

BufferedWriterPrintWriter) накапливает записи в буфере в памяти. Байты не попадают на диск до тех пор, пока буфер не заполнится или не выполнится 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
Нужно форматирование printfPrintWriter поверх буферизованного writer'а
Только legacy-кодBufferedWriter(new FileWriter(...))

По умолчанию используйте Files.writeString, когда текст уже готов, и Files.newBufferedWriter, когда строите контент построчно. К PrintWriter обращайтесь только когда нужен printf.

Практический пример: все writer'ы рядом

Приведённая ниже программа записывает одно и то же содержимое тремя разными способами — современным одноразовым, построчным потоком через BufferedWriter и форматированным через PrintWriter с использованием printf — затем демонстрирует режимы APPEND и TRUNCATE_EXISTING по умолчанию, а также сценарий «забыл закрыть». Все записи выполняются во временный файл, поэтому пример запускается в любой среде.

java— editable, runs on the server

Что следует извлечь из этого примера:

  • 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() — а также способ удаления дерева каталогов без написания рекурсии вручную.

Практика

Практика
`Files.writeString(path, text)` без аргументов `OpenOption`. Что произойдёт, если файл уже существует?
`Files.writeString(path, text)` без аргументов `OpenOption`. Что произойдёт, если файл уже существует?
Was this page helpful?