Home > Enterprise >  Issue with creating nested map using streams and collectors
Issue with creating nested map using streams and collectors

Time:05-17

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.

CodePudding user response:

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}

CodePudding user response:

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}}
  • Related