Java.nio:最简洁的递归目录删除

63

我目前正试图递归删除一个目录... 奇怪的是,我能找到的最短的代码片段是以下结构,使用了一个临时内部类和一个访问者模式...

Path rootPath = Paths.get("data/to-delete");

try {
  Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
      System.out.println("delete file: " + file.toString());
      Files.delete(file);
      return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
      Files.delete(dir);
      System.out.println("delete dir: " + dir.toString());
      return FileVisitResult.CONTINUE;
    }
  });
} catch(IOException e){
  e.printStackTrace();
}

来源:这里

考虑到新的nio API可以去除大量冗余和样板文件,这种方法感觉非常笨拙且繁琐...

是否有更简洁的方式来强制递归删除目录?

我正在寻找纯本地Java 1.8方法,请不要链接到外部库...


这感觉非常笨拙和啰嗦。为什么?这是一个非常好的方法。而且Java 8的Files.walk不会给你这个机会。 - Tunaki
3
因为这迫使用户重新定义一个简单的递归删除... 因为这需要15行代码... 为什么不使用像 Files.deleteRecursively(Path) 这样的东西,或者可能一些可选标志? - fgysin
答案是,在内置的NIO.2中根本不存在。您可以使用Files.list进行递归处理,但这与您拥有的解决方案相同,我更喜欢您的解决方案。 - Tunaki
3
Kotlin在其stdlib中拥有这个函数。真的没有理由不包含它。 - KeksArmee
2
@KeksArmee,除此之外,Kotlin函数将始终遵循符号链接。 - FutureShocked
6个回答

148

您可以结合NIO 2和Stream API使用。

Path rootPath = Paths.get("/data/to-delete");
// before you copy and paste the snippet
// - read the post till the end
// - read the javadoc to understand what the code will do 
//
// a) to follow softlinks (removes the linked file too) use
// Files.walk(rootPath, FileVisitOption.FOLLOW_LINKS)
//
// b) to not follow softlinks (removes only the softlink) use
// the snippet below
try (Stream<Path> walk = Files.walk(rootPath)) {
    walk.sorted(Comparator.reverseOrder())
        .map(Path::toFile)
        .peek(System.out::println)
        .forEach(File::delete);
}
  • Files.walk - 返回 rootPath 下的所有文件/目录,包括子目录和文件
  • .sorted - 将列表以相反的顺序排序,因此目录本身位于包括子目录和文件之后
  • .map - 将 Path 映射到 File
  • .peek - 只用于显示正在处理的条目
  • .forEach - 在每个 File 对象上调用 .delete() 方法

编辑 正如首先由@Seby提到并由@John Dough引用的那样,应该在try-with-resource结构中使用Files.walk()。感谢两位。

来自Files.walk文档

如果需要及时处理文件系统资源,应使用 try-with-resources 结构,以确保在完成流操作后调用流的 close 方法。

编辑

以下是一些数字。
目录/data/to-delete包含 jdk1.8.0_73 的解压缩的rt.jar和最新版本的activemq构建。

files: 36,427
dirs :  4,143
size : 514 MB

毫秒为单位的时间

                    int. SSD     ext. USB3
NIO + Stream API    1,126        11,943
FileVisitor         1,362        13,561
两个版本都没有打印文件名。限制因素最大的是驱动器,而不是实现方式。
编辑:
关于选项 FileVisitOption.FOLLOW_LINKS 的一些附加信息。
假设以下文件和目录结构。
/data/dont-delete/bar
/data/to-delete/foo
/data/to-delete/dont-delete -> ../dont-delete

使用

Files.walk(rootPath, FileVisitOption.FOLLOW_LINKS)

将会跟随符号链接,文件 /tmp/dont_delete/bar 也将被删除。

使用

Files.walk(rootPath)

不会跟随符号链接,文件/tmp/dont_delete/bar不会被删除。

注意:切勿在不理解其含义的情况下使用代码复制粘贴。


1
@Tunaki 我进行了一个快速测试,首先删除所有文件,然后删除所有目录 --> 结果需要的时间几乎是两倍。因为你必须要遍历所有目录两次。 - SubOptimal
1
@Tunaki 请看一下我的更新答案。我添加了一些图表。 - SubOptimal
4
дҪ дёәд»Җд№ҲиҰҒж·»еҠ FileVisitOption.FOLLOW_LINKSж Үеҝ—пјҹиҝҷж„Ҹе‘ізқҖдҪ дјҡжё…з©әз”ұз¬ҰеҸ·й“ҫжҺҘжҢҮеҗ‘зҡ„ж•ҙдёӘзӣ®ж Үж ‘гҖӮ - Sasha
7
不需要使用.map(Path::toFile);可以将.forEach方法的参数改为Files.delete - pdxleif
7
@pdxleif 然后您需要在传递给 forEach(...) 的消费者函数中处理 Files.delete(Path p)IOException。而 file.delete() 不会抛出异常。 - SubOptimal
显示剩余19条评论

15

这正是我所需要的,谢谢。 - Sean
工作得很好!谢谢 - Jhoan Manuel Muñoz Serrano
1
这不是原始发帖者所问的:“因此,请不要链接到外部库”。 - gouessej

9
以下解决方案无需将路径转换为文件对象:
Path rootPath = Paths.get("/data/to-delete");     
final List<Path> pathsToDelete = Files.walk(rootPath).sorted(Comparator.reverseOrder()).collect(Collectors.toList());
for(Path path : pathsToDelete) {
    Files.deleteIfExists(path);
}

你为什么要将数据收集到一个列表中,然后再迭代这个列表,而不是直接使用 forEach 终止流呢? - Amadán
1
我这样做是因为这样在文件删除过程中更容易捕获异常。 - asmaier
1
我明白了。受检异常很糟糕。;-) 或者,可以将Files.deleteIfExists()包装在自己的方法中,并在那里处理异常。问题是:在这种情况下,除了创建未经检查的异常之外,您还想要做什么呢? :-) - Amadán
1
检查型异常并不糟糕,如果它抛出了一个运行时异常,那你甚至都不会意识到这个“stream”可能会在中途失败并停止处理——无论是检查型异常还是未检查型异常,它都会在中途失败。 - john16384
1
你可以在不收集的情况下从流中获取可迭代对象; Iterable<Path> paths = Files.walk(...).sorted(...)::iterator; for (var path: paths) { ... } - Coderino Javarino

5

如果你必须使用Java 7和NIO

Path path = Paths.get("./target/logs");
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
  @Override
  public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
    Files.delete(file);
    return FileVisitResult.CONTINUE;
  }

  @Override
  public FileVisitResult postVisitDirectory(Path dir, IOException exc)
      throws IOException {
    Files.delete(dir);
    return FileVisitResult.CONTINUE;
  }
});

1
如果您正在使用虚拟文件系统(例如Zip),那么这是唯一的方法:FileSystems.newFileSystem("jar:file:my.zip", ...).getPath("path") - 只能使用NIO方法删除此路径。 - AlexV

0
Files.walk(pathToBeDeleted).sorted(Comparator.reverseOrder()).forEach(Files::delete);

如果需要“及时处理文件系统资源”,则需要使用“try with resources”模式来关闭流。

另外,可能是一个不受欢迎的评论,但使用库会更加清晰和易读。将代码放在共享函数中,它不会占用太多空间。每个查看您代码的人都必须验证此代码是否执行了正确的删除操作,这绝非显而易见。


1
这不会关闭流,因此目录本身可能被阻塞并处于不一致的状态。 - Seby
1
将路径映射到文件对我来说有一种不好的感觉。省略.map(),改为以.forEach(Files::delete)结束!注意这里的Files实用类是复数形式! - Amadán
1
@Amadán 已更新。 - user1122069
2
很遗憾,@Amadán是错误的,你不能像这样调用Files::delete,因为它会抛出已检查的异常,所以你现在的答案不起作用。 - john16384
@john16384,Files.walk也一样。如果不使用库函数,就无法避免检查异常,而每个人通常都应该这样做。 - user1122069
显示剩余3条评论

-1

FileUtils.deleteDirectoryApache Commons IO 递归删除一个目录。

示例:

Path pathToBeDeleted = TEMP_DIRECTORY.resolve(DIRECTORY_NAME);

boolean result = FileUtils.deleteDirectory(pathToBeDeleted.toFile());

更多信息请参见在Java中递归删除目录


3
我明确表示“我正在寻找纯粹的本地Java 1.8方法,所以请不要链接到外部库”... - fgysin
还有,这需要将路径转换为文件。 - kap

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接