假设有一个非常简单的程序,列出给定目录下所有子目录。听起来很简单吧?但在Java中列出所有子目录的唯一方法是使用FilenameFilter结合File.list()。
这对于简单情况可以工作,但当文件夹有大约150,000个文件和2个子文件夹时,等待45秒迭代所有文件并测试file.isDirectory()是愚蠢的。是否有更好的方法来列出子目录呢?
附注:抱歉,请不要讲授关于在同一目录中拥有太多文件的问题。这是我们实际环境的一部分要求。
假设有一个非常简单的程序,列出给定目录下所有子目录。听起来很简单吧?但在Java中列出所有子目录的唯一方法是使用FilenameFilter结合File.list()。
这对于简单情况可以工作,但当文件夹有大约150,000个文件和2个子文件夹时,等待45秒迭代所有文件并测试file.isDirectory()是愚蠢的。是否有更好的方法来列出子目录呢?
附注:抱歉,请不要讲授关于在同一目录中拥有太多文件的问题。这是我们实际环境的一部分要求。
正如已经提到的,这基本上是一个硬件问题。磁盘访问总是很慢,而且大多数文件系统实际上并不设计用于处理拥有那么多文件的目录。
如果由于某种原因必须将所有文件存储在同一目录中,我认为你将必须维护自己的缓存。这可以使用本地数据库(如sqlite、HeidiSQL或HSQL)来完成。如果您想获得极高的性能,请使用一个java TreeSet并将其缓存在内存中。这意味着至少您将不需要经常读取目录,并且可能可以在后台完成。您可以通过使用操作系统本机文件更新通知API(Linux上的inotify)订阅目录更改来进一步减少刷新列表的需求。
这似乎对你来说不可能,但我曾通过将文件“哈希”到子目录中来解决类似的问题。在我的情况下,挑战是存储数百万张具有数字ID的图像。我构建了以下目录结构:
images/[id - (id % 1000000)]/[id - (id % 1000)]/[id].jpg
这对我们很有效,我会推荐这种解决方法。你可以通过仅取文件名的前两个字母,然后是接下来的两个字母,来实现与字母数字文件名类似的功能。我曾经也这样做过一次,效果也很好。
你知道可能的子目录名称有限吗?如果是这样,请使用循环遍历所有可能的名称,并检查目录是否存在。
否则,在大多数底层操作系统中,您无法仅获取目录名称(例如,在Unix中,目录列表只是读取“目录”文件的内容,因此没有快速找到“仅目录”的方法,而不列出所有文件)。
然而,在Java7的NIO.2中(参见http://java.sun.com/developer/technicalArticles/javase/nio/#3),有一种方法可以获得流式目录列表,因此您不会得到完整的文件元素数组,从而减少了内存/网络的混乱。
当我在调试一个遍历大量文件的Java应用程序时,我遇到了类似的问题。它使用了一种旧的方法。
for (File f : new File("C:\\").listFiles()) {
if (f.isDirectory()) {
continue;
}
}
看起来每个 f.isDirectory() 都是对本地文件系统的调用,而至少在 NTFS 上,这种调用非常缓慢。Java7 NIO 有额外的API,但并不是所有方法都很好用。我将在此提供 JMH 基准测试结果。
Benchmark Mode Cnt Score Error Units
MyBenchmark.dir_listFiles avgt 5 0.437 ? 0.064 s/op
MyBenchmark.path_find avgt 5 0.046 ? 0.001 s/op
MyBenchmark.path_walkTree avgt 5 1.702 ? 0.047 s/op
这段代码的执行结果是数字:
java -jar target/benchmarks.jar -bm avgt -f 1 -wi 5 -i 5 -t 1
static final String testDir = "C:/Sdk/Ide/NetBeans/src/dev/src/";
static final int nCycles = 50;
public static class Counter {
int countOfFiles;
int countOfFolders;
}
@Benchmark
public List<File> dir_listFiles() {
List<File> files = new ArrayList<>(1000);
for( int i = 0; i < nCycles; i++ ) {
File dir = new File(testDir);
files.clear();
for (File f : dir.listFiles()) {
if (f.isDirectory()) {
continue;
}
files.add(f);
}
}
return files;
}
@Benchmark
public List<Path> path_walkTree() throws Exception {
final List<Path> files = new ArrayList<>(1000);
for( int i = 0; i < nCycles; i++ ) {
Path dir = Paths.get(testDir);
files.clear();
Files.walkFileTree(dir, new SimpleFileVisitor<Path> () {
@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes arg1) throws IOException {
files.add(path);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes arg1)
throws IOException {
return path == dir ? FileVisitResult.CONTINUE : FileVisitResult.SKIP_SUBTREE;
}
});
}
return files;
}
@Benchmark
public List<Path> path_find() throws Exception {
final List<Path> files = new ArrayList<>(1000);
for( int i = 0; i < nCycles; i++ ) {
Path dir = Paths.get(testDir);
files.clear();
files.addAll(Files.find(dir, 1, (path, attrs)
-> true /*!attrs.isDirectory()*/).collect(Collectors.toList()));
}
return files;
}
*.jpg
*Out.txt
只为那些你不确定是否是文件夹的对象实际创建文件对象。
我不确定使用 cmd.exe
是否会增加额外负担,但是可以有以下一种可能的方式:
...
Runtime r = Runtime.getRuntime();
Process p = r.exec("cmd.exe /k dir /s/b/ad C:\\folder");
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
for (;;) {
String d = br.readLine();
if (d == null)
break;
System.out.println(d);
}
...
cmd.exe
进程处于活动状态,并针对想要搜索的每个目录将命令传输到该进程。 - finnw/symlinks/a/b/cde
将链接到
/realfiles/abcde
(其中/realfiles是您的150,000个文件所在的位置)
您需要创建和维护此目录结构,我没有足够的信息来确定是否实用。但是上述操作将为您的非分层(且较慢)目录创建一个快速索引。
还有一个递归并行扫描 http://blogs.oracle.com/adventures/entry/fast_directory_scanning。基本上兄弟节点是并行处理的。还进行了鼓舞人心的性能测试。