Home > Software design >  How to generate a Map based on a List of objects containing a Map as and attribute
How to generate a Map based on a List of objects containing a Map as and attribute

Time:05-24

I'm getting familiar with the Stream API and facing some problems.

Here is my code.

class Student {
    String name;
    int height;
    LocalDate dob;
    Map<String, Integer> scores;
    
    public Student(String name, int height, LocalDate dob, Map<String, Integer> scores) {
        this.name = name;
        this.height = height;
        this.dob = dob;
        this.scores = scores;
    }

// getters, setters and toString ...
}


public static void main(String [] args) {
        
        LocalDate dob1 = LocalDate.of(2000, 1, 1);
        LocalDate dob2 = LocalDate.of(2001, 2, 10);
        LocalDate dob3 = LocalDate.of(2001, 3, 15);
        LocalDate dob4 = LocalDate.of(2002, 4, 18);
        LocalDate dob5 = LocalDate.of(2002, 5, 19);

        Map<String, Integer> scores1 = new HashMap<String, Integer>();
        scores1.put("Math", 100);
        scores1.put("Physics", 95);
        scores1.put("Chemistry", 90);
        scores1.put("Biology", 100);
        
        Map<String, Integer> scores2 = new HashMap<String, Integer>();
        scores2.put("Math", 90);
        scores2.put("Physics", 55);
        scores2.put("Chemistry", 95);
        scores2.put("Biology", 85);
        
        Map<String, Integer> scores3 = new HashMap<String, Integer>();
        scores3.put("Math", 85);
        scores3.put("Physics", 50);
        scores3.put("Chemistry", 100);
        scores3.put("Biology", 75);
        
        Map<String, Integer> scores4 = new HashMap<String, Integer>();
        scores4.put("Math", 50);
        scores4.put("Physics", 45);
        scores4.put("Chemistry", 88);
        scores4.put("Biology", 40);
        
        Map<String, Integer> scores5 = new HashMap<String, Integer>();
        scores5.put("Math", 65);
        scores5.put("Physics", 100);
        scores5.put("Chemistry", 88);
        scores5.put("Biology", 55);
        
        Student s1 = new Student("Tom", 6, dob1, scores1);
        Student s2 = new Student("Dan", 7, dob2, scores2);
        Student s3 = new Student("Ron", 5, dob3, scores3);
        Student s4 = new Student("Pete", 5, dob4, scores4);
        Student s5 = new Student("Sam", 6, dob5, scores5);
        
        List<Student> students = new ArrayList<Student>();
        students.add(s1);
        students.add(s2);
        students.add(s3);
        students.add(s4);
        students.add(s5);
}

From the above List<Student> I am trying to generate and print a map Map<String, <Map<String, Integer>> containing students with the highest score in each subject in the format <subject_name, <student_name, Score>>

So far I was able to come up with a solution shown below.

public static void printStudentWithHighestScoreInEachSubject(List<Student> S) {

        HashMap<String, Map<String, Integer>> map = new HashMap<String, Map<String, Integer>>();
        List<String> sub = S.get(0).getScores().keySet().stream().collect(Collectors.toList());

        for (String subj : sub) {
            HashMap<String, Integer> t = new HashMap<String,Integer>();
            
            for (Student s: S) {
                Integer score = s.getScores().get(subj);
                t.put(s.getName(), score);
            }
            map.put(subj, t);
        }
        
        HashMap<String, Map.Entry<String, Integer>> res = (HashMap<String, Map.Entry<String, Integer>>) map.entrySet().stream().collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue().entrySet().stream().max((x1, x2) -> x1.getValue().compareTo(x2.getValue())).get()));
        System.out.println(res);
}

I am trying to come up with a solution that does not uses the for loops and handles everything using the Stream API.

CodePudding user response:

Stream over your students list, flatmaping subjects and map to a simple entry with subject as key and student as value, collect to map using subject as key and a binaryoperator to get the student with the max score for the key subject, wrap this collector in a Collectors.collectingAndThen to build your final result:

public static void printStudentWithHighestScoreInEachSubject(List<Student> students) {
    Map<String, Map<String, Integer>> result =

    students.stream()
            .flatMap(student -> student.getScores().keySet().stream()
                                       .map(subject -> new SimpleEntry<>(subject, student)))
            .collect(
                Collectors.collectingAndThen(
                        Collectors.toMap(Entry::getKey, Function.identity(),
                                         BinaryOperator.maxBy(Comparator.comparing(e -> e.getValue().getScores().get(e.getKey())))),
                        map -> map.entrySet().stream()
                                .collect(Collectors.toMap(Entry::getKey,
                                        e -> Map.of(e.getValue().getValue().getName(), e.getValue().getValue().getScores().get(e.getKey()))))
                ));

    System.out.println(result);
}

CodePudding user response:

That doable using a flavor of collect() that expects three arguments (suplier, accumulator and combiner) and Java 8 method Map.merge():

public static void printStudentWithHighestScoreInEachSubject(List<Student> students) {

    Map<String, Map.Entry<String, Integer>> bestStudentsScoreBySubject = students.stream()
        .collect(HashMap::new,
            (Map<String, Map.Entry<String, Integer>> map, Student student) ->
                student.getScores().forEach((k, v) -> map.merge(k, Map.entry(student.getName(), v),
                    (oldV, newV) -> oldV.getValue() < v ? newV : oldV)),
            (left, right) ->  right.forEach((k, v) -> left.merge(k, v,
                    (oldV, newV) -> oldV.getValue() < newV.getValue() ? newV : oldV)));
    
    bestStudentsScoreBySubject.forEach((k, v) -> System.out.println(k   " : "   v));
}

In place of identical lambda expressions which you can see in the accumulator and combiner we can introduce a local variable of type BiFunction inside the method and refer to it by its name, or we make use of the static method BinaryOperator.maxBy() which is self-explanatory and more readable than a lambda containing a ternary operator.

Method maxBy() expects a comparator, which can be defined using Java 8 static method Map.Entry.comparingByValue():

BinaryOperator.maxBy(Map.Entry.comparingByValue())

With that the code above might be written as follows:

public static void printStudentWithHighestScoreInEachSubject(List<Student> students) {
    
    Map<String, Map.Entry<String, Integer>> bestStudentsScoreBySubject = students.stream()
        .collect(HashMap::new,
            (Map<String, Map.Entry<String, Integer>> map, Student student) ->
                student.getScores().forEach((k, v) -> map.merge(k, Map.entry(student.getName(), v),
                    BinaryOperator.maxBy(Map.Entry.comparingByValue()))),
            (left, right) ->  right.forEach((k, v) -> left.merge(k, v,
                    BinaryOperator.maxBy(Map.Entry.comparingByValue()))));
    
    bestStudentsScoreBySubject.forEach((k, v) -> System.out.println(k   " : "   v));
}

Output (for the data-sample provided in the question):

Chemistry : Ron=100
Biology : Tom=100
Math : Tom=100
Physics : Sam=100

You can play around with this Online demo.

Sidenotes:

  • Adhere to the Java naming conventions, avoid names like S - it's not very descriptive, and parameter names should start with a lower-case letter.
  • You might think about introducing a utility class that will provide convenient access to a list of subjects instead of extracting it from the attributes of a random student.
  • It might make sense to define a class (record) responsible for storing the data-sets like subject student's name score and subject score. It will allow you to manipulate the data effectively, by reducing the code complexity and making it more meaningful (assuming that these records and attributes will have meaningful names).

CodePudding user response:

// Group by subject then score, get the sorted TreeMap
Map<String, TreeMap<Integer, List<String>>> map = students.stream()
    .flatMap(stu -> stu.getScores().entrySet().stream().map(entry -> entry.getKey()   "-"   entry.getValue()   "-"   stu.getName()))
    .collect(Collectors.groupingBy(info -> info.split("-")[0], Collectors.groupingBy(info -> Integer.valueOf(info.split("-")[1]), TreeMap::new, Collectors.toList())));

//Map score names to name score pairs
Map<String, Map<String, Integer>> result = map.entrySet().stream().collect(Collectors.toMap(
    Map.Entry::getKey,
    entry -> {
        Map.Entry<Integer, List<String>> scoreNames = entry.getValue().lastEntry();
        return scoreNames.getValue().stream().collect(Collectors.toMap(info -> info.split("-")[2], name -> scoreNames.getKey()));
    }
));
  • Related