【问题标题】:Java List Directory With Sorting Performance Issue具有排序性能问题的 Java 列表目录
【发布时间】:2022-10-24 18:00:36
【问题描述】:

我正在构建一个文件资源管理器,其中按名称(不区分大小写)对目录和文件进行排序,并在文件之前对目录进行排序,正在使用以下代码,但它在性能方面很慢,所以还有其他方法可以实现这一点:

File[] directories = new File(path).listFiles(File::isDirectory);
File[] files = new File(path).listFiles(File::isFile);

Arrays.sort(directories, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER));
Arrays.sort(files, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER));

File[] list = new File[directories.length + files.length];

System.arraycopy(directories, 0, list, 0, directories.length);  
System.arraycopy(files, 0, list, directories.length, files.length); 

【问题讨论】:

    标签: java performance


    【解决方案1】:

    首先,您应该进行基准测试以找出确切的瓶颈在哪里。 How do I write a correct micro-benchmark in Java? 应该是一个好的开始。

    现在,一个想法,这可能会有所帮助。

    1. 在一个数组中获取所有文件和目录。像这样,您只需一次操作即可获取文件和文件夹,而不是两次。访问磁盘上的文件和文件夹并不是非常快速的操作,因此您需要减少这些操作。此外,如果您查看listFiles(FileFilter) 的实现,它会迭代文件夹中的所有内容以查找匹配项,因此所有元素的迭代次数减少了 1 次。
      File[] directoriesAndFiles = folder.listFiles(file -> file.isDirectory() || file.isFile());
      
      1. 为排序编写复合比较器。从代码来看,您首先需要目录。
      public class FileTypeComparator implements Comparator<File> {
      
          @Override
          public int compare(File first, File second) {
              if (first.isDirectory() && second.isFile()) {
                  return -1;
              }
              if (first.isFile() && second.isDirectory()) {
                  return 1;
              }
              return 0;
          }
      }
      

      将它与您当前的比较器结合起来并进行排序:

      Comparator<File> compositeComparator = new FileTypeComparator()
          .thenComparing(File::getName, String.CASE_INSENSITIVE_ORDER);
      Arrays.sort(directoriesAndFiles, compositeComparator);
      

      像这样,您也不需要将初始数组复制到结果数组中。

    【讨论】:

      【解决方案2】:

      如果文件夹中有大量文件或使用非 SSD 存储,则对 File.listFiles()File.isDirectory(或 isFile)的调用会使文件夹扫描变得非常缓慢。可以一步进行扫描,但按目录/文件排序仍将再次重复对 isDirectory/isFile 的调用。

      相反,您应该考虑使用Files.find 实现,这意味着您可以同时读取所有文件属性,这样排序就不会再次重新读取文件系统属性。它在一个流中被巧妙地处理。这是一个仅打印当前文件夹的排序项目和修改时间的示例:

      public static Stream<Map.Entry<Path, BasicFileAttributes>>
      find(Path dir, int maxDepth, BiPredicate<Path, BasicFileAttributes> matcher, FileVisitOption... options) throws IOException {
      
          // Using ConcurrentHashMap is safe to use with parallel()
          ConcurrentHashMap<Path,BasicFileAttributes> attrs = new ConcurrentHashMap<>();
      
          BiPredicate<Path, BasicFileAttributes> predicate = (p,a) -> (matcher == null || matcher.test(p, a)) && attrs.put(p, a) == null;
          return Files.find(dir, maxDepth, predicate, options).map(p -> Map.entry(p, attrs.remove(p)));
      }
      
      public static void main_sort_files(String[] args) throws IOException {
          Path dir = Path.of(args[0]);
          int depth = 1;
      
          // Note it is easy to add more sort fields here:
          Comparator<Entry<Path, BasicFileAttributes>> compDirs = Comparator.comparing(entry -> entry.getValue().isRegularFile());
          Comparator<Entry<Path, BasicFileAttributes>> comparing = compDirs.thenComparing(entry -> entry.getKey().getFileName().toString(), String.CASE_INSENSITIVE_ORDER);
      
          try(var files = find(dir, depth, (p,a) -> true)) {
              files.sorted(comparing).forEach(entry -> System.out.println(entry.getKey() +" modified "+entry.getValue().lastModifiedTime()));
      
              // Alternatively get files+folder Paths:
              // List<Path> contents = files.sorted(comparing).map(Entry::getKey).toList();
          }
      }
      

      可以通过将深度编辑为 Integer.MAX_VALUE 来进行树扫描。

      编辑

      由于您更喜欢处理地图,您可以更改 find 方法以直接返回地图并分别对键进行排序,或者您可以将排序后的条目放入 LinkedHashMap 以使用标准 for 循环进行迭代,例如:

      try(var files = find(dir, depth, (p,a) -> true)) {
          Map<Path, BasicFileAttributes> sorted = files.sorted(comparing)
                  .collect(Collectors.toMap(Entry::getKey, Entry::getValue,
                                            (a,b) -> a, LinkedHashMap::new));
      
          for (var entry : sorted.entrySet()) {
              BasicFileAttributes attr = entry.getValue();
              Path path = entry.getKey();
              System.out.println((attr.isDirectory() ? "DIR  ":"FILE ")+path +" modified "+attr.lastModifiedTime());
          }
      }
      

      【讨论】:

      • 嘿。为什么我们在Files.find 谓词中需要attrs.put(p, a) == null
      • 它确保所有代码都是一个简单的表达式谓词,如果需要路径,则返回 true,并具有记录 Path+BasicFileAttribute 的附加效果。否则谓词必须是一个不太整洁但等效的代码块,如(p,a) -&gt; { boolean b wanted = (matcher == null || matcher.test(p, a)); if(wanted) { attrs.put(p, a); } return wanted;}
      • 这对我来说太复杂了 :) 但无论如何我都在尝试,但是我如何使用基本的 for 循环而不是 for each,因为我在循环内重新分配了一个变量,并且在 lambda 表达式中收到有关 FINAL 变量的错误?我试图在网上找到但所有示例都用于每个示例。
      猜你喜欢
      • 2014-12-08
      • 1970-01-01
      • 2011-11-23
      • 1970-01-01
      • 2013-09-25
      • 2020-07-24
      • 2013-03-18
      • 2021-04-10
      • 1970-01-01
      相关资源
      最近更新 更多