使用Collection.stream按特定属性进行动态分组

时间:2016-01-13 15:11:25

标签: java collections java-8 grouping java-stream

我试图通过使用Java 8 Collection-Stream按多个属性对对象列表进行分组。

这非常有效:

public class MyClass
{
   public String title;
   public String type;
   public String module;
   public MyClass(String title, String type, String module)
   {
      this.type = type;
      this.title = title;
      this.module= module;
   }
}

List<MyClass> data = new ArrayList();
data.add(new MyClass("1","A","B"));
data.add(new MyClass("2","A","B"));
data.add(new MyClass("3","A","C"));
data.add(new MyClass("4","B","A"));

Object result = data.stream().collect(Collectors.groupingBy((MyClass m) 
-> m.type, Collectors.groupingBy((MyClass m) -> m.module)));

但我想让它更有活力。 我只想指定一个应该用于GroupBy的String-Array(或List)。

类似的东西:

Object groupListBy(List data, String[] groupByFieldNames)
{
    //magic code
}

我想打电话:

groupListBy(data, new String[]{"type","module"});

如何使groupBy-Method更具动态性,就像我的例子一样?

2 个答案:

答案 0 :(得分:12)

使代码更具动态性的主要问题是,您事先并不知道要分组的元素数量。在这种情况下,最好按所有元素的List进行分组。这是有效的,因为如果所有元素的所有元素相同且顺序相同,则两个列表相等。

在这种情况下,我们将按照由每种数据类型和模块组成的列表进行分组,而不是按类型和模块进行分组。

private static Map<List<String>, List<MyClass>> groupListBy(List<MyClass> data, String[] groupByFieldNames) {
    final MethodHandles.Lookup lookup = MethodHandles.lookup();
    List<MethodHandle> handles = 
        Arrays.stream(groupByFieldNames)
              .map(field -> {
                  try {
                      return lookup.findGetter(MyClass.class, field, String.class);
                  } catch (Exception e) {
                      throw new RuntimeException(e);
                  }
              }).collect(toList());
    return data.stream().collect(groupingBy(
            d -> handles.stream()
                        .map(handle -> {
                            try {
                                return (String) handle.invokeExact(d);
                            } catch (Throwable e) {
                                throw new RuntimeException(e);
                            }
                        }).collect(toList())
        ));
}

代码的第一部分将字段名称数组转换为List MethodHandle。对于每个字段,为该字段检索MethodHandle:这是通过从MethodHandles.lookup()获取查找并使用findGetter查找给定字段名称的句柄来完成的:

  

生成一个方法句柄,提供对非静态字段的读访问权。

其余代码创建分类器来分组。在数据实例上调用所有句柄以返回String值列表。此Stream被收集到List中以充当分类器。

示例代码:

public static void main(String[] args) {
    List<MyClass> data = new ArrayList<>();
    data.add(new MyClass("1", "A", "B"));
    data.add(new MyClass("2", "A", "B"));
    data.add(new MyClass("3", "A", "C"));
    data.add(new MyClass("4", "B", "A"));

    System.out.println(groupListBy(data, new String[] { "type", "module" }));
}

输出:

{[B, A]=[4], [A, B]=[1, 2], [A, C]=[3]}

当覆盖MyClass.toString()时仅返回title

答案 1 :(得分:5)

您还可以考虑提供一个功能列表(必须包含一个)来代替名称列表来对元素进行分组。

这些函数应将MyClass的元素映射到对象,以便您可以使用Function<MyClass, ?>

private static Map<List<Object>, List<MyClass>> groupListBy(List<MyClass> data, Function<MyClass, ?> mandatory, Function<MyClass, ?>... others) {
   return data.stream()
              .collect(groupingBy(cl -> Stream.concat(Stream.of(mandatory), Stream.of(others)).map(f -> f.apply(cl)).collect(toList())));
}

一些电话示例:

groupListBy(data, m -> m.type); //group only by type
groupListBy(data, m -> m.type, m -> m.module); //group by type and by module

当然,您可以将此方法设为通用,以便返回Map<List<Object>, List<U>>,其函数类型为U -> Object