Abhi G
Abhi G

Reputation: 11

How to generate a Map based on a List of objects containing a Map as and attribute

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.

Upvotes: 1

Views: 143

Answers (3)

Alexander Ivanchenko
Alexander Ivanchenko

Reputation: 29078

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

Upvotes: 1

Eritrean
Eritrean

Reputation: 16508

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

Upvotes: 1

vince
vince

Reputation: 306

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

Upvotes: 0

Related Questions