使用流和收集器创建嵌套地图的问题

Issue with creating nested map using streams and collectors

class QuizAnswers {
  List<CheckboxAnswer> checkBoxAnswers;
}

class CheckboxAnswer {
  int questionId;
  // The indices of the selected answer choices
  List<Integer> answer_selections;
}

我函数的输入是 List<QuizAnswers>

我想创建映射 <CheckboxAnswer.questionId : <CheckboxAnswer.answer_selection, total count of answer_selection>Map<Integer, Map<Integer, Long>> 的输出。换句话说,我想创建一个嵌套映射,将每个多项选择测验问题映射到一个表示该测验问题每个答案选择的总选择数的映射。

假设输入 List<QuizAnswers> quizAnswersList 为:

[ {questionId: 1, answer_selection: [1,2]},    
  {questionId: 1, answer_selection:[1,2,3,4]},  
  {questionId: 2, answer_selection:[3]},   
  {questionId: 2, answer_selection:[1]} ]

那么我希望输出为:

{1 : {1:2, 2:2, 3:1, 4:1}, 2: {1:1, 3:1}}

因为 Id = 1 的问题在答案选择 211 的答案选择 3 和 [=23 上收到了两个选择=] 而带有 Id=2 的问题在答案选择 13.

上有 1 个选择

我试过了

quizAnswersList.stream()
            .flatMap(
                quizAnswers ->
                    quizAnswers.getCheckboxAnswers().stream())
            .collect(
                answer -> {
                  return Collectors.groupingBy(
                      answer.getQuestionId(),
                      answer.getAnswerSelections().stream()
                          .collect(
                              Collectors.groupingBy(
                                  answerSelection -> answerSelection, 
                                  Collectors.counting())));
                        });

这给我一个错误,第一个 collect() 没有采用正确的参数。

虽然你很接近,但你对收集器的使用在语法上是不正确的。

首先,所需的方法 collect() 需要一个收集器(还有另一种形式的 collect() 需要树“函数” : supplier、accumulator 和 combiner;不要混淆它们),这是正确的语法:

.collect(MyCollector);

现在,关于 groupingBy() 收藏家。我们需要它的版本需要一个 classifier 函数 和一个 下游收集器 :

.collect(Collectors.groupingBy(function, AnotherCollector);

作为下游收集器,我们需要提供 flatMapping()。它期望一个函数应该将每个 CheckboxAnswer 转换为 答案选择值流 以便能够将它们中的每一个映射到它的计数) 和 下游收集器 。反过来,作为 flatMapping() 下游 我们需要再次提供 groupingBy()counting() 作为其 下游收集器 .

收集器的最终结构如下:

.collect(Collectors.groupingBy(function, // <- getting a question Id
    Collectors.flatMapping(function,     // <- obtaining `answer selection values` as a stream of Integer
        Collectors.groupingBy(function,  // <- Function.identity() is used to retain a `answer selection` without changes
            Collectors.counting()        // <- obtaining a count for each `answer selection`
);

现在,让我们把所有的东西放在一起。

public static void main(String[] args) {
    List<QuizAnswers> quizAnswersList =
        List.of(new QuizAnswers(List.of(new CheckboxAnswer(1, List.of(1,2)),
                                        new CheckboxAnswer(1, List.of(1,2,3,4)))),
                new QuizAnswers(List.of(new CheckboxAnswer(2, List.of(3)),
                                        new CheckboxAnswer(2, List.of(1)))));

    Map<Integer, Map<Integer, Long>> answerSelectionToCountById = quizAnswersList.stream()
        .map(QuizAnswers::getCheckBoxAnswers)
        .flatMap(List::stream)
        .collect(Collectors.groupingBy(
            CheckboxAnswer::getQuestionId,
                Collectors.flatMapping(checkboxAnswer -> checkboxAnswer.getAnswerSelections().stream(),
                    Collectors.groupingBy(Function.identity(),
                        Collectors.counting()))));
    
    answerSelectionToCountById.forEach((k, v) -> System.out.println(k + " : " + v));
}

输出

1 : {1=2, 2=2, 3=1, 4=1}
2 : {1=1, 3=1}

分两步得出最终结果可能更容易:按 questionId 分组后,您需要映射到 answer_selections。这可以使用 Collectors.mapping 来完成,这样您最终会得到一个 Map<Integer,List<List<Integer>>> 的中间结果,它看起来像这样:

Map<Integer, List<List<Integer>>> intermediate =
        quizAnswersList.stream()
                .flatMap(quizAnswers -> quizAnswers.getCheckBoxAnswers().stream())
                .collect(Collectors.groupingBy(CheckboxAnswer::getQuestionId,
                                Collectors.mapping(CheckboxAnswer::getAnswer_selections, Collectors.toList())));

System.out.println(intermediate);

这会给你这样的输出:

{1=[[1, 2], [1, 2, 3, 4]], 2=[[3], [1]]}

由于以上内容不是您真正想要的,您需要再执行一步并包装此处完成的映射

Collectors.mapping(CheckboxAnswer::getAnswer_selections, Collectors.toList())

in 到 Collectors.collectingAndThen 将上面地图的列表列表类型的值转换为 Map<Integer,Long> 可以像下面显示的那样完成(包括上面的步骤,这只是有用的解释中间结果,只需要下面的代码):

Map<Integer, Map<Integer, Long>> finalresult =
quizAnswersList.stream()
        .flatMap(quizAnswers -> quizAnswers.getCheckBoxAnswers().stream())
        .collect(Collectors.groupingBy(CheckboxAnswer::getQuestionId,
                Collectors.collectingAndThen(
                        Collectors.mapping(CheckboxAnswer::getAnswer_selections, Collectors.toList()),
                        lists -> lists.stream()
                                .flatMap(List::stream)
                                .collect(Collectors.groupingBy(Function.identity(),Collectors.counting())))));

System.out.println(finalresult);

这会给你想要的结果

{1={1=2, 2=2, 3=1, 4=1}, 2={1=1, 3=1}}