Удаление файлов в Java
Удаление файлов и директорий в Java: File.delete, Files.delete и Files.deleteIfExists — отличия и применение.
Три небольших вызова, одно большое различие. File.delete() возвращает boolean на всё — от успешного удаления до отказа в доступе; Files.delete() бросает конкретные исключения для каждого сбоя; Files.deleteIfExists() — промежуточный вариант: boolean только для ответа на вопрос «удалил ли я что-нибудь?», а для реальных ошибок — исключения. Выбор между ними зависит от того, насколько важно знать причину неудачи.
Также рассматривается: удаление непустого дерева директорий (ни один из трёх вызовов не делает этого самостоятельно), опция открытия DELETE_ON_CLOSE для временных файлов, и безопасный паттерн «перемести, затем удали» для атомарной замены файла.
File.delete() — устаревший, возвращает boolean
File f = new File("notes.txt");
boolean ok = f.delete(); // true on success
// false on every failure: missing, locked, perms, non-empty dirТот же изъян устаревшего API, что у mkdir и renameTo: один boolean для всех исходов. Метод возвращает false, если файл не существует, если недостаточно прав, если путь указывает на непустую директорию или — в Windows — если другой процесс держит файл открытым. По возвращаемому значению нельзя понять, что именно пошло не так.
File.delete() удаляет:
- Обычный файл.
- Пустую директорию. Непустые директории дают
false. - Символическую ссылку саму по себе (не цель).
Если достаточно знать, что «файл исчез к концу вызова», лучше проверить f.exists() после, а не полагаться на возвращаемое значение:
f.delete();
if (f.exists()) throw new IOException("could not delete " + f);Именно такой паттерн встречается в старых кодовых базах как замена прямой проверки результата.
Files.delete(path) — современный, бросает исключения
Аналог из java.nio.file заменяет boolean конкретными исключениями:
Path p = Path.of("notes.txt");
Files.delete(p);
// throws NoSuchFileException — the file didn't exist
// throws DirectoryNotEmptyException — path was a non-empty directory
// throws AccessDeniedException — permission denied
// throws IOException — anything else (locked, OS error, network FS hiccup)Типы исключений являются подклассами IOException, поэтому широкий catch (IOException e) по-прежнему работает. Разница в том, что реальная обработка ошибок может быть точной:
try {
Files.delete(path);
} catch (NoSuchFileException e) {
// already gone — not an error in the "delete if there" pattern
} catch (DirectoryNotEmptyException e) {
// need a recursive delete; handled below
}Если «файл уже удалён» — это нормально, используйте Files.deleteIfExists вместо перехвата NoSuchFileException.
Files.deleteIfExists(path) — промежуточный вариант
boolean deleted = Files.deleteIfExists(path);
// returns true — the file existed and was deleted
// returns false — the file did not exist
// throws DirectoryNotEmptyException, AccessDeniedException, etc. for real failuresЗдесь boolean лишь различает «я что-то удалил» и «нечего удалять». Реальные ошибки по-прежнему вызывают исключения. Это именно тот вызов, который нужен для кода setUp / tearDown, идемпотентной очистки и паттерна «удали этот старый маркер, если он есть»:
Files.deleteIfExists(Path.of("lock")); // safe whether the lock was there or notИспользуйте таблицу:
| Что нужно… | Используйте |
|---|---|
| Устаревший boolean без информации об ошибке | File.delete() |
| «Удали или скажи, почему не вышло» | Files.delete(path) |
| «Удали, если есть; молчи, если нет» | Files.deleteIfExists(path) |
Удаление непустого дерева директорий
Ни один из одиночных методов удаления не удаляет непустую директорию. Существуют два стандартных паттерна:
Паттерн 1: обход с удалением в обратном порядке. Files.walk возвращает Stream<Path> в произвольном порядке; сортировка в обратном порядке помещает листья вперёд, так что каждый родительский элемент оказывается пустым к моменту его обработки.
try (Stream<Path> walk = Files.walk(root)) {
walk.sorted(Comparator.reverseOrder())
.forEach(p -> {
try { Files.delete(p); } catch (IOException e) { throw new UncheckedIOException(e); }
});
}Лаконично — именно этот вариант чаще всего используется сегодня. Компромисс: все пути загружаются в память для сортировки. Для директорий с миллионами записей предпочтительнее паттерн 2.
Паттерн 2: Files.walkFileTree с SimpleFileVisitor. Паттерн посетителя позволяет удалять листья при посещении и родительские директории в postVisitDirectory — без сортировки:
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override public FileVisitResult visitFile(Path file, BasicFileAttributes a) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});Результат тот же, без сортировки в памяти, но кода больше. API посетителя рассматривается в главе Walk file tree.
В JDK нет встроенного rmrf. Оба паттерна выше — стандартные заменители; во многих кодовых базах поверх одного из них оборачивают небольшой вспомогательный метод Files.deleteRecursively(root).
DELETE_ON_CLOSE — временные файлы, которые удаляются сами
Для случая «файл нужен только пока поток открыт» опция StandardOpenOption.DELETE_ON_CLOSE удаляет файл при закрытии потока — даже на пути исключения:
Path scratch = Files.createTempFile("work-", ".tmp");
try (BufferedWriter w = Files.newBufferedWriter(scratch,
StandardOpenOption.WRITE, StandardOpenOption.DELETE_ON_CLOSE)) {
// ... write to and read from scratch ...
} // scratch is gone after this brace, regardless of how we got hereНа большинстве Unix-систем файл немедленно отвязывается от директории при открытии (другие процессы больше не видят его по имени; только дескриптор удерживает его живым). Это правильный паттерн для короткоживущих временных данных — не нужно запоминать никакой конструкции try/finally.
File.deleteOnExit() — более старая, менее надёжная версия: она ставит удаление в очередь для выполнения при завершении JVM. Она не вызывается при kill -9 или аварийном завершении JVM, поэтому файлы могут утечь. Используйте DELETE_ON_CLOSE, когда время жизни файла привязано к потоку; используйте try/finally (или try-with-resources с оберткой AutoCloseable), когда это не так.
Замечание об «атомарной замене»
Замена одного файла другим атомарно — чтобы читатель никогда не увидел наполовину записанную версию — это не паттерн «удали, затем запиши». Стандартная идиома — «запиши в соседний файл, затем атомарно переименуй»:
Path target = Path.of("data.json");
Path tmp = target.resolveSibling("data.json.tmp");
Files.writeString(tmp, payload);
Files.move(tmp, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);ATOMIC_MOVE меняет два пути местами за один шаг ОС (на поддерживающих это файловых системах). Старый data.json заменяется; момента, когда файл написан наполовину или отсутствует, не существует.
Рабочий пример: каждый метод удаления и рекурсивная очистка
Программа ниже строит небольшое дерево, а затем поочерёдно проверяет каждый метод удаления — устаревший boolean, современную версию с исключениями, промежуточный вариант «если существует» и наконец паттерн Files.walk + reverseOrder для удаления всего дерева. Каждый шаг выводит, что произошло.
Что можно извлечь из запуска:
- Два вызова
File.delete()дляa.txtвернулиtrue, затемfalse. Второйfalseвыглядит идентично отказу в доступе — это и есть изъян устаревшего API. Files.delete(sub)бросилDirectoryNotEmptyException, аFiles.delete(missing)—NoSuchFileException. Два конкретных подклассаIOException, два разных режима сбоя — именно то, что boolean-API не может передать.Files.deleteIfExists(b)вернулtrueв первый раз иfalseво второй. Второйfalseозначает только «файла не было» — реальный сбой (отказ в доступе, блокировка) вызвал бы исключение.- Блок
Files.walk + reverseOrderудалил листья первыми, а родительские директории последними. Каждый вызовFiles.deleteпо пути успешно завершился, потому что к моменту обращения к директории её содержимое уже было удалено. - Файл
DELETE_ON_CLOSEисчез к моменту закрытия потока записи — гарантированно, даже на пути исключения. (На большинстве Unix-систем он отвязывается от директории сразу при открытии, поэтомуFiles.existsможет уже вернутьfalseвнутриtry; в Windows файл существует до закрытия дескриптора. В любом случае ничего не остаётся.) Это самый чистый паттерн для временных файлов в JDK — без хука завершения, без запоминанияtry/finally.
Что дальше
На этом завершаются высокоуровневые главы «делаем одно действие с файлом». Следующая глава, Byte Streams in Java, опускается на уровень ниже: InputStream и OutputStream — базовая побайтовая абстракция, на которой построены все файлы, сокеты и каналы в java.io. Многие вспомогательные методы, которые вы уже использовали — Files.readString, Files.newBufferedWriter, даже FileReader — являются декораторами над этими двумя интерфейсами.