25

我正在尝试计算光盘上文件的大小。在 java-7 中,这可以使用Files.walkFileTree来完成,如我在此处的回答中所示。

但是,如果我想使用 java-8 流来执行此操作,它将适用于某些文件夹,但不适用于所有文件夹。

public static void main(String[] args) throws IOException {
    long size = Files.walk(Paths.get("c:/")).mapToLong(MyMain::count).sum();
    System.out.println("size=" + size);
}

static long count(Path path) {
    try {
        return Files.size(path);
    } catch (IOException | UncheckedIOException e) {
        return 0;
    }
}

上面的代码适用于路径a:/files/,但c:/它会抛出异常

Exception in thread "main" java.io.UncheckedIOException: java.nio.file.AccessDeniedException: c:\$Recycle.Bin\S-1-5-20
at java.nio.file.FileTreeIterator.fetchNextIfNeeded(Unknown Source)
at java.nio.file.FileTreeIterator.hasNext(Unknown Source)
at java.util.Iterator.forEachRemaining(Unknown Source)
at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Unknown Source)
at java.util.stream.AbstractPipeline.copyInto(Unknown Source)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(Unknown Source)
at java.util.stream.AbstractPipeline.evaluate(Unknown Source)
at java.util.stream.LongPipeline.reduce(Unknown Source)
at java.util.stream.LongPipeline.sum(Unknown Source)
at MyMain.main(MyMain.java:16)

我了解它的来源以及如何使用 Files.walkFileTree API 来避免它。

但是如何使用Files.walk() API 来避免这个异常呢?

4

5 回答 5

31

不,这个例外是无法避免的。

异常本身发生在 的延迟提取中Files.walk(),因此为什么您没有及早看到它以及为什么无法规避它,请考虑以下代码:

long size = Files.walk(Paths.get("C://"))
        .peek(System.out::println)
        .mapToLong(this::count)
        .sum();

在我的系统上,这将在我的计算机上打印:

C:\
C:\$Recycle.Bin
Exception in thread "main" java.io.UncheckedIOException: java.nio.file.AccessDeniedException: C:\$Recycle.Bin\S-1-5-18

并且由于在第三个文件的(主)线程上引发异常,该线程上的所有进一步执行都会停止。

我相信这是一个设计失败,因为它现在Files.walk是绝对不可用的,因为你永远不能保证遍历目录时不会出错。

需要注意的重要一点是堆栈跟踪包含一个sum()andreduce()操作,这是因为路径被延迟加载,所以在 点reduce(),大部分流机器被调用(在堆栈跟踪中可见),然后它获取路径,在这一点上UnCheckedIOException发生。

如果您让每个步行操作在它们自己的线程上执行,它可能会被规避。但这不是你想做的事情。

此外,检查一个文件是否真的可以访问是没有价值的(尽管在某种程度上很有用),因为你不能保证它在 1ms 之后仍然是可读的。

未来扩展

我相信它仍然可以修复,尽管我不知道它是如何FileVisitOption工作的。
目前有一个FileVisitOption.FOLLOW_LINKS,如果它基于每个文件运行,那么我怀疑FileVisitOption.IGNORE_ON_IOEXCEPTION也可以添加一个,但是我们无法在其中正确地注入该功能。

于 2014-04-04T16:57:14.280 回答
19

2017 年对于那些一直来到这里的人。

当您确定文件系统行为并且确实想在出现任何错误时停止时,请使用 Files.walk() 。通常 Files.walk 在独立应用程序中没有用处。我经常犯这个错误,也许我很懒。当我看到像 100 万个文件这样的小文件所花费的时间超过几秒钟时,我意识到我的错误。

我推荐walkFileTree。从实现FileVisitor接口开始,这里我只想统计文件。不好的类名,我知道。

class Recurse implements FileVisitor<Path>{

    private long filesCount;
    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
       return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        //This is where I need my logic
        filesCount++;
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
        // This is important to note. Test this behaviour
        return FileVisitResult.CONTINUE;
    }

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

    public long getFilesCount() {
        return filesCount;
    }
}

然后像这样使用您定义的类。

Recurse r = new Recurse();
Files.walkFileTree(Paths.get("G:"), r);
System.out.println("Total files: " + r.getFilesCount());

我相信你知道如何修改你自己的FileVisitor<Path>接口类的实现来做其他事情filesize,比如我发布的例子。参考文档中的其他方法

速度:

  • Files.walk : 20 分钟以上,但异常失败
  • Files.walkFileTree:5.6 秒,完美回答。

编辑:与所有事情一样,使用测试来确认处理异常的行为,除了我们上面选择不关心的那些之外,它们仍然会发生。

于 2017-01-09T06:16:58.180 回答
5

我发现使用 Guava 的 Files 类为我解决了这个问题:

    Iterable<File> files = Files.fileTreeTraverser().breadthFirstTraversal(dir);
    long size = toStream( files ).mapToLong( File::length ).sum();

toStream我将 Iterable 转换为 Stream 的静态实用程序函数在哪里。只是这个:

StreamSupport.stream(iterable.spliterator(), false);
于 2014-07-15T11:52:36.427 回答
3

简短的回答是你不能。

异常来自FileTreeWalker.visit.

准确地说,它试图newDirectoryStream在失败时构建一个(此代码超出您的控制范围):

// file is a directory, attempt to open it
DirectoryStream<Path> stream = null;
try {
    stream = Files.newDirectoryStream(entry);
} catch (IOException ioe) {
    return new Event(EventType.ENTRY, entry, ioe); // ==> Culprit <== 
} catch (SecurityException se) {
    if (ignoreSecurityException)
        return null;
    throw se;
}

也许你应该提交一个错误

于 2014-04-04T17:01:47.183 回答
1

过滤掉目录 -> Files::isRegularFile

try(Stream<Path> pathStream = Files.walk(Path.of("/path/to/your/dir"))
        ) {
            pathStream
                    .filter(Files::isRegularFile)
                    .forEach(System.out::println);
        } catch (IOException e) {
            e.printStackTrace();
        }
于 2021-09-02T20:32:24.850 回答