使用Java递归列出目录中的所有文件

98

我有一个函数,可以递归打印目录中所有文件的名称。问题在于,我的代码非常慢,因为它必须在每次迭代时访问远程网络设备。

我的计划是首先递归加载目录中的所有文件,然后使用正则表达式筛选出不想要的所有文件。是否有更好的解决方案?

public static printFnames(String sDir) {
    File[] faFiles = new File(sDir).listFiles();
    for (File file : faFiles) {
        if (file.getName().matches("^(.*?)")) {
            System.out.println(file.getAbsolutePath());
        }
        if (file.isDirectory()) {
            printFnames(file.getAbsolutePath());
        }
    }
}

这只是一个测试。以后我不会再像这样使用代码;相反,我会将每个与高级正则表达式匹配的文件的路径和修改日期添加到数组中。


1
你的问题是什么?你只是想确认这段代码能够工作吗? - Richard JP Le Guen
不,我知道这段代码有效,但它非常缓慢,并且感觉很愚蠢,因为它需要访问文件系统并获取每个子目录的内容,而不是一次性获取所有内容。 - Hultner
1
可能是Java递归列出文件的重复问题。 - Prahalad Gaggar
20个回答

146
假设这是实际的生产代码,您需要编写,那么我建议使用已经解决了这种问题的解决方案 - Apache Commons IO,具体来说是FileUtils.listFiles()。它处理嵌套目录、过滤器(基于名称、修改时间等)。
例如,对于您的正则表达式:
Collection files = FileUtils.listFiles(
  dir, 
  new RegexFileFilter("^(.*?)"), 
  DirectoryFileFilter.DIRECTORY
);

这将递归搜索与正则表达式^(.*?)匹配的文件,并将结果作为集合返回。
值得注意的是,这并不比编写自己的代码更快,它正在执行相同的操作 - 在Java中遍历文件系统只是很慢。区别在于,Apache Commons版本中不会有任何错误。

是的,问题在于扫描文件夹需要一个多小时,每次启动程序检查更新都非常烦人。如果我用C语言编写程序的这一部分,其余部分用Java,会更快吗?如果是这样,是否会有任何显著的差异?目前,我已更改了if isdir行上的代码,并添加了一个正则表达式以使目录也必须匹配才能包含在搜索中。我看到您的示例中写着DirectoryFileFilter.DIRECTORY,我想我可以在那里使用一个正则表达式过滤器。 - Hultner
1
使用本地调用编写代码肯定会更快 - FindFirstFile / FineNextFile 允许您查询文件属性,而无需进行单独的调用 - 这对于高延迟网络可能会产生巨大的影响。 Java 对此的处理方式非常低效。 - Kevin Day
5
问题和答案都超过5年了,这段时间内Java推出了两个重大版本,因此您可能希望调查更新的功能,例如Java 7 NIO。 - Hultner
是的,但那也不错。顺便说一下,你的探究也是正确的。 - Hanzallah Afgan
显示剩余3条评论

83
在Java 8中,可以使用Files.find()进行一行代码的搜索,并支持任意大的深度(例如999),并且使用BasicFileAttributesisRegularFile()方法来寻找普通文件。
public static printFnames(String sDir) {
    Files.find(Paths.get(sDir), 999, (p, bfa) -> bfa.isRegularFile()).forEach(System.out::println);
}

为了添加更多过滤条件,可以加强lambda表达式,例如筛选出在最近24小时内修改过的所有jpg文件:

(p, bfa) -> bfa.isRegularFile()
  && p.getFileName().toString().matches(".*\\.jpg")
  && bfa.lastModifiedTime().toMillis() > System.currentMillis() - 86400000

3
建议始终在try-with-resources块中使用返回流的文件方法:否则,资源将保持打开状态。 - riccardo.tasso
终端操作会在流本身上调用close吗? - Dragas
@Dragas 是的。我的消费者只是一个简单的例子;在现实生活中,你会做一些更有用的事情。 - Bohemian
这是Files.find文档中的说明: API注释: 必须在try-with-resources语句或类似控制结构内使用此方法,以确保在流操作完成后及时关闭流的打开目录。编辑:终端操作不会关闭流: [https://dev59.com/014c5IYBdhLWcg3w7t7E#27382060] - radiantRazor

30

这是一个非常简单的递归方法,用于从给定的根目录获取所有文件。

它使用Java 7 NIO Path类。

private List<String> getFileNames(List<String> fileNames, Path dir) {
    try(DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
        for (Path path : stream) {
            if(path.toFile().isDirectory()) {
                getFileNames(fileNames, path);
            } else {
                fileNames.add(path.toAbsolutePath().toString());
                System.out.println(path.getFileName());
            }
        }
    } catch(IOException e) {
        e.printStackTrace();
    }
    return fileNames;
} 

18

使用Java 7,引入了更快速的遍历目录树的方法,具有PathsFiles功能。它们比“旧”的File方法要快得多。

以下是用正则表达式遍历并检查路径名称的代码:

public final void test() throws IOException, InterruptedException {
    final Path rootDir = Paths.get("path to your directory where the walk starts");

    // Walk thru mainDir directory
    Files.walkFileTree(rootDir, new FileVisitor<Path>() {
        // First (minor) speed up. Compile regular expression pattern only one time.
        private Pattern pattern = Pattern.compile("^(.*?)");

        @Override
        public FileVisitResult preVisitDirectory(Path path,
                BasicFileAttributes atts) throws IOException {

            boolean matches = pattern.matcher(path.toString()).matches();

            // TODO: Put here your business logic when matches equals true/false

            return (matches)? FileVisitResult.CONTINUE:FileVisitResult.SKIP_SUBTREE;
        }

        @Override
        public FileVisitResult visitFile(Path path, BasicFileAttributes mainAtts)
                throws IOException {

            boolean matches = pattern.matcher(path.toString()).matches();

            // TODO: Put here your business logic when matches equals true/false

            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult postVisitDirectory(Path path,
                IOException exc) throws IOException {
            // TODO Auto-generated method stub
            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult visitFileFailed(Path path, IOException exc)
                throws IOException {
            exc.printStackTrace();

            // If the root directory has failed it makes no sense to continue
            return path.equals(rootDir)? FileVisitResult.TERMINATE:FileVisitResult.CONTINUE;
        }
    });
}

5
很好的回答:)还有一个名为“SimpleFileVisitor”的实现类,如果你不需要所有实现的功能,可以只覆盖所需的函数。 - GalDude33

14

使用Java 7 NIO快速获取目录内容的方法:

import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.FileSystems;
import java.nio.file.Path;

...

Path dir = FileSystems.getDefault().getPath(filePath);
DirectoryStream<Path> stream = Files.newDirectoryStream(dir);
for (Path path : stream) {
   System.out.println(path.getFileName());
}
stream.close();

3
好的,但是只获取一个目录的文件。如果您想查看所有子目录,请参考我的备选答案。 - Dan
3
"Files.newDirectoryStream" 可能会抛出 IOException 异常。我建议您将该行代码放在 Java7 的 "try-with-statement" 语句中,这样无论是否发生异常,流都将自动关闭,而不需要使用 finally 块。 可以参考此处:https://dev59.com/lmMm5IYBdhLWcg3wRdXe - Greg

12

Java提供的读取文件系统文件夹内容的接口性能不是很好(正如您所发现的)。 JDK 7通过完全新的接口修复了这个问题,这应该为这些操作带来本机级别的性能。

核心问题在于Java会为每个文件调用一个本机系统调用。在延迟较低的接口上,这并没有什么大问题,但是在甚至具有中等延迟的网络上,这确实会累积起来。如果您对上述算法进行分析,您会发现大部分时间都花费在麻烦的isDirectory()调用上,因为您为每个isDirectory()调用产生了一次往返。大多数现代操作系统可以在最初请求的文件/文件夹列表中提供此类信息(而不是查询每个单独的文件路径以获取其属性)。

如果您不能等待JDK7,解决这种延迟的一种策略是使用ExecutorService进行多线程处理,并设置最大线程数以执行您的递归。这不是很理想(您需要处理输出数据结构的锁定),但它比单线程处理要快得多。

在所有关于这种事情的讨论中,我强烈建议您与使用本机代码(甚至是执行大致相同操作的命令行脚本)时的最佳方案进行比较。说遍历网络结构需要一个小时并没有太大意义。告诉我们您可以在本机上完成这项工作只需要7秒,但在Java中需要一个小时,这会引起人们的关注。


3
现在已经有Java 7了,因此提供一个在Java 7中如何实现的示例将会很有帮助。或者至少提供一个链接或一个类名,在Google上进行搜索也可以。毕竟这里是「stackoverflow」而不是「理论计算机科学」;-)。 - Martin
3
好的,让我们看看......我的原帖是在2010年3月发布的......现在已经是2012年1月了......我刚刚查看了我的设备清单历史记录,没有发现自己在2010年3月拥有时间机器,所以我认为我可以回答问题而不必给出明确的例子;-) - Kevin Day
4
这是你要查找的文档链接:http://docs.oracle.com/javase/7/docs/api/java/nio/file/Files.html#walkFileTree%28java.nio.file.Path,%20java.util.Set,%20int,%20java.nio.file.FileVisitor%29。 - trutheality

7

这将完美地发挥作用,而且它是递归的。

File root = new File("ROOT PATH");
for (File file : root.listFiles())
{
    getFilesRecursive(file);
}


private static void getFilesRecursive(File pFile)
{
    for(File files : pFile.listFiles())
    {
        if(files.isDirectory())
        {
            getFilesRecursive(files);
        }
        else
        {
            // Do your thing
            //
            // You can either save in HashMap and
            // use it as per your requirement
        }
    }
}

1
如果您想使用Java <7,那么这是一个很好的答案。 - ssimm

4

我个人喜欢这个版本的FileUtils。以下是一个例子,它可以在目录或其任意子目录中找到所有的mp3或flac文件:

String[] types = {"mp3", "flac"};
Collection<File> files2 = FileUtils.listFiles(/path/to/your/dir, types , true);

3
这将能正常工作。
public void displayAll(File path){      
    if(path.isFile()){
        System.out.println(path.getName());
    }else{
        System.out.println(path.getName());         
        File files[] = path.listFiles();
        for(File dirOrFile: files){
            displayAll(dirOrFile);
        }
    }
}


1
欢迎来到StackOverflow,Mam's,请问您能否澄清一下您的答案是如何改进或替代现有众多答案的? - Lilienthal
这是从哪里复制来的? - Peter Mortensen

2

Java 8

public static void main(String[] args) throws IOException {

        Path start = Paths.get("C:\\data\\");
        try (Stream<Path> stream = Files.walk(start, Integer.MAX_VALUE)) {
            List<String> collect = stream
                .map(String::valueOf)
                .sorted()
                .collect(Collectors.toList());

            collect.forEach(System.out::println);
        }


    }

这是从哪里复制来的? - Peter Mortensen

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