【问题标题】:Java 8 Stream filtering and grouping by same expensive method callJava 8 Stream 通过相同的昂贵方法调用进行过滤和分组
【发布时间】:2016-10-16 07:18:27
【问题描述】:

我正在寻找一种以干净的方式优化Stream 处理的方法。

我有类似的东西:

try (Stream<Path> stream = Files.list(targetDir)) {
    Map<String, List<Path>> targetDirFilteredAndMapped = stream.parallel()                                                                                                
        .filter(path -> sd.containsKey(md5(path)))                                                                                                                    
        .collect(Collectors.groupingBy(path -> md5(path)));
} catch (IOException ioe) { // manage exception }

由于md5 函数非常昂贵,我想知道是否有一种方法可以在每个文件中只调用一次。

有什么建议吗?

【问题讨论】:

  • 元组会很好,如果 java 有的话。 (和自动拆包,让它看起来不错)

标签: java java-8 java-stream


【解决方案1】:

您可以创建一些PathWrapper 对象,其中包含Path 实例及其对应的md5(path)。

public class PathWrapper
{
    Path path;
    String md5; // not sure if it's a String
    public PathWrapper(Path path) {
        this.path = path;
        this.md5 = md5(path);
    }
    public Path getPath() {return path;}
    public String getMD5() {return md5;}
}

然后将您的流映射到Stream&lt;PathWrapper&gt;:

try (Stream<Path> stream = Files.list(targetDir)) {
    Map<String, List<Path>> targetDirFilteredAndMapped =
        stream.parallel() 
              .map(PathWrapper::new)
              .filter(path -> sd.containsKey(path.getMD5()))                                                                                                                    
              .collect(Collectors.groupingBy(PathWrapper::getMD5,
                                             Collectors.mapping(PathWrapper::getPath,
                                                                Collectors.toList())));
} catch (IOException ioe) { /* manage exception */ }

【讨论】:

  • 你甚至可以使用AbstractMap.SimpleImmutableEntry 代替自己的类
  • @ArneBurmeister 好主意,我不知道那门课。尽管使用该类名的方法引用会变得很长:)
  • 如果我们不想创建专用的包装类,请使用javafx.util.Pair作为包装类
  • @NicolasFilotto 如果不在使用 JavaFX 的富客户端内,我不会这样做,因为在迁移到 JavaFX 成为 RT 库的可选部分的 Java 9 时会遇到问题。
【解决方案2】:

如果md5 操作确实在性能上占主导地位,您可以考虑在此处停止过滤,然后删除不匹配的组:

try(Stream<Path> stream = Files.list(targetDir)) {
    Map<String, List<Path>> targetDirFilteredAndMapped = stream.parallel()
        .collect(Collectors.groupingBy(p -> md5(p), HashMap::new, Collectors.toList()));
    targetDirFilteredAndMapped.keySet().retainAll(sd.keySet());
} catch (IOException ioe) { 
    // manage exception
}

当然,这暂时需要更多内存。如果这是一个问题,使用更复杂的解决方案(如其他答案所示)是不可避免的。

【讨论】:

    【解决方案3】:

    创建专用类的另一种方法是直接使用collect 方法,您将负责在累加器中进行md5 计算,并且组合器将负责合并条目。

    try (Stream<Path> stream = Files.list(targetDir)) {
        Map<String, List<Path>> targetDirFilteredAndMapped =
            stream.parallel()
                  .collect(HashMap::new,
                           (m, p) -> {
                               String res = md5(p);
                               if(sd.containsKey(res)) {
                                   m.computeIfAbsent(res, k -> new ArrayList<>()).add(p);
                               }
                            },
                            (m1, m2) -> m2.forEach((k, v) -> m1.computeIfAbsent(k, k2 -> new ArrayList<>()).addAll(v)));
    } catch (IOException ioe) { 
        // manage exception
    }
    

    正如@Holger 指出的那样,您可以通过避免使用更好的合并功能创建新列表来优化这一点:

    (m1, m2) -> m2.forEach((k,v) -> m1.merge(k, v, (l1,l2) -> { l1.addAll(l2); return l1; })) 
    

    【讨论】:

    • 不确定HashMap::new 将如何处理并行流...也许可以保证合并是单线程的?
    • @GPI 是供应商函数,所以每个线程都会从自己的空映射开始进行工作。
    • merge函数浪费资源,一直使用addAll,当第一个map中没有list的时候构造一个新的list。更好的函数是(m1, m2) -&gt; m2.forEach((k,v) -&gt; m1.merge(k, v, (l1,l2)-&gt;{ l1.addAll(l2);return l1; } ))(这基本上是内置的groupingBy 收集器使用的)。
    • @Holger 真的!一如既往地感谢您的宝贵意见;)
    【解决方案4】:

    我在这种情况下使用元组。

    public static void main(String [] args) {
        Map<String, String> sd = Maps.newHashMap();
        Stream<Path> stream = Stream.empty();
        Map<String, List<Path>> targetDirFilteredAndMapped = stream.parallel()
            .map(path -> Tuple.tuple(path, md5(path)))
            .filter(tuple -> sd.containsKey(tuple.right()))
            .collect(groupingBy(Tuple::right,
                     mapping(Tuple::left,
                     toList())));
    }
    
    private static String md5(final Path path) {
            return "md5";
    }
    

    不幸的是,java 中没有元组(比如 scala 中的 ()),所以我创建了这样的类:

    @ToString
    @EqualsAndHashCode
    public class Tuple<L, R> {
        public static <L, R> Tuple<L, R> tuple(L left, R right) {
            return new Tuple<>(left, right);
        }
    
        private final L left;
        private final R right;
    
        private Tuple(L left, R right) {
            this.left = left;
            this.right = right;
        }
    
        public L left() {
            return left;
        }
    
        public R right() {
            return right;
        }
    }
    

    您还可以创建某种私有类来存储 Path 和 md5,但元组使用起来更快。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-11-09
      • 1970-01-01
      • 2017-04-13
      • 1970-01-01
      • 2020-12-23
      • 1970-01-01
      • 2017-04-13
      • 2020-06-20
      相关资源
      最近更新 更多