如何将List<P>中的元素分组为Map<K,List<V>>并保留顺序?

18

我有一个由 Google Places API 获取的 Google PlaceSummary 对象列表。我想通过它们的 Google Place ID 进行收集和分组,但同时保留元素的顺序。我认为可行的方法是:

Map<String, List<PlaceSummary>> placesGroupedByPlaceId =
            places.stream()
                  .collect(Collectors.groupingBy(
                          PlaceSummary::getPlaceId,
                          LinkedHashMap::new,
                          Collectors.mapping(PlaceSummary::getPlaceId, toList())
                  ));

但是它甚至无法编译。根据Java API文档中的Collectors,它看起来应该可以。

之前我有这段代码:

    Map<String, List<PlaceSummary>> placesGroupedByPlaceId = places.stream()
            .collect(Collectors.groupingBy(PlaceSummary::getPlaceId));

然而,Streams API上的标准.collect()不会保留后续HashMap中元素的顺序(显然因为HashMap是无序的)。我希望输出结果是一个LinkedHashMap,这样Map就按每个bucket插入的顺序排序。

然而,我建议的解决方案无法编译。首先,它无法识别PlaceSummary::getPlaceId,因为它说它不是一个函数 - 即使我知道它是。其次,它说我不能将LinkedHashMap<Object,Object>转换为M。 M应该是泛型集合,所以应该被接受。

如何使用Java Stream API将列表转换为LinkedHashMap?是否有简洁的方法可以做到这一点?如果太难理解,我可能只能采用旧学校的Java 8之前的方法。

我注意到有另一个Stack Overflow回答关于将List转换为LinkedHashMap,但这没有我想要的解决方案,因为我需要收集“this”我正在迭代的对象。


从 https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#groupingBy-java.util.function.Function-java.util.function.Supplier-java.util.stream.Collector- 给出的示例来看,它应该可以编译通过。除非您已经静态导入了它,否则可能需要使用 Collectors.toList() 而不是 toList() - njzk2
@njzk2 我认为 toList 不是问题 - 就像你说的那样,它是静态导入的。在 IntelliJ 中,LinkedHashMap::new 行存在以下错误: 方法引用中的返回类型错误:无法将 java.util.LinkedHashMap<java.lang.Object, java.lang.Object> 转换为 M。 - James Murphy
1
映射收集器适用于值,因此您希望p -> p而不是PlaceSummary :: getPlaceId。 - Alexis C.
@AlexisC。那么这个意思是p -> p.getPlaceId吗?那么...这是否意味着我可以只使用p -> p来引用“this”元素? - James Murphy
1
不使用p -> p.getPlaceId,意味着PlaceSummary分组值将按其地点ID进行映射。如果您想要实例本身,只需应用恒等函数即可,如p -> p。 - Alexis C.
在groupingBy中,PlaceSummary :: getPlaceId很奇怪,它显然返回一个String,但您想要一个PlaceSummary - 编辑,没有看到答案。 - njzk2
4个回答

23

你已经非常接近你想要的东西了:

Map<String, List<PlaceSummary>> placesGroupedByPlaceId =
            places.stream()
                  .collect(Collectors.groupingBy(
                          PlaceSummary::getPlaceId,
                          LinkedHashMap::new,
                          Collectors.mapping(Function.identity(), Collectors.toList())
                  ));

Collectors.mapping方法中,您需要提供PlaceSummary实例而不是地点ID。在上面的代码中,我使用了Function.identity():这个收集器用于构建值,因此我们需要累积地点本身(而不是它们的ID)。
请注意,可以直接编写Collectors.toList(),而不是Collectors.mapping(Function.identity(), Collectors.toList())
到目前为止,您拥有的代码无法编译,因为实际上它正在创建一个Map<String,List<String>>:您正在为每个ID累积ID(这相当奇怪)。
您可以将其编写为通用方法:
private static <K, V> Map<K, List<V>> groupByOrdered(List<V> list, Function<V, K> keyFunction) {
    return list.stream()
                .collect(Collectors.groupingBy(
                    keyFunction,
                    LinkedHashMap::new,
                    Collectors.toList()
                ));
}

然后像这样使用:

Map<String, List<PlaceSummary>> placesGroupedById = groupByOrdered(places, PlaceSummary::getPlaceId);

在生产代码中试用了一下 - 运行得很好,谢谢。显然是对 API 的误解。 - James Murphy
此外,我更喜欢使用placeSummary -> placeSummary代替Function.identity(),因为它似乎更清晰一些。同时澄清一下。如果您理解代码的目的,它并不太奇怪。我的尝试是将重复的Id分组,然后使用规则过滤掉重复项。其中一个规则是基于它是否是增强地点(我们增强地点以提供Google API不知道的其他信息)之一来保留重复项。 - James Murphy

4
我认为你对最终的收集器有些困惑了。它仅代表每个映射值中需要包含什么。不需要有第二个“mapping”收集器,因为你只想要一个原始对象列表。
    Map<String, List<PlaceSummary>> placesGroupedByPlaceId =
          places.stream()
                .collect(Collectors.groupingBy(PlaceSummary::getPlaceId,
                                               LinkedHashMap::new,
                                               Collectors.toList()));

是的,我认为那有点让我困惑。老实说,如果参数顺序是键,值映射,然后是 toList(),那会更好一些 - 但只是小问题。将来会记住这个...谢谢。 - James Murphy

0
/**
 * I have written this code more generic, if you want then you can group based on any * 
 * instance variable , id, name etc via passing method reference.
**/

class Student {
    private int id;
    private String name;
    public Student(int id, String name) {this.id = id;this.name = name;}
    /**
     * @return the id
     */
    public int getId() {return id;}
    /**
     * @param id
     *            the id to set
     */
    public void setId(int id) {this.id = id;}
    /**
     * @return the name
     */
    public String getName() {return name;}
    /**
     * @param name
     *            the name to set
     */
    public void setName(String name) {this.name = name;}
}

public class StudentMain {

    public static void main(String[] args) {

        List<Student> list = new ArrayList<>();
        list.add(new Student(1, "Amit"));
        list.add(new Student(2, "Sumit"));
        list.add(new Student(1, "Ram"));
        list.add(new Student(2, "Shyam"));
        list.add(new Student(3, "Amit"));
        list.add(new Student(4, "Pankaj"));

        Map<?, List<Student>> studentById = groupByStudentId(list,
                Student::getId);
        System.out.println(studentById);

       Map<?, List<Student>> studentByName = groupByStudentId(list,
                Student::getName);
        System.out.println(studentByName);

    }

    private static <K, V> Map<?, List<V>> groupByStudentId(List<V> list,
            Function<V, K> keyFunction) {
        return list.stream().collect(
                Collectors.groupingBy(keyFunction, HashMap::new,
                        Collectors.toList()));
    }
}

0
如果您需要在维护顺序的同时进行分组并应用函数(归约),也许可以使用类似以下方式进行计数。
final Map<Integer,Long>map=stream.collect(Collectors.groupingBy(function
   ,LinkedHashMap::new
   ,Collectors.collectingAndThen(Collectors.counting(),Function.identity()))
 )

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接