Класс Java NIO Files
Высокоуровневые файловые операции в Java с java.nio.file.Files — чтение, запись, копирование, перемещение, обход.
Path (из предыдущей главы) — это существительное. Files — глагол: статический вспомогательный класс, каждый метод которого принимает Path и выполняет что-то с файлом по этому пути. Именно здесь живут однострочники, которые незаметно укорачивали остальные части этой главы: Files.readString, Files.newBufferedReader, Files.createTempFile, Files.size. В этой главе рассматривается полный каталог методов.
Files — большой класс: около 80 методов, сгруппированных по назначению: чтение, запись, создание, инспекция, изменение, обход. Запоминать их все не нужно — достаточно знать, что именно здесь стоит искать, когда нужно что-то сделать с файлом.
Чтение
Однострочные читатели всего файла:
String text = Files.readString(path); // UTF-8 by default (Java 11+)
String utf16 = Files.readString(path, StandardCharsets.UTF_16);
byte[] bytes = Files.readAllBytes(path);
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);Для файлов, достаточно небольших, чтобы поместиться в памяти, readString и readAllBytes — правильный выбор. Они открывают файл, читают его целиком, закрывают и возвращают содержимое. Никаких потоков, буферов или логики закрытия.
Для файлов, слишком больших для полной загрузки, используйте потоковые формы:
try (BufferedReader r = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = r.readLine()) != null) process(line);
}
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
lines.filter(...).forEach(...); // closes the file when the stream closes
}
try (InputStream in = Files.newInputStream(path)) {
// raw bytes for binary formats
}Files.lines — это BufferedReader.lines с уже встроенной логикой открытия и закрытия. try-with-resources вокруг Stream выполняет закрытие — без него дескриптор файла утечёт.
Запись
Та же структура на стороне записи:
Files.writeString(path, "hello\n", StandardCharsets.UTF_8);
Files.write(path, bytes); // byte[]
Files.write(path, lines, StandardCharsets.UTF_8); // Iterable<? extends CharSequence>Все три — атомарные однострочники: открыть, записать, закрыть. По умолчанию они создают или перезаписывают — если файл существовал, его предыдущее содержимое удаляется. Для дозаписи:
Files.writeString(path, "more\n", StandardCharsets.UTF_8, StandardOpenOption.APPEND);Для потоковой формы (постепенная запись):
try (BufferedWriter w = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
for (String line : lines) w.write(line);
}Опции открытия
Каждый метод чтения/записи, открывающий файл, принимает необязательные varargs типа StandardOpenOption:
| Опция | Значение |
|---|---|
READ | Открыть для чтения |
WRITE | Открыть для записи |
CREATE | Создать, если отсутствует; ничего не делать, если присутствует |
CREATE_NEW | Создать, если отсутствует; ошибка, если присутствует |
APPEND | Запись в конец файла |
TRUNCATE_EXISTING | Очистить содержимое при открытии |
DELETE_ON_CLOSE | Удалить при закрытии канала (временные файлы) |
SYNC / DSYNC | Блокировать запись до подтверждения ОС, что данные на диске |
Режим открытия по умолчанию для newBufferedWriter и writeString — CREATE, TRUNCATE_EXISTING, WRITE. По умолчанию для newBufferedReader и readString — READ. Явные опции переопределяют умолчания — передача любой опции отключает неявный набор, поэтому при настройке обычно нужно повторять неявные:
Files.newBufferedWriter(path, StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND); // appends, creates if absentСоздание
Files.createFile(path); // empty file; fails if it exists
Files.createDirectory(path); // single dir; fails if parent absent
Files.createDirectories(path); // recursive: like `mkdir -p`
Files.createSymbolicLink(link, target);
Files.createLink(link, target); // hard link
Path tmpFile = Files.createTempFile("prefix-", ".txt"); // in the default temp dir
Path tmpDir = Files.createTempDirectory("prefix-");createDirectories — правильный инструмент для сценария «хочу, чтобы этот каталог существовал». Метод идемпотентен: если каталог уже есть, он возвращает управление без ошибки; если какой-либо предок отсутствует, создаёт всю цепочку. createDirectory (без -ies) делает только один уровень и завершается с ошибкой, если родительского каталога нет — почти всегда это неверный выбор, если только вам специально не нужна такая проверка.
Для временных файлов перегрузки createTempFile и createTempDirectory автоматически выбирают системный временный каталог и возвращают созданный Path. Используйте .toFile().deleteOnExit() для очистки или явный Files.delete в блоке finally.
Инспекция
Предикаты и методы доступа:
boolean ok = Files.exists(path);
boolean nope = Files.notExists(path); // NOT the negation of exists
boolean file = Files.isRegularFile(path);
boolean dir = Files.isDirectory(path);
boolean link = Files.isSymbolicLink(path);
boolean read = Files.isReadable(path);
boolean write = Files.isWritable(path);
boolean exec = Files.isExecutable(path);
long size = Files.size(path); // throws IOException
FileTime mtime = Files.getLastModifiedTime(path);
String mimeType = Files.probeContentType(path); // best-effort, can return null
UserPrincipal owner = Files.getOwner(path);exists и notExists — не отрицания друг друга: оба могут вернуть false, когда доступ к файлу невозможно определить (отказ в доступе, оборванная символическая ссылка). Используйте нужный из них — !exists(p) и notExists(p) различаются в граничных случаях.
Копирование, перемещение, удаление
Files.copy(source, target); // fails if target exists
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.copy(source, target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES); // copy mtime/owner too
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE); // rename within a filesystem; rename-or-fail
Files.delete(path); // throws if absent
boolean deleted = Files.deleteIfExists(path); // idempotentFiles.move с ATOMIC_MOVE — правильный инструмент для сценария «записать во временный файл, затем атомарно заменить рабочий файл». На одной файловой системе это преобразуется в rename(2); рабочий файл мгновенно переключается со старого на новый без промежуточного состояния. Так строятся записи, безопасные при сбоях:
Path tmp = path.resolveSibling(path.getFileName() + ".tmp");
Files.writeString(tmp, content, StandardCharsets.UTF_8);
Files.move(tmp, path, StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);Если JVM завершится аварийно после writeString, но до move, рабочий файл останется нетронутым.
Список и обход
try (Stream<Path> entries = Files.list(directory)) {
entries.forEach(System.out::println); // direct children only
}
try (Stream<Path> tree = Files.walk(directory)) {
tree.filter(Files::isRegularFile).forEach(...); // recursive
}
try (Stream<Path> tree = Files.walk(directory, 2)) { // depth-limited
...
}
try (Stream<Path> found = Files.find(directory, Integer.MAX_VALUE,
(p, attrs) -> attrs.isRegularFile() && p.toString().endsWith(".log"))) {
...
}Всегда используйте try-with-resources вокруг этих методов — нижележащий DirectoryStream открыт до закрытия Stream. Пропустите закрытие — и JVM будет удерживать дескриптор каталога до вмешательства сборщика мусора, что в долго работающем процессе означает «никогда». В следующей главе, Java Walk File Tree, подробнее рассматривается обходчик.
Почему эта глава короткая
Files не требует обширного изложения. Каждый метод делает одну вещь, имена методов говорят сами за себя, параметры — это Path, Charset и Option. Когнитивная нагрузка — в каталоге: знать, что доступно, а не в поведении каждого конкретного метода. Пролистайте Javadoc для java.nio.file.Files один раз; возвращайтесь, когда нужен глагол, который вы не помните.
Пример: полный жизненный цикл
Программа ниже создаёт временный каталог, записывает небольшой текстовый файл с помощью writeString, читает его обратно с помощью readString, дозаписывает с нужной опцией открытия, копирует файл, перемещает его атомарно, на каждом шаге отображает содержимое каталога и в конце очищает с помощью deleteIfExists. Это повседневный жизненный цикл файла в Java, сжатый в один метод main.
Что важно извлечь из выполнения:
Files.writeString(...)открыл файл, записал содержимое и закрыл его — один вызов там, гдеjava.ioпотребовал быFileOutputStream+OutputStreamWriter(UTF-8)+BufferedWriter+try-with-resources. Умолчание «усечь при открытии» — именно то, что нужно для «сохранить это содержимое». Когда нужно сохранить существующее содержимое, явныйStandardOpenOption.APPEND(передаётся вместе сWRITE) — это переопределение.Files.lines(log).filter(...)выполнил ту же работу потокового чтения, что иBufferedReader.lines(), со встроенной логикой открытия-закрытия.try-with-resources вокругStream— это механизм закрытия; пропустите его — и дескриптор файла утечёт. Каждый методFiles, возвращающийStream, являетсяCloseable; относитесь к нему соответственно.- Шаг копирования использовал и
REPLACE_EXISTING(разрешить перезапись), иCOPY_ATTRIBUTES(перенести mtime/owner). БезCOPY_ATTRIBUTESу резервной копии было бы свежее mtime, что важно для проверок «актуальна ли эта резервная копия?».Files.copyпо умолчанию ведёт себя консервативно; всё остальное активируется явно. - Блок атомарного перемещения — это шаблон безопасной записи: записать содержимое в
target.tmp, затем применитьATOMIC_MOVEк рабочему имени. Если JVM аварийно завершится в середине записи, рабочий файл останется нетронутым; если переименование успешно, рабочий файл переключается мгновенно. На одной файловой системе это преобразуется вrename(2)— шага копирования нет. Используйте это для любого файла, где читатели никогда не должны видеть частично записанное состояние (конфигурация, сохранённые файлы, сгенерированные ресурсы). Files.walk(dir)создалStream<Path>из каждого элемента под каталогом в порядке обхода в глубину. Очистка на шаге 10 выполнила сортировку в обратном порядке, чтобы дочерние элементы удалялись до родительских — тот же приём, что и при рекурсивном удалении. (Полноценный вспомогательный метод удаления дерева находится в следующей главе в разделеwalkFileTree; потоковая форма здесь — более короткий вариант для небольших деревьев.)
Что дальше
Files охватывает операции, действующие на один файл или один уровень каталога. Следующая глава, Java Walk File Tree, подробнее рассматривает обход всего дерева каталогов — Files.walkFileTree, FileVisitor, пропуск поддеревьев, API паттерна «Посетитель», обрабатывающий случаи, с которыми потоковая форма не справляется.