【问题标题】:How to neglect objects during stream grouping or partitioning?如何在流分组或分区期间忽略对象?
【发布时间】:2022-02-02 14:37:54
【问题描述】:

Collectors.groupingByCollectors.partitioningBy期间有可能忽略一些元素?

当然,我知道我可以将.filter() 放在流中的任何位置。但是我的问题是我必须运行一个相当复杂和昂贵的评估,来决定我的对象应该被划分到哪个“组”中。

此外,在收集过程中,我总是想忽略很多物品。

示例:想象一个List<Foo>,我想将其拆分为 2 个列表。这很容易,但我怎么能另外忽略所有不符合我评估条件的对象呢?

var map = foos.stream().collect(Collectors.groupingBy(
    foo -> {
        int bar = complexEvaluation(foo);
        if (bar > 1000) return true;
        if (bar < 0) return false;
        //TODO how to neglect the rest between 0-1000
    },
    Collectors.mapping(foo -> foo.id, Collectors.toSet())
));

【问题讨论】:

  • 如果您不想使用filter(),您可能需要遍历这些值并应用另一组if 语句。
  • 如果我理解正确,您不能使用filter() 进行过滤,因为您在分组时需要所述过滤器的逻辑。对吗?
  • @JettoMartínez 但这是正确的,因为如果我想在之前应用 filter(),我必须运行 complexEvaluation 两次。一次用于过滤器忽略 0-1000 的所有内容,一次用于分组/分区。并且该操作太昂贵而无法运行两次(包含数据库调用)。

标签: java java-stream collectors


【解决方案1】:

只需使用enum 来定义您的 3 个案例:

enum Categories {
    HIGH, LOW, NEGATIVE
}

var map = foos.stream().collect(Collectors.groupingBy(
    foo -> {
        int bar = complexEvaluation(foo);
        if (bar > 1000) return HIGH;
        if (bar < 0) return NEGATIVE;
        return LOW;
    },
    Collectors.mapping(foo -> foo.id, Collectors.toSet())
));

如果不需要,请忽略或删除LOW。它还有一个额外的好处,那就是赋予你的类别更多的意义,而不是仅仅将它们命名为true/false,并且如果你将来需要更多的类别,可以更容易地重构。

唯一的缺点是它构建了一个无用的LOW 集合,但只有与其他集合和complexEvaluation() 操作相比它真的很大时才会出现问题。

【讨论】:

    【解决方案2】:

    如果要避免临时存储,则必须实现自己的收集器:

    var map = foos.parallelStream().collect(
        () -> Map.of(true, new HashSet<ID>(), false, new HashSet<ID>()),
        (o, foo) -> {
            int bar = complexEvaluation(foo);
            ID id = foo.id;
            if (bar > 1000) o.get(true).add(id);
            else if (bar < 0) o.get(false).add(id);
        },
        (a, b) -> { a.get(true).addAll(b.get(true)); a.get(false).addAll(b.get(false)); }
    );
    

    此示例与partitioningBy 具有相同的行为,始终为truefalse 创建条目。

    IDfoo.id 类型的占位符,您没有在问题中包含它。

    【讨论】:

      【解决方案3】:

      要在filtergroupingBy 中重用complexEvaluation 的结果,您可以在过滤之前调用并将结果存储在包装类中。

      foos.stream()
          .map(foo -> {
              int bar = complexEvaluation(foo);
              if (bar > 1000) Pair.of(foo, true);
              if (bar < 0) Pair.of(foo, false);
              return Pair.of(foo, null);
          )
          .filter(fooPair -> fooPair.getRight() != null)
          .collect(Collectors.groupingBy(
              Pair::getRight(),
              Collectors.mapping(fooPair -> fooPair.getLeft().id, Collectors.toSet()
          );
      

      但是,这只是在您出于某种原因坚持使用groupingBy 的情况下。

      使用 foreach 的替代方案会更容易阅读:

      Map<Boolean, Set<Foo>> groups = new HashMap<>();
      foos.stream()
          .forEach(foo -> {
              int bar = complexEvaluation(foo);
              if (bar > 1000) groups.computeIfAbsent(true, k->new HashSet<>()).add(foo);
              if (bar < 0)    groups.computeIfAbsent(false, k->new HashSet<>()).add(foo);
         })
          
      

      【讨论】:

      • 你能给出一个使用forEach的优化例子吗?
      • 添加了forEach的示例
      • 是的,我明白了。这就是我开始的方式,但认为将流内部的值收集到流外部的集合中并不好。但可能更适合我的情况。
      • 但是,您也可以使用forEach(foo -&gt; …) 代替.stream() .forEach(foo -&gt; …),或者只使用普通的for(var foo: foos) …,并且不必担心从流内部访问流外部的工件。
      【解决方案4】:

      如果我正确理解您的意图,您希望在收集某些值时过滤掉它们。 Collectors.filtering() 是可行的。

      请注意,filtering() 可以消除某些桶的所有值,但不会导致空桶被删除。在下面的示例中,1、2、3、4、5 的存储桶将为空。

          public static void main(String[] args) {
              var foos = List.of(-100, 1, 2, 3, 4, 5, 1001, 1002, 1003);
              
              var map = foos.stream()
                      .collect(Collectors.groupingBy(
                                  UnaryOperator.identity(),
                                  Collectors.filtering(foo -> foo < 0 || foo > 1000,
                                          Collectors.toSet())));
      
              System.out.println(map);
          }
      

      地图

      {1=[], 2=[], 3=[], -100=[-100], 4=[], 5=[], 1001=[1001], 1002=[1002], 1003=[1003]}
      

      更新

      我修改了这个问题,下面提供的解决方案预先计算了这些值。

      在这个版本中,流中不需要的条目被过滤掉,这使得 Collector 中的代码更易于阅读。我希望使用整数而不是 foo 对象不是问题。

          public static void main(String[] args) {
              List<Integer> foos = List.of(-100, 1, 2, 3, 4, 5, 101, 102, 103);
      
              Map<Integer, Integer> fooToValue = getFooToValueMap(foos);
      
              Map<Boolean, Set<Integer>> map = getFoosMap(fooToValue);
      
              System.out.println(map);
          }
      
          private static Map<Boolean, Set<Integer>> getFoosMap(Map<Integer, Integer> fooToValue) {
              return fooToValue.entrySet().stream()
                      .filter(entry -> entry.getValue() < 0 || entry.getValue() > 1000)
                      .collect(Collectors.partitioningBy(
                                  entry -> entry.getValue() > 1000,
                                  Collectors.mapping(Map.Entry::getKey, Collectors.toSet())
                      ));
          }
      
          private static Map<Integer, Integer> getFooToValueMap(List<Integer> list) {
              return list.stream()
                      .collect(Collectors.toMap(UnaryOperator.identity(), foo -> complexEvaluation(foo)));
          }
      
          private static int complexEvaluation(int foo) {
              return (int) Math.signum(foo) * foo * foo;
          }
      

      地图

      {false=[-100], true=[101, 102, 103]}
      

      【讨论】:

      • 这将是一个选项如果可以在将foo.id 收集到最后一组之前提取它。
      • 这仅在对象上容易获得要过滤的值时才有效(无需计算),在这种情况下,您最好在管道中直接使用filter()
      • 是的,这就是问题所在:必须计算值...
      • @membersound 那么也许可以预先计算这些值?将每个值与对应的foo 对象关联,然后创建映射条目流。
      • @membersound 我已经更新了我的评论,以便解决您指出的问题。
      猜你喜欢
      • 1970-01-01
      • 2019-03-11
      • 1970-01-01
      • 2011-10-22
      • 1970-01-01
      • 2021-02-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多