Files.walk(),计算总大小

27

我正在尝试计算我的磁盘上文件的大小。在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:/,它会抛出以下异常:

以上代码适用于路径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来避免此异常呢?

5个回答

32

不,这个异常是无法避免的。

异常本身发生在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()reduce() 操作,这是因为路径被延迟加载,所以在调用 reduce() 时,流机制的大部分代码已经被调用(在堆栈跟踪中可见),然后它获取路径,在那一点上发生了 UnCheckedIOException 异常。如果让每个遍历操作在它们自己的线程上执行,可能可以规避这个问题。但这不是你想做的事情。此外,检查文件是否实际可访问是没有意义的(尽管某种程度上有用),因为你不能保证1毫秒后它是可读的。

未来扩展

我认为仍然可以解决这个问题,但我不知道 FileVisitOption 究竟是如何工作的。
目前有一个 FileVisitOption.FOLLOW_LINKS,如果它是基于每个文件操作的,那么我会认为可以添加一个 FileVisitOption.IGNORE_ON_IOEXCEPTION,但我们无法在其中正确地注入该功能。


6
同意,这是设计上的失误。 - Boon
好的分析。我认为我可能更喜欢另一个Files.walk(),它也接受一个错误处理程序或类似的东西。 - Aksel Willgert
7
好的,分析得很好,点赞。涵盖此问题的错误(增强请求)是JDK-8039910 - Stuart Marks
3
6年后,这个漏洞被标记为“未来项目”关闭,并且没有后续的迹象 :( - zb226

21

对于那些不断到达此处的人,这是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秒钟,完美地完成任务

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


1
Apache Commons-IO的DirectoryWalker与Files.walkFileTree的速度几乎相同。https://docs.leponceau.org/java-examples/java-evaluation/org.apache.commons.io.FileWalkerPerfTest.html vs https://docs.leponceau.org/java-examples/java-evaluation/tests.java.nio.file.FilesWalkFileTreePerfTest.html - user1050755
1
最好扩展SimpleFileVisitor(抽象类)而不是FileVisitor(接口)。然而,你必须重写visitFileFailed(),因为默认实现讽刺地模仿了Files.walk() - Mark Jeronimus
是的,人们会坚持使用Files.walk()并遇到相同的问题。这并不是一个错误,但接口存在的原因就在于此。这就是方法。 - Abhishek Dujari

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);

fileTreeTraverser现已被弃用。 - Drakes
@Drakes Files.fileTraverser().breadthFirst() 或者 MoreFiles.fileTraverser(sourcePath).breadthFirst() - MariuszS

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;
}

也许你应该提交一个漏洞报告

我已经提交了一个描述我的确切情况的问题,请参考http://stackoverflow.com/questions/23220542 - Muhammad Hewedy

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();
        }

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