【问题标题】:How can I combine the results from a Collectors.groupingBy如何组合来自 Collectors.groupingBy 的结果
【发布时间】:2021-03-01 22:49:50
【问题描述】:

我正在玩 Java 反射并了解有关 Stream.collect 的更多信息。

我有一个注释 MyTag,它有两个属性(idtype enum[Normal|Failure])。 另外,我有一个带有 MyTag 的注释方法列表,我可以使用 Collectors.groupingBy 通过 MyTag 注释的 id 属性对这些方法进行分组:

List<Method> ml = getMethodsAnnotatedWith(anClass.getClass(),
                                           MyTag.class);
Map<String, List<Method>> map = ml.stream().collect(groupingBy(m -> {
      var ann = m.getDeclaredAnnotation(MyTag.class);
      return ann.anId();
    }, TreeMap::new, toList()));

现在我需要将生成的列表减少为一个对象,该对象仅由两个具有相同 MyTag.id 的项目组成,一个具有 MyTag.type=Normal,另一个具有 MyTag.type=Failure。所以它会产生类似 Map> 的东西。如果出现两次以上,我必须只选择第一个,记录并忽略其余的。

我怎样才能做到这一点?

【问题讨论】:

    标签: java java-stream


    【解决方案1】:

    你可以使用

    Map<String, Map<Type, Method>> map = Arrays.stream(anClass.getClass().getMethods())
        .filter(m -> m.isAnnotationPresent(MyTag.class))
        .collect(groupingBy(m -> m.getDeclaredAnnotation(MyTag.class).anId(),
                TreeMap::new,
                toMap(m -> m.getDeclaredAnnotation(MyTag.class).aType(),
                      m -> m, (first, last) -> first,
                      () -> new EnumMap<>(Type.class))));
    

    结果将注解 ID 属性映射到 Map,从 Type(枚举常量 NORMALFAILURE)映射到第一个遇到的带有匹配注解的方法。虽然“first”在迭代 Reflection 发现的方法时没有实际意义,因为它不保证任何特定的顺序。

    () -&gt; new EnumMap&lt;&gt;(Type.class) 映射工厂不是必需的,当您不指定工厂时,它也可以与默认使用的通用映射一起使用。但是EnumMap 将以稍微更有效的方式处理您只有两个常量要映射的情况,并且它的迭代顺序将匹配枚举常量的声明顺序。

    我认为,EnumMapPair&lt;Method, Method&gt; 更好,后者需要记住哪个方法与“正常”相关联,哪个与“失败”相关联。适应两个以上的常数也更容易。此外,EnumMap 是内置的,不需要 3rd 方库。

    【讨论】:

    • 谢谢你,霍尔格。非常有趣和灵活的方法。在我的测试中,我能够做类似你的事情,但使用内部groupingBy。但它使用了枚举文本。我不知道EnumMap。如果我想减少到一个单一的元素,海关收集器会是要走的路吗?
    • @Cristiano 你的意思是“一个单一的元素”而不是内部地图吗?如果值的普通 Reduction 适合您的任务,您可以使用合并函数将外部 groupingBy 替换为 toMap。否则,groupingBy 使用不同的收集器,无论是内置的还是自定义的,都是正确的选择。与this Q&A比较。
    【解决方案2】:

    以下示例可以很容易地适应您的代码:

    import java.util.Arrays;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.TreeMap;
    import java.util.stream.Collectors;
    
    import org.apache.commons.lang3.tuple.Pair;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class Test {
    
        private static final Logger logger = LoggerFactory.getLogger(Test.class);
    
        public static void main(String[] args) {
            List<Pair<String, String>> ml = Arrays.asList(
                    Pair.of("key1", "value1"),
                    Pair.of("key1", "value1"),
                    Pair.of("key1", "value2"),
                    Pair.of("key2", "value1"),
                    Pair.of("key2", "value3"));
    
            Map<String, Pair<String, String>> map = ml.stream().collect(
                    Collectors.groupingBy(m -> {
                        return m.getKey();
                    }, TreeMap::new, Collectors.toList()))
                    .entrySet()
                    .stream()
                    .collect(Collectors.toMap(
                            Map.Entry::getKey, e -> convert(e.getValue())));
    
            System.out.println(map.values());
        }
    
        private static Pair<String, String> convert(List<Pair<String, String>> original) {
            long count1 = original.stream().filter(e -> Objects.equals(e.getValue(), "value1")).count();
            long count2 = original.stream().filter(e -> Objects.equals(e.getValue(), "value2")).count();
            if (count1 > 1) {
                logger.warn("More than one occurrence of value1");
            }
            if (count2 > 1) {
                logger.warn("More than one occurrence of value2");
            }
            return Pair.of(count1 > 0 ? "value1" : null,
                    count2 > 0 ? "value2" : null);
        }
    
    }
    
    • 使用方法代替 Pair
    • m.getDeclaredAnnotation(MyTag.class).anId()对应pair.getKey()

    以下结果打印到控制台:

    01:23:27.959 [main] WARN syglass.Test2 - More than one occurrence of value1
    [(value1,value2), (value1,null)]
    

    【讨论】:

      【解决方案3】:

      首先,创建自己的MethodPair 类:

      class MethodPair {
        private final Method failure;
        private final Method normal;
      
        public MethodPair(Method failure, Method normal) {
          this.failure = failure;
          this.normal = normal;
        }
      
        public Method getFailure() {
          return failure;
        }
      
        public Method getNormal() {
          return normal;
        }
      
        public MethodPair combinedWith(MethodPair other) {
          return new MethodPair(
              this.failure == null ? other.failure : this.failure,
              this.normal == null ? other.normal : this.normal)
          );
        }
      }
      

      注意combinedWith 方法。这对我们将要做的减少很有用。

      使用reducing 收集器代替toList

      Map<String, MethodPair> map = ml.stream().collect(groupingBy(m -> {
        var ann = m.getDeclaredAnnotation(MyTag.class);
        return ann.anId();
      }, TreeMap::new,
          Collectors.reducing(new MethodPair(null, null), method -> {
            var type = method.getDeclaredAnnotation(MyTag.class).type();
            if (type == Type.NORMAL) {
              return new MethodPair(null, method);
            } else {
              return new MethodPair(method, null);
            }
          }, MethodPair::combinedWith)
          ));
      

      如果您可以分两步执行此操作,我建议您先创建Map&lt;String, List&lt;Method&gt;&gt;,然后将其值映射到新地图。 IMO 这更具可读性:

      Map<String, List<Method>> map = ml.stream().collect(groupingBy(m -> {
        var ann = m.getDeclaredAnnotation(MyTag.class);
        return ann.anId();
      }, TreeMap::new, toList()));
      var result = map.entrySet().stream().collect(Collectors.toMap(entry -> entry.getKey(), entry -> {
        Method normal = null;
        Method failure = null;
        for (var m : entry.getValue()) {
          var type = m.getDeclaredAnnotation(MyTag.class).type();
          if (type == Type.NORMAL && normal == null) {
            normal = m;
          } else if (type == Type.FAILURE && failure == null) {
            failure = m;
          }
          if (normal != null && failure != null) {
            break;
          }
        }
        return new MethodPair(failure, normal);
      }));
      

      【讨论】:

      • 非常好的解决方案,谢谢。我正在测试它,但有一个问题:每次组合器与 Identity 进行比较时,由于 MethodPair::combineWith 空检查,都会引发 NullPointerException。
      • @Cristiano 现在应该修复了。
      • @Swepper,我能够创建一个在groupingBy 之后使用的收集器实现,而不是使用reducing。这使我可以只更改可变累加器类中的方法值(当为 null 时),而不是每次都创建一个新对象。它也可以并行正常工作。 @Holger 的答案无疑是大多数情况下最灵活的答案,但我选择你的答案是因为它促使我​​了解我需要什么。谢谢。
      猜你喜欢
      • 2023-03-05
      • 1970-01-01
      • 2016-07-19
      • 2018-08-15
      • 1970-01-01
      • 1970-01-01
      • 2018-02-26
      • 2018-11-24
      • 1970-01-01
      相关资源
      最近更新 更多