手动链接 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"。好处是你不会得到一张地图的地图等等
我想对一个人的列表进行分组。一个人有一些属性,如姓名、国家、城镇、邮政编码等。我写了静态代码,效果很好:
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"。好处是你不会得到一张地图的地图等等