使用nio.file.DirectoryStream递归列出目录中的所有文件

44

我希望列出指定目录及其子目录中的所有文件,不包括目录。

以下是我的当前代码。它无法正常工作,因为它只列出了指定目录中的文件和目录。

我该如何解决这个问题?

final List<Path> files = new ArrayList<>();

Path path = Paths.get("C:\\Users\\Danny\\Documents\\workspace\\Test\\bin\\SomeFiles");
try
{
  DirectoryStream<Path> stream;
  stream = Files.newDirectoryStream(path);
  for (Path entry : stream)
  {
    files.add(entry);
  }
  stream.close();
}
catch (IOException e)
{
  e.printStackTrace();
}

for (Path entry: files)
{
  System.out.println(entry.toString());
}

4
@BrianRoach 这怎么是一个重复的问题?我是想解决使用 nio.file.DirectoryStream 的问题。 - Danny Rancher
@BrianRoach 这不是重复的问题,这个问题是关于Java 7 api的DirectoryStream,而不是你链接到的listFiles()。 - Fred
@fred 你的意思是按照那个问题中得到赞同的两个答案所解释的完全的方式去做吗? - Brian Roach
1
@BrianRoach 唯一“upvoted”的答案是直接回答问题并引用了DirectoryStream,仍然不是重复的。 - Fred
3
@BrianRoach,我希望你能提供一个使用Java 7 nio的方法。你认为我是在重复一个关于使用Java 6 io的方法的问题。这两个问题是不同的。请认识到你的错误。谢谢。 - Danny Rancher
显示剩余3条评论
9个回答

77

Java 8 提供了一个好的方法来实现这个:

Files.walk(path)

此方法返回Stream<Path>


更好的描述在这里:https://dev59.com/S3I95IYBdhLWcg3w_zas - reencode

32

编写一个方法,如果下一个元素是目录,则该方法将调用自身。

void listFiles(Path path) throws IOException {
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
        for (Path entry : stream) {
            if (Files.isDirectory(entry)) {
                listFiles(entry);
            }
            files.add(entry);
        }
    }
}

6
流应该始终关闭以释放资源 :) - Alan
如果您尝试在路径上使用带有过滤器参数的Files.newDirectoryStream,则此方法将无法正常工作。 - ACV
@ACV 我很好奇,为什么不呢? - Abhijit Sarkar
@AbhijitSarkar 我忘了,因为那是在2016年。你最近在哪里呢? - ACV
2
@ACV 我在这个领域待了一段时间。未经量化的声明的问题在于它们后来无法得到证实。如果我在1999年说2+2=4,我到2018年仍然能够证明它。 - Abhijit Sarkar

28

看看FileVisitor,非常好用。

 Path path= Paths.get("C:\\Users\\Danny\\Documents\\workspace\\Test\\bin\\SomeFiles");
 final List<Path> files=new ArrayList<>();
 try {
    Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
     @Override
     public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
          if(!attrs.isDirectory()){
               files.add(file);
          }
          return FileVisitResult.CONTINUE;
      }
     });
 } catch (IOException e) {
      e.printStackTrace();
 }

我认为这个解决方案没有Evgniy的解决方案那么简洁。 - Danny Rancher
3
这是个主观的问题。我喜欢FileVisitor方法。 - Adisesha
walkFileTree()的问题在于它是一个内部类,由于Java不支持闭包,你无法访问父元素。如果你只是操作文件,那么它会很好地工作,但如果你需要从另一个范围引用它,它就不能很好地匹配。 - Fred
2
@Fred 当你的解决方案需要修改原始或不可变数据类型时,内部类并不是合适的选择,而像JS这样的语言中的闭包则允许你这样做。例如,获取最长的文件名。我相信这就是你在最后一句话中所说的。我的回答是针对这个问题的背景而给出的。 - Adisesha
我喜欢这种方法! - silentsudo

6

如果你想避免函数递归调用并且使用文件列表作为成员变量,你可以使用一个栈:

private List<Path> listFiles(Path path) throws IOException {
    Deque<Path> stack = new ArrayDeque<Path>();
    final List<Path> files = new LinkedList<>();

    stack.push(path);

    while (!stack.isEmpty()) {
        DirectoryStream<Path> stream = Files.newDirectoryStream(stack.pop());
        for (Path entry : stream) {
            if (Files.isDirectory(entry)) {
                stack.push(entry);
            }
            else {
                files.add(entry);
            }
        }
        stream.close();
    }

    return files;
}

递归使用隐式栈,为什么要使用外部栈?你能得到什么好处吗? - Abhijit Sarkar
没有太大的区别,更多的是编码风格的偏好。以下是一些原因:如果递归太深,你最终会遇到StackOverflowException;你可以认为使用显式堆栈使代码更易于阅读;在这种情况下,您不需要为每个递归级别保留一个流;如果您需要执行任何类型的预处理或后处理(例如错误处理)或在递归期间保持上下文(例如添加文件列表),则无需再调用此函数。 - Duarte Meneses

4
这是我想到的最短实现方式:
final List<Path> files = new ArrayList<>();
Path path = Paths.get("C:\\Users\\Danny\\Documents\\workspace\\Test\\bin\\SomeFiles");
try {
    Files.walk(path).forEach(entry -> list.add(entry));
} catch (IOException e) {
    e.printStackTrack();
}

3
使用Rx Java,可以通过多种方式解决需求,同时仍然使用JDK中的DirectoryStream。
以下组合将给您所需的效果,我将按顺序解释它们:
方法1。使用flatMap()和defer()操作符进行递归处理
方法2。使用flatMap()和fromCallable操作符进行递归处理
注意:如果您将flatMap()的使用替换为concatMap(),则目录树导航将必须以深度优先搜索(DFS)的方式进行。使用flatMap(),不能保证DFS效果。
方法1:使用flatMap()和defer()
   private Observable<Path> recursiveFileSystemNavigation_Using_Defer(Path dir) {
       return Observable.<Path>defer(() -> {
            //
            // try-resource block
            //
            try(DirectoryStream<Path> children = Files.newDirectoryStream(dir))
            {
                //This intermediate storage is required because DirectoryStream can't be navigated more than once.
                List<Path> subfolders = Observable.<Path>fromIterable(children)
                                                        .toList()
                                                        .blockingGet();


                return Observable.<Path>fromIterable(subfolders)
                        /* Line X */    .flatMap(p -> !isFolder(p) ? Observable.<Path> just(p) : recursiveFileSystemNavigation_Using_Defer(p), Runtime.getRuntime().availableProcessors());

                //      /* Line Y */  .concatMap(p -> !isFolder(p) ? Observable.<Path> just(p) : recursiveFileSystemNavigation_Using_Defer(p));

            } catch (IOException e) {
                /*
                 This catch block is required even though DirectoryStream is  Closeable
                 resource. Reason is that .close() call on a DirectoryStream throws a 
                 checked exception.
                */
                return Observable.<Path>empty();
            }
       });
    }

这种方法是通过查找给定目录的子文件和子目录,然后将它们作为Observables发出。如果子项是文件,则立即可供订阅者使用;否则,在第X行上的flatMap()将递归调用该方法,并将每个子目录作为参数传递。对于每个这样的子目录,flatMap将在同一时间内内部订阅它们的所有子级。这就像一个需要控制的连锁反应。
因此,使用Runtime.getRuntime().availableProcessors()设置了flatMap()的最大并发级别,并防止其同时订阅所有子文件夹。如果不设置并发级别,想象一下当一个文件夹有1000个子项时会发生什么。
使用defer()可以防止过早地创建DirectoryStream,并确保只有在真正订阅以查找其子文件夹时才会创建。
最后,该方法返回一个Observable < Path >,以便客户端可以订阅并对结果执行有用的操作,如下所示:
//
// Using the defer() based approach
//
recursiveDirNavigation.recursiveFileSystemNavigation_Using_Defer(startingDir)
                    .subscribeOn(Schedulers.io())
                    .observeOn(Schedulers.from(Executors.newFixedThreadPool(1)))
                    .subscribe(p -> System.out.println(p.toUri()));

使用defer()的缺点是,如果其参数函数抛出已检查异常,则无法很好地处理。因此,即使DirectoryStream (实现了Closeable)在try-resource块中创建,我们仍然需要捕获IOException,因为自动关闭DirectoryStream会抛出该已检查异常。
在使用基于Rx的样式时,为了处理错误,使用catch()块听起来有点奇怪,因为即使错误也作为事件发送到反应式编程中。因此,为什么不使用一种操作符将这些错误公开为事件呢。
Rx Java 2.x添加了一种更好的替代方案,名为fromCallable()。第二种方法展示了它的使用。
方法2.使用flatMap()和fromCallable操作符
此方法使用fromCallable()操作符,该操作符将Callable作为参数。由于我们希望采用递归方法,因此来自可调用对象的期望结果是给定文件夹的子级的Observable。由于我们希望订阅者在结果可用时接收结果,因此需要从此方法返回一个Observable。由于内部可调用对象的结果是Observables子级列表,因此净效果是Observables Observables。
   private Observable<Observable<Path>> recursiveFileSystemNavigation_WithoutExplicitCatchBlock_UsingFromCallable(Path dir) {
       /*
        * fromCallable() takes a Callable argument. In this case the callbale's return value itself is 
        * a list of sub-paths therefore the overall return value of this method is Observable<Observable<Path>>
        * 
        * While subscribing the final results, we'd flatten this return value.
        * 
        * Benefit of using fromCallable() is that it elegantly catches the checked exceptions thrown 
        * during the callable's call and exposes that via onError() operator chain if you need. 
        * 
        * Defer() operator does not give that flexibility and you have to explicitly catch and handle appropriately.   
        */
       return Observable.<Observable<Path>> fromCallable(() -> traverse(dir))
                                        .onErrorReturnItem(Observable.<Path>empty());

    }

    private Observable<Path> traverse(Path dir) throws IOException {
        //
        // try-resource block
        //
        try(DirectoryStream<Path> children = Files.newDirectoryStream(dir))
        {
            //This intermediate storage is required because DirectoryStream can't be navigated more than once.
            List<Path> subfolders = Observable.<Path>fromIterable(children)
                                                    .toList()
                                                    .blockingGet();

            return Observable.<Path>fromIterable(subfolders)
                    /* Line X */    .flatMap(p -> ( !isFolder(p) ? Observable.<Path> just(p) : recursiveFileSystemNavigation_WithoutExplicitCatchBlock_UsingFromCallable(p).blockingSingle())
                                             ,Runtime.getRuntime().availableProcessors());

            //      /* Line Y */  .concatMap(p -> ( !isFolder(p) ? Observable.<Path> just(p) : recursiveFileSystemNavigation_WithoutExplicitCatchBlock_UsingFromCallable(p).blockingSingle() ));

        }
    }

订阅者将需要按照以下方式展开结果流:

//
// Using the fromCallable() based approach
//
recursiveDirNavigation.recursiveFileSystemNavigation_WithoutExplicitCatchBlock_UsingFromCallable(startingDir)
                        .subscribeOn(Schedulers.io())
                        .flatMap(p -> p)
                        .observeOn(Schedulers.from(Executors.newFixedThreadPool(1)))
                        .subscribe(filePath -> System.out.println(filePath.toUri()));

在traverse()方法中,为什么X行使用阻塞Get?
因为递归函数返回一个Observable ,但是flatmap在该行需要一个可订阅的Observable。
两种方法中,Y行都使用了concatMap()。
因为如果我们不想在flatmap()内部订阅时并行执行,那么就可以舒适地使用concatMap()。
在两种方法中,方法isFolder的实现如下:
private boolean isFolder(Path p){
    if(p.toFile().isFile()){
        return false;
    }

    return true;
}

Java RX 2.0的Maven坐标

<dependency>
    <groupId>io.reactivex.rxjava2</groupId>
    <artifactId>rxjava</artifactId>
    <version>2.0.3</version>
</dependency>

Java文件中的导入

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.concurrent.Executors;
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;

2
完善实现:它将从子文件夹读取每个文件,只是一个快速检查。
Path configFilePath = FileSystems.getDefault().getPath("C:\\Users\\sharmaat\\Desktop\\issue\\stores");
List<Path> fileWithName = Files.walk(configFilePath)
                .filter(s -> s.toString().endsWith(".java"))
                .map(Path::getFileName)
                .sorted()
                .collect(Collectors.toList());

for (Path name : fileWithName) {
    // printing the name of file in every sub folder
    System.out.println(name);
}

1
尝试这个...它遍历每个文件夹并打印文件夹和文件:-
public static void traverseDir(Path path) {
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
        for (Path entry : stream) {
            if (Files.isDirectory(entry)) {
                System.out.println("Sub-Folder Name : " + entry.toString());
                traverseDir(entry);
            } else {
                System.out.println("\tFile Name : " + entry.toString());
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

-1

尝试:您将获得目录和子目录路径列表; 可能有无限的子目录,请尝试使用递归过程。

public class DriectoryFileFilter {
    private List<String> filePathList = new ArrayList<String>();

    public List<String> read(File file) {
        if (file.isFile()) {
            filePathList.add(file.getAbsolutePath());
        } else if (file.isDirectory()) {
            File[] listOfFiles = file.listFiles();
            if (listOfFiles != null) {
                for (int i = 0; i < listOfFiles.length; i++){
                    read(listOfFiles[i]);
                }
            } else {
                System.out.println("[ACCESS DENIED]");
            }
        }
        return filePathList;
    }
}

谢谢您的回答,但是这并没有使用问题中指定的nio.file.DirectoryStream。敬礼。 - Danny Rancher
是的,它只使用了 java.io. - Zaw Than oo

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