Reputation: 11
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
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:
S
- it's not very descriptive, and parameter names should start with a lower-case letter.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
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
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