简体   繁体   中英

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

The input to my function is a List<QuizAnswers> .

I want to create an output of Map<Integer, Map<Integer, Long>> that maps <CheckboxAnswer.questionId : <CheckboxAnswer.answer_selection, total count of answer_selection> . In other words, I want to create a nested map that maps each multiple selection quiz question to a map representing the total number of selections on each answer choice of that quiz question.

Suppose the input List<QuizAnswers> quizAnswersList as:

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

Then I would want the output to be:

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

Because the question with Id = 1 received two selections on answer choice 2 and 1 and 1 selection on answer choice 3 and 4 while the question with Id=2 had 1 selection on answer choice 1 and 3 .

I have tried

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

Which is giving me an error that the first collect() is not taking the right arguments.

Although you were close, your usage of collectors is syntactically incorrect.

Firstly, the required method collect() expects a collector ( there's also another flavor of collect() that expect tree "functions": supplier, accumulator and combiner; don't confuse them ), that's the correct syntax:

.collect(MyCollector);

Now, regarding the groupingBy() collector. We need its version that expects a classifier function and a downstream collector :

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

And as a downstream collector we need to provide flatMapping() . Which expects a function that should transform each CheckboxAnswer into a stream of answer selection values ( in order to be able to map each of them to its count ), and a downstream collector . In turn as the downstream of flatMapping() again we need to provide groupingBy() and counting() as its downstream collector .

The final structure of collectors will be the following:

.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`
);

Now, let's put all the things together.

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

Output

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

It is may be easier to get to the final result in two steps: After grouping by your questionId you need to map to your answer_selections . This can be done using Collectors.mapping so that you end up with an intermediate result of Map<Integer,List<List<Integer>>> which could look like something like:

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

This will give you an output like:

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

Since the above is not what you realy wanted you need to do one more step and wrap the mapping which is done here

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

in to Collectors.collectingAndThen to turn the values of above map which are of type list of lists to a Map<Integer,Long> which can be done like showed below (including the above step, which only was useful to explain the intermediate result, only the code below is needed):

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

which will give you the desired result

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM