tigh75
tigh75

Reputation: 55

Creating a nested Map using Streams and Collectors

class QuizAnswers {
  List<MultipleChoiceAnswer> multipleChoiceAnswers;
  List<FreeResponseAnswer> freeResponseAnswers; //not relevant to this question
}

class MultipleChoiceAnswer {
  int questionId;
  // The index of the selected multiple choice question
  int answer_selection;
}

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

I want to create an output of Map<Integer, Map<Integer, Long>> that maps <MultipleChoiceAnswer.questionId : <MultipleChoiceAnswer.answer_selection, total count of answer_selection>. In other words, I want to create a nested map that maps each multiple choice 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: 2},    
  {questionId: 1, answer_selection:2},  
  {questionId: 1, answer_selection:3},   
  {questionId: 2, answer_selection:1} ]

Then I would want the output to be:

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

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

I have tried

quizAnswersList.stream()
            .map(
                quizAnswers ->
                    quizAnswers.getMultipleChoiceAnswers().stream()
                        .collect(
                            Collectors.groupingBy(
                                MultipleChoiceAnswer::getQuestionId,
                                Collectors.groupingBy(
                                    MultipleChoiceAnswer::getAnswerSelection,
                                    Collectors.counting()))));

Which is giving me an error. I am not very familiar with streams and collectors in general, so I'd love to learn how to do this correctly.

Upvotes: 2

Views: 1307

Answers (2)

WJS
WJS

Reputation: 40057

I want to create an output of Map<Integer, Map<Integer, Long>> that maps <MultipleChoiceAnswer.questionId : <MultipleChoiceAnswer.answer_selection>, total count of answer_selection>.

You were close. You just didn't flatMap the MultipleChoiceAnswers onto the stream so you had a nested stream and that was causing the problem.

Based on your edited question, here is what I came up with.

List<MultipleChoiceAnswer> mca =
        List.of(new MultipleChoiceAnswer(1, 2),
                new MultipleChoiceAnswer(1, 2),
                new MultipleChoiceAnswer(1, 3),
                new MultipleChoiceAnswer(2, 1));

// more could be added to the List.  You only provided one.
List<QuizAnswers> list = List.of(new QuizAnswers(mca));
  • flatMap all the MultipleChoice lists
  • group them by the questionId
  • then subgroup them according to AnswerSelection and get a count
  • then you get the Map output you requested.
Map<Integer,Map<Integer,Long>> map = list.stream()
        .flatMap(s -> s.getMultipleChoiceAnswers().stream())
        .collect(Collectors.groupingBy(
                MultipleChoiceAnswer::getQuestionId,
                Collectors.groupingBy(
                        MultipleChoiceAnswer::getAnswerSelection,
                        Collectors.counting())));

map.entrySet().forEach(System.out::println);

prints

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

Questions

  • how do you want to handle multiple QuizAnswer instances?
  • how do you want to handle multiple MulitpleChoiceAnswer lists. You only provided one of each.

They could all be flatmapped together and processed as above. But I think there could be some differences in the Answers (perhaps for different tests) which you don't want grouped and counted as the same.

Example

If I add the following to the List<QuizAnswers>

List<MultipleChoiceAnswer> mca2 =
List.of(new MultipleChoiceAnswer(1, 2),
        new MultipleChoiceAnswer(1, 2),
        new MultipleChoiceAnswer(5, 2),
        new MultipleChoiceAnswer(5, 2));

And process using the above solution, the output would be

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

Upvotes: 1

Alexander Ivanchenko
Alexander Ivanchenko

Reputation: 29048

input to my function is a List<QuizAnswers>. I want to create an output of Map<Integer, Map<Integer, Long>>

I have tried quizAnswersList.stream().map(quizAnswers -> ... ) which is giving me an error.

Method map() is an intermediate operation, i.e. it yields a stream. Therefore, if you try to assign the stream-statement you've listed to a variable of type Map you'll get a compilation error.

A stream pipeline needs to end with a terminal operation like collect in order to be executed and produce a result.

And before collect() you have to apply flatMap(), which expects a stream as an argument, to transform the stream of QuizAnswers in to a stream of MultipleChoiceAnswer.

Your usage of collectors is correct and doesn't require any changes.

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

    Map<Integer, Map<Integer, Long>> totalCountOfAnswerSelectionByQuestion =
        quizAnswersList.stream()
            .flatMap(quizAnswers -> quizAnswers.getMultipleChoiceAnswers().stream())
            .collect(Collectors.groupingBy(MultipleChoiceAnswer::getQuestionId,
                        Collectors.groupingBy(MultipleChoiceAnswer::getAnswerSelection,
                            Collectors.counting())));

    System.out.println(totalCountOfAnswerSelectionByQuestion);
}

Output

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

Upvotes: 1

Related Questions