Создание файлов в Java
Создание файлов и директорий в Java с помощью File.createNewFile, Files.createFile и Files.createDirectories.
«Создать новый пустой файл» и «создать дерево директорий» — две простейшие операции с файловой системой, однако Java предоставляет четыре перекрывающихся способа выполнить каждую из них. Различия важны: это разница между «падает, если файл существует» и «молча перезаписывает», между mkdir и mkdir -p, между устаревшим методом, возвращающим boolean, и современным, выбрасывающим исключение.
Эта глава охватывает четыре способа создания:
File.createNewFile()— устаревшее создание файла.File.mkdir()/File.mkdirs()— устаревшее создание директорий.Files.createFile(path)— современное атомарное «создать или завершить с ошибкой».Files.createDirectory(path)/Files.createDirectories(path)— современное создание директорий.
А также вспомогательные методы для временных файлов (Files.createTempFile, Files.createTempDirectory) и флаги OpenOption, позволяющие писателям создавать файлы неявно.
File.createNewFile() — устаревший метод, возвращает boolean
File f = new File("data/users.txt");
boolean created = f.createNewFile(); // true if it actually created the file
// false if it already existed
// throws IOException if the parent dir is missingКонтракт — атомарная проверка и создание: ОС гарантирует, что ни один другой процесс не сможет создать файл по тому же пути между проверкой существования и самим созданием. Это делает createNewFile примитивной блокировкой в некоторых устаревших идиомах «PID-файла»: if (!f.createNewFile()) throw new IllegalStateException("already running");
Чего он не делает:
- Он не создаёт отсутствующие родительские директории.
new File("does/not/exist/file.txt").createNewFile()выбрасываетIOException. - Возвращает
false(а не исключение), если файл уже существует.
Если вам нужно лишь, чтобы файл существовал по завершении вызова, возврат false вполне приемлем. Если вам нужен гарантированно новый файл (семантика блокировки), false — сигнал для перехода к другому пути выполнения.
File.mkdir() и File.mkdirs()
new File("logs").mkdir(); // creates "logs" — fails if "." has no perms or parent missing
new File("a/b/c").mkdirs(); // creates "a", "a/b", and "a/b/c" — like `mkdir -p`Оба возвращают boolean, оба теряют информацию о причине сбоя. mkdir падает, если родительская директория отсутствует; mkdirs — нет. Оба возвращают true только при успешном создании директории — если она уже существует, возвращается false. В сочетании с проблемой «нет информации об ошибке» это пример устаревшего API, который обычно оборачивают в вспомогательный метод:
File dir = new File("data");
if (!dir.exists() && !dir.mkdirs()) throw new IOException("cannot create " + dir);Современный Files.createDirectories(path) — это однострочная замена.
Files.createFile(path) — современный метод, выбрасывает исключение
Files.createFile — это аналог File.createNewFile() из пакета java.nio.file с одним важным отличием: он выбрасывает исключение вместо возврата boolean.
Path p = Path.of("data/users.txt");
Files.createFile(p); // creates an empty regular file
// throws FileAlreadyExistsException if it exists
// throws NoSuchFileException if the parent is missingFileAlreadyExistsException — то, что вы перехватываете, если важен факт существования файла; NoSuchFileException — то, что вы перехватываете (или предотвращаете с помощью createDirectories), если родительская директория может отсутствовать. Типы исключений — конкретные подклассы IOException, поэтому обобщённый catch (IOException e) по-прежнему работает.
Можно передать аргументы FileAttribute для установки прав доступа POSIX в момент создания в Unix — чаще всего это используется для гарантии того, что секретные файлы (приватные ключи, токены) создаются с правами 0600:
import static java.nio.file.attribute.PosixFilePermissions.*;
var attr = asFileAttribute(fromString("rw-------"));
Files.createFile(Path.of("/tmp/secret"), attr); // born with 0600 permissions, atomically(На Windows этот вызов выбрасывает UnsupportedOperationException, поскольку там нет модели прав доступа POSIX — добавьте проверку платформы, если ориентируетесь на обе системы.)
Files.createDirectory и Files.createDirectories
Та же разница, что между mkdir и mkdir -p, только на основе исключений:
Files.createDirectory(Path.of("logs")); // one level deep; parent must exist
Files.createDirectories(Path.of("a/b/c")); // creates every missing ancestorcreateDirectory выбрасывает FileAlreadyExistsException, если целевой путь уже существует и не является директорией; если это директория — тоже выбрасывает (обычно нежелательное поведение).
createDirectories — более удобный вариант: он ничего не делает, если все директории уже существуют, и создаёт то, чего не хватает. Он не выбрасывает исключение, если путь уже существует как директория. Это делает его идемпотентным — безопасным для вызова при старте без проверки exists().
Временные файлы и директории
Для тестов, временного хранилища и ситуаций «мне нужно куда-то положить это на несколько минут» JDK предоставляет Files.createTempFile и Files.createTempDirectory:
Path scratch = Files.createTempFile("session-", ".log"); // /tmp/session-3829387.log
Path workdir = Files.createTempDirectory("export-"); // /tmp/export-1827392Оба выбирают уникальное имя в системной временной директории, оба возвращают Path к созданному объекту и оба создают объект с ограниченными правами доступа в Unix. Префикс и суффикс — это подсказки, к которым JDK добавляет уникальное значение; выбрать точное имя нельзя (в этом и смысл: другой вызывающий не может его предсказать и перезаписать ваш файл).
Временные файлы не удаляются автоматически. Нужно либо:
- Вызвать
Files.deleteIfExists(path)по завершении работы; либо - Вызвать
path.toFile().deleteOnExit(), чтобы запланировать удаление при завершении JVM (не срабатывает при принудительном завершении); либо - Открыть файл с
StandardOpenOption.DELETE_ON_CLOSE, если он нужен только на время открытого потока.
Писатели создают файлы неявно
В большинстве случаев вызов «создать файл» вообще не нужен — писатель создаёт его за вас. Files.newBufferedWriter, Files.write и Files.writeString принимают varargs OpenOption..., определяющие поведение при наличии или отсутствии файла:
import static java.nio.file.StandardOpenOption.*;
Files.writeString(path, "hello\n", CREATE, WRITE, TRUNCATE_EXISTING);
Files.writeString(path, "more\n", CREATE, WRITE, APPEND);
Files.writeString(path, "new\n", CREATE_NEW); // fails if file existsCREATE— создать, если отсутствует; иначе открыть существующий.CREATE_NEW— создать; выброситьFileAlreadyExistsException, если файл уже существует. Та же семантика, что уFiles.createFile.TRUNCATE_EXISTING— очистить содержимое файла при открытии (поведение по умолчанию дляwriteStringбез режима добавления).APPEND— записывать в конец без усечения.
По умолчанию Files.writeString (без опций) использует CREATE, WRITE, TRUNCATE_EXISTING — то есть «создать или перезаписать». Files.newBufferedWriter по умолчанию ведёт себя так же. Для режима добавления нужно указать его явно.
Практический пример: все способы создания рядом
Программа ниже строит небольшое дерево директорий с нуля в системной временной папке, используя оба API и несколько режимов открытия. Каждый шаг выводит, что изменилось; последний блок демонстрирует, что происходит при повторном выполнении операции над уже существующим путём.
Что стоит вынести из запуска:
- Первый
legacy.createNewFile()вернулtrue(создан); второй —false(уже существует).booleanне сообщает, что именно произошло — нужно помнить соглашение. deep.mkdirs()успешно выполнился один раз и вернулfalseпри повторном вызове. Этотfalseнеотличим от «нет прав доступа» или «родительская директория отсутствует» — именно эту проблему отсутствия информации об ошибке решаетFiles.Files.createFileна существующем пути выбросилFileAlreadyExistsException. Тип исключения конкретен, поэтому реальный обработчик может отличить «уже существует» от «нет прав доступа» без разбора строк.Files.createDirectories, вызванный дважды подряд, не причинил никакого вреда при втором вызове. Именно это свойство делает его правильным выбором для кода инициализации: не нужна проверка, просто вызываем.Files.writeString(log, "line 1\n")создал файл, потому чтоCREATEвходит в опции по умолчанию. Второй и третий вызовы явно использовалиAPPEND, и файл накопил три строки. Четвёртый вызов использовалCREATE_NEWи отказался перезаписывать. Значения по умолчанию рассчитаны на случай «перезаписать новым содержимым»; режим добавления нужно указывать явно.Files.createTempFile(root, "scratch-", ".tmp")создал имя вродеscratch-1827392.tmp— ваш префикс и суффикс плюс уникальный фрагмент, выбранный JVM, чтобы два параллельных вызова никогда не конфликтовали.- При очистке обход
rootвыполняется в обратном порядке, чтобы дочерние файлы и директории удалялись раньше родительских.Files.deleteотказывается удалять непустую директорию; такой порядок — это и есть реализация ручногоrm -rf.
Что дальше
Вы умеете создавать файлы; следующая глава, Чтение файлов в Java, посвящена их чтению — сначала с помощью современных однострочников (Files.readString, Files.readAllLines, Files.lines), затем с помощью классического стека декораторов FileReader / BufferedReader / Scanner, чтобы в последующих главах был фундамент для работы с устаревшим кодом.