W3docs

Удаление файлов в 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 для удаления всего дерева. Каждый шаг выводит, что произошло.

java— editable, runs on the server

Что можно извлечь из запуска:

  • Два вызова 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 — являются декораторами над этими двумя интерфейсами.

Практика

Практика
`Files.deleteIfExists(path)` возвращает `false`, когда…
`Files.deleteIfExists(path)` возвращает `false`, когда…
Was this page helpful?