Немного странное поведение Files.delete и Files.deleteIfExists

Я получил такой код:

paths.forEach(folderPath -> {
        Path to = folderPath.getRoot().resolve(folderPath.getParent().subpath(0, folderPath.getNameCount() - 1)); // До имени (исключительно)
        try {
            Files.list(folderPath).forEach(filePath -> {
                try { Files.move(filePath, to.resolve(filePath.getFileName()), StandardCopyOption.ATOMIC_MOVE); }
                catch (IOException e) { processException(e); }
            });
            if (Files.list(folderPath).count() == 0)
                Files.deleteIfExists(folderPath); // this call
        } catch (IOException e) { processException(e); }
    });

После вызова методов удаления я блокирую пустой каталог (сразу после его вызова проверил), но не удаляю до закрытия приложения. Я нахожу это немного странным, но хочу знать, почему это происходит.

(Я использую Windows 10)

1 ответ

Решение

Из документации Files.list(Path):

Этот метод должен использоваться внутри оператора try-with-resources или аналогичной структуры управления, чтобы гарантировать, что открытый каталог потока будет закрыт сразу после завершения операций потока.

Вы этого не делаете, поэтому применяется следующая часть Files.deleteIfExists(…):

В некоторых операционных системах может быть невозможно удалить файл, когда он открыт и используется этой виртуальной машиной Java или другими программами.

Вы должны использовать

paths.forEach(folderPath -> {
    Path to = folderPath.getParent();
    try {
        try(Stream<Path> files = Files.list(folderPath)) {
            files.forEach(filePath -> {
                try{Files.move(filePath, to.resolve(filePath.getFileName()), ATOMIC_MOVE);}
                catch (IOException e) { processException(e); }
            });
        }
        try {
            Files.deleteIfExists(folderPath);
        } catch(DirectoryNotEmptyException ex) {
            // may happen as you continue when Files.move fails,
            // but you already reported the original exception then
        }
    } catch (IOException e) { processException(e); }
});

Это закрывает поток файлов перед попыткой удалить каталог. Обратите внимание, что операция второго потока была удалена, этот вид предварительной проверки является расточительным и должен быть ненужным, когда все move операция прошла успешно. Но если какое-то другое приложение вставляет новый файл одновременно, нет никакой гарантии, что это не произойдет между вашим Files.list(folderPath).count() == 0 проверка и последующее deleteIfExists вызов.

Более чистым решением было бы помнить, когда move не удалось. Когда нет move не удалось, все еще не пустой каталог следует рассматривать как ошибочную ситуацию, о которой следует сообщать, как о любой другой ошибке, например

paths.forEach(folderPath -> {
    Path to = folderPath.getParent();
    try {
        boolean allMovesSucceeded;
        try(Stream<Path> files = Files.list(folderPath)) {
          allMovesSucceeded = files
            .map(filePath -> {
                try {
                    Files.move(filePath, to.resolve(filePath.getFileName()), ATOMIC_MOVE);
                    return true;
                }
                catch(IOException e) { processException(e); return false; }
            }).reduce(Boolean.TRUE, Boolean::logicalAnd);

        }
        if(allMovesSucceeded) Files.deleteIfExists(folderPath);
    } catch (IOException e) { processException(e); }
});
Другие вопросы по тегам