手动链接 GroupBy 收集器

Manually chain GroupBy collectors

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

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

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

您可以创建一个函数,采用任意数量的属性进行分组,并在循环中构造 groupingBy-Collector,每次将其自身的先前版本作为 downstream收藏家。

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);
}

注意 grouper 函数的顺序是颠倒的,所以你必须以相反的顺序传递它们(或者在函数内部颠倒它们,例如使用 Collections.reverse 或 Guava 的 Lists.reverse,随你喜欢)。

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

或者像这样,使用 old-school 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);
}

这两种方法都将 return 变成 Map<?,Map<?,Map<?,T>>>,就像在您的原始代码中一样。根据您要对该数据执行的操作,也可能值得考虑使用 Map<List<?>,T>,如 Tunaki 所建议的那样。

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

假设您想按姓名和国家/地区分组。那么你可以使用:

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

之所以可行,是因为当两个列表以相同顺序包含相同元素时,它们被视为相等。因此,在生成的地图中,您将为每个不同的名称/国家/地区对提供一个键,并将具有这些特定名称和国家/地区的人员列表作为值。换句话说,不是说 "group by name, then group by country",而是说 "group by name and country"。好处是你不会得到一张地图的地图等等