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

我正在尝试使用 Java 8 Collection-Stream 按多个属性对对象列表进行分组.

I am trying to group a list of objects by mulitple attributes, by using 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 的字符串数组(或列表).

But I would like to make it a little more dynamic. I just want to specify an String-Array (or List) which should be used to GroupBy.

类似:

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

我想打电话:

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

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

How can I make the groupBy-Method more dynamic, like in my example?

推荐答案

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

The main problem with making that code more dynamic is that you don't know in advance how many elements there will be to group by. In such a case, it is best to group by the List of all the elements. This works because two lists are equal if all of their elements are equal and in the same order.

在这种情况下,我们将按由每个数据类型和模块组成的列表来分组,而不是先按类型分组,然后按模块分组.

In this case, instead of grouping by the type and then the module, we will group by the list consisting of each data type and module.

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/lang/invoke/MethodHandle.html">MethodHandle.对于每个字段,为该字段检索 MethodHandle:这是通过从 MethodHandles.lookup() 并使用 <代码>findGetter:

The first part of the code transforms the array of field names into a List of MethodHandle. For each field, a MethodHandle is retrieved for that field: this is done by obtaining a lookup from MethodHandles.lookup() and looking up a handle for the given field name with findGetter:

生成一个方法句柄,授予对非静态字段的读取访问权限.

Produces a method handle giving read access to a non-static field.

其余代码创建分类依据.在数据实例上调用所有句柄以返回 String 值列表.这个Stream被收集到一个List中,作为分类器.

The rest of the code creates the classifier to group by from. All the handles are invoked on the data instance to return the list of String value. This Stream is collected into a List to serve as classifier.

示例代码:

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 时.

when MyClass.toString() is overriden to return the title only.

相关文章