手动链组按收集器

2022-09-02 21:49:34

我想对人员列表进行分组。一个人有一些属性,如姓名,国家,城镇,邮政编码等。我写了静态代码,效果很好:

Object groupedData = data.stream().collect(groupingBy(Person::getName, Collectors.groupingBy(Person::getCountry, Collectors.groupingBy(Person::getTown))));

但问题是,它不是动态的。有时我只想按名称和城镇分组,有时按属性分组。我该怎么做?非Java 8解决方案也受到欢迎。


答案 1

您可以创建一个函数,该函数采用任意数量的属性进行分组,并在循环中构造 - ,每次都将以前版本的自身作为收集器传递。groupingByCollectordownstream

public static <T> Map collectMany(List<T> data, Function<T, ?>... groupers) {
    Iterator<Function<T, ?>> iter = Arrays.asList(groupers).iterator();
    Collector collector = Collectors.groupingBy(iter.next());
    while (iter.hasNext()) {
        collector = Collectors.groupingBy(iter.next(), collector);
    }
    return (Map) data.stream().collect(collector);
}

请注意,函数的顺序是相反的,因此您必须以相反的顺序传递它们(或在函数内部反转它们,例如使用 或 Guava's ,无论您喜欢哪种)。grouperCollections.reverseLists.reverse

Object groupedData = collectMany(data, Person::getTown, Person::getCountry, Person::getName);

或者像这样,使用老式循环来反转函数中的数组,即您不必以反序传递石斑鱼(但恕我直言,这更难理解):for

public static <T> Map collectMany(List<T> data, Function<T, ?>... groupers) {
    Collector collector = Collectors.groupingBy(groupers[groupers.length-1]);
    for (int i = groupers.length - 2; i >= 0; i--) {
        collector = Collectors.groupingBy(groupers[i], collector);
    }
    return (Map) data.stream().collect(collector);
}

这两种方法都将返回 ,就像在原始代码中一样。根据您要对这些数据执行的操作,可能还值得考虑使用 Tunaki 建议的 。Map<?,Map<?,Map<?,T>>>Map<List<?>,T>


答案 2

您可以按由要分组依据的属性组成的列表进行分组。

假设您要按名称和国家/地区进行分组。然后,您可以使用:

Map<List<Object>, List<Person>> groupedData = 
    data.stream().collect(groupingBy(p -> Arrays.asList(p.getName(), p.getCountry())));

这是可行的,因为当两个列表以相同的顺序包含相同的元素时,它们被认为是相等的。因此,在生成的地图中,您将为每个不同的名称/国家/地区对提供一个键,并将具有这些特定名称和国家/地区的人员列表作为值。换句话说,它不是说“按名称分组,然后按国家分组”,而是有效地说“按名称和国家分组”。优点是你不会最终得到地图的地图,等等。