在学习编程的时候,回到Turbo Pascal时代,我设法使用FindFirst
, FindNext
和FindClose
函数在目录中列出文件。 首先,我想出了一个打印给定目录内容的过程。 您可以想象我为能够真正从自身调用该过程以递归遍历文件系统而感到自豪。 好吧,那时我还不知道递归一词,但是它确实有用。 Java中的类似代码如下所示:
public void printFilesRecursively(final File folder) {for (final File entry : listFilesIn(folder)) {if (entry.isDirectory()) {printFilesRecursively(entry);} else {System.out.println(entry.getAbsolutePath());}}
}private File[] listFilesIn(File folder) {final File[] files = folder.listFiles();return files != null ? files : new File[]{};
}
不知道File.listFiles()
可以返回null
,是吗? 这就是它发出I / O错误的信号,就像IOException
不存在一样。 但这不是重点。 System.out.println()
很少是我们所需要的,因此该方法既不可重用也不可组合。 这可能是“ 打开/关闭”原理的最佳反例。 我可以想象文件系统递归遍历的几个用例:
- 获取所有文件的完整列表以供显示
- 查找与给定模式/属性匹配的所有文件(还要检出
File.list(FilenameFilter)
) - 搜索一个特定文件
- 处理每个文件,例如通过网络发送
上面的每个用例都有一系列独特的挑战。 例如,我们不想建立所有文件的列表,因为在开始处理它之前将花费大量的时间和内存。 我们希望通过流水线计算(但没有笨拙的访问者模式)来处理文件的发现和延迟。 另外,我们还希望使搜索短路以避免不必要的I / O。 幸运的是,在Java 8中,其中一些问题可以通过流解决:
final File home = new File(FileUtils.getUserDirectoryPath());
final Stream<Path> files = Files.list(home.toPath());
files.forEach(System.out::println);
请记住, Files.list(Path)
(Java 8中的新增功能)没有考虑子目录,我们将在以后进行修复。 这里最重要的一课是: Files.list()
返回Stream<Path>
–我们可以传递,组合,映射,过滤等的值。它非常灵活,例如,计算我拥有的文件数非常简单在每个扩展名的目录中:
import org.apache.commons.io.FilenameUtils;//...final File home = new File(FileUtils.getUserDirectoryPath());
final Stream<Path> files = Files.list(home.toPath());
final Map<String, List<Path>> byExtension = files.filter(path -> !path.toFile().isDirectory()).collect(groupingBy(path -> getExt(path)));byExtension.forEach((extension, matchingFiles) ->System.out.println(extension + "\t" + matchingFiles.size()));//...private String getExt(Path path) {return FilenameUtils.getExtension(path.toString()).toLowerCase();
}
好吧,您可能会说,只是另一个API。 但是一旦我们需要更深入 ,递归遍历子目录,它就会变得非常有趣。 流的一项惊人功能是,您可以通过各种方式将它们相互组合。 老Scala说“ flatMap that shit”在这里也适用,请查看以下递归Java 8代码:
//WARNING: doesn't compile, yet:private static Stream<Path> filesInDir(Path dir) {return Files.list(dir).flatMap(path ->path.toFile().isDirectory() ?filesInDir(path) :singletonList(path).stream());
}
由filesInDir()
延迟生成的Stream<Path>
包含目录中的所有文件,包括子目录。 您可以通过调用map()
, filter()
, anyMatch()
, findFirst()
等将其用作任何其他流。但是它实际上如何工作? flatMap()
与map()
类似,但是map()
是直接的1:1转换, flatMap()
允许用多个条目替换输入Stream
中的单个条目。 如果使用map()
,则最终会得到Stream<Stream<Path>>
(或者可能是Stream<List<Path>>
)。 但是flatMap()
通过展开内部条目来展平该结构。 让我们看一个简单的例子。 想象Files.list()
返回两个文件和一个目录。 对于文件, flatMap()
随该文件一起接收一个元素的流。 我们不能简单地返回该文件,而必须对其进行包装,但实际上这是无操作的。 对于目录,它变得更加有趣。 在这种情况下,我们递归地调用filesInDir()
。 结果,我们获得了该目录的内容流,并将其注入到外部流中。
上面的代码简短,甜美,并且…无法编译。 这些讨厌的人再次检查了异常。 这是一个固定的代码,包装检查过的异常以保持理智:
public static Stream<Path> filesInDir(Path dir) {return listFiles(dir).flatMap(path ->path.toFile().isDirectory() ?filesInDir(path) :singletonList(path).stream());
}private static Stream<Path> listFiles(Path dir) {try {return Files.list(dir);} catch (IOException e) {throw Throwables.propagate(e);}
}
不幸的是,这种相当优雅的代码还不够懒。 flatMap()
急切求值,因此即使我们几乎不要求第一个文件,它始终会遍历所有子目录。 您可以尝试使用我的小型LazySeq
库,该库尝试提供甚至更lazy-seq
抽象,类似于Scala中的流或Clojure中的lazy-seq
。 但是,即使是标准的JDK 8解决方案也可能确实有用,并且可以大大简化您的代码。
翻译自: https://www.javacodegeeks.com/2014/07/turning-recursive-file-system-traversal-into-stream.html