高效遍历文件列表的方法

9
我正在寻找一种高效的方法来迭代一个或多个目录中的数千个文件。
似乎唯一迭代目录中文件的方法是使用File.list*()函数。这些函数有效地将文件列表加载到某种集合中,然后让用户迭代它。从时间/内存消耗的角度来看,这似乎是不实际的。我尝试查看commons-io和其他类似的工具,但它们最终都在某个地方调用File.list*()。JDK7的walkFileTree()很接近,但我无法控制何时选择下一个元素。
我有超过150,000个文件在一个目录中,并且经过许多-Xms/-Xmm试运行,我摆脱了内存溢出问题。但是填充数组所需的时间并没有改变。
我希望创建一种可迭代的类,该类使用opendir()/closedir()等函数按需惰性加载文件名。有办法做到这一点吗?
更新:

Java 7 NIO.2支持通过java.nio.file.DirectoryStream进行文件迭代。它是一个Iterable类。至于JDK6及以下版本,唯一的选择是File.list*()方法。


我不知道是否存在标准解决方案。我猜想除了在C中自己实现并通过JNI访问它之外,没有其他方法可以做到这一点... - Sergey Savenko
这个问题中的答案可能会有所帮助 - https://dev59.com/T3NA5IYBdhLWcg3wQ7Yw - charlemagne
我怀疑这里的真正问题是你只有一个包含150K个文件的目录。我肯定不想以这种方式对文件系统进行压力测试。你不能使用子目录吗?也许可以通过文件名前两个字符来分组文件? - Dilum Ranatunga
1
@DilumRanatunga:多年的经验告诉我,修复代码比要求用户改变工作方式更具成本效益 :) - Unmanned Player
这些文件名之间有共同的模式吗? - adranale
4个回答

4

以下是一个例子,演示如何迭代目录条目而不必将它们全部存储在数组中(159k)。根据需要添加错误/异常/关闭/超时处理。此技术使用辅助线程加载一个小的阻塞队列。

用法如下:

FileWalker z = new FileWalker(new File("\\"), 1024); // start path, queue size
Iterator<Path> i = z.iterator();
while (i.hasNext()) {
  Path p = i.next();
}

示例:

public class FileWalker implements Iterator<Path> {
  final BlockingQueue<Path> bq;
  FileWalker(final File fileStart, final int size) throws Exception {
  bq = new ArrayBlockingQueue<Path>(size);
  Thread thread = new Thread(new Runnable() {
    public void run() {
      try {
        Files.walkFileTree(fileStart.toPath(), new FileVisitor<Path>() {
          public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
            return FileVisitResult.CONTINUE;
          }
          public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
            try {
              bq.offer(file, 4242, TimeUnit.HOURS);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
            return FileVisitResult.CONTINUE;
          }
          public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
            return FileVisitResult.CONTINUE;
          }
          public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
            return FileVisitResult.CONTINUE;
          }
        });
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  });
  thread.setDaemon(true);
  thread.start();
  thread.join(200);
}
public Iterator<Path> iterator() {
  return this;
}
public boolean hasNext() {
  boolean hasNext = false;
  long dropDeadMS = System.currentTimeMillis() + 2000;
  while (System.currentTimeMillis() < dropDeadMS) {
    if (bq.peek() != null) {
      hasNext = true;
      break;
    }
    try {
      Thread.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
  return hasNext;
}
public Path next() {
  Path path = null;
  try {
    path = bq.take();
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  return path;
}
public void remove() {
  throw new UnsupportedOperationException();
}
}

谢谢!额外的线程部分有点烦人,但我会想办法将这个Runnable推到一些无人机线程上。 - Unmanned Player
@Eshan - 尽管它会死掉,但这只是小代价。但请记住,如果您的while(hasNext())提前终止,它将保持活动状态。正如您所注意到的那样,您需要添加一些故障保护代码。但是,这种技术可以使内存使用非常低。 - Java42

1

从时间/内存消耗的角度来看,这似乎是不切实际的。

即使有150,000个文件,也不会消耗过多的内存。

我希望创建一种可迭代的类,使用类似于opendir()/closedir()的函数来按需惰性加载文件名。有什么方法可以做到这一点吗?

您需要编写或查找本地代码库以访问这些C函数。这可能会引入更多问题而不是解决问题。我的建议是只使用File.list()并增加堆大小。


其实,还有另一种可行的hacky替代方案。使用System.exec运行ls命令(或Windows的等效命令),然后编写你的迭代器以读取和解析命令输出文本。这避免了使用Java中的原生库所涉及的复杂性。

该软件在15年前设计时犯了一个错误,即使用分叉线程来执行当时设计师认为的“并行”操作。今天的当前版本使用100多个线程,使用1.5 GiB+内存才能在JDK 6上运行。目录列表只会增加更多的内存占用。这就是我所说的不切实际的意思。在这里,JNI/System.exec()不是一个选项。 - Unmanned Player
"JNI/System.exec()在这里不是一个选项"。那么在使用Java 6时,你已经没有其他选择了。抱歉。 - Stephen C
1
15年前设计软件时,犯了一个错误,即通过分叉线程来完成当时设计师认为的“并行”操作。听起来你需要先解决这个问题。实际上,考虑到JNI和exec不是选项,你可能别无选择。但好消息是,你可以通过重构使用有界线程池执行器服务来替换猖獗的线程分叉,并因此消除90多个线程堆栈等的内存开销。 - Stephen C
或者计划C...对用户进行反弹。"如果您想在非常大的目录上运行此应用程序,请购买一台具有大量内存的64位机器,并在64位JVM中运行该应用程序。否则,它会崩溃。抱歉。" - Stephen C
我们已经做了相当长一段时间了。我感到有点不好,于是主动清理了代码,并开始推动管理层允许我(和其他几个人)重新编写一些主要的性能攻击性代码。 - Unmanned Player

0
你能否按文件类型分组加载以缩小批次范围?

将文件分成不同目录组听起来不错。我在一个用户的站点上尝试过这种方法,结果他们在两个目录(一个是“a-z”,另一个是“0-9”)中填满了数千个文件。正如我在另一条评论中所说的那样,修复代码比要求用户改变工作方式更容易 :) - Unmanned Player

0
我只是在想为什么一个普通的file.list()方法,它返回文件名的String[](而不是file.listFiles()),会消耗大量内存?这是一个本地调用,只返回文件的名称。也许你可以迭代它,并懒惰地加载你需要的任何文件对象。

我的错。这是个打字错误。应该是File.list() - Unmanned Player
这正是Charles在他的回答中写的。 - Unmanned Player

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