Reputation: 59
I have a class
static class Student {
private String surname;
private String firstName;
private String secondName;
private int yearOfBirth;
private int course;
private int groupNumber;
private int mathGrade;
private int engGrade;
private int physicGrade;
private int programmingGrade;
private int chemistryGrade;
And there is a method that adds students to the map for the course
public Map<Integer, Double> averageInt(List<Student> students) {
Map<Integer, Double> map2 = students.stream()
.collect(Collectors.groupingBy(Student::getCourse,
Collectors.averagingInt(Student::getEngGrade)));
return map2;
}
However, I need several average values in one map at the same time.
Not only engGrade, but also mathGrade, programmingGrade and so on. I think the code in this case should be in the format Map<Integer, List<Double>>
but I don’t know how to do it. Tell me please
For example, I now display "Course = 1, average eng grade = ..."
And I need to display "Course = 1, average eng grade = ..., average math grade = ..."
, ie so that there are multiple Double values in the map
Upvotes: 5
Views: 324
Reputation: 21975
I see two ways of achieving this
Collectors::teeing
Collectors#teeing
If you're using java-12 or higher, you could use Collectors::teeing
Returns a Collector that is a composite of two downstream collectors.
public static Map<Integer, List<Double>> averageInt(List<Student> students) {
return students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.teeing(
Collectors.teeing(
Collectors.averagingInt(Student::getEngGrade),
Collectors.averagingInt(Student::getMathGrade),
(englishAverage, mathAverage) -> {
List<Double> averages = new ArrayList<>();
averages.add(englishAverage);
averages.add(mathAverage);
return averages;
}
),
Collectors.averagingInt(Student::getPhysicGrade),
(averages, physicsAverage) -> {
averages.add(physicsAverage);
return averages;
}
)
));
}
And it gives the following results
public static void main(String[] args) {
Student studentOne = new Student(1, 5, 1, 1);
Student studentTwo = new Student(1, 1, 9, 2);
Student studentThree = new Student(1, 2, 9, 3);
Student studentFour = new Student(2, 5, 6, 4);
Student studentFive = new Student(2, 8, 1, 5);
Student studentSix = new Student(3, 3, 6, 0);
Student studentSeven = new Student(3, 5, 7, 7);
Student studentEight = new Student(3, 3, 6, 8);
Student studentNine = new Student(3, 4, 1, 9);
Student studentTen = new Student(4, 9, 1, 0);
List<Student> students = List.of(studentOne, studentTwo, studentThree, studentFour, studentFive, studentSix, studentSeven, studentEight, studentNine, studentTen);
System.out.println(averageInt(students));
}
Result
{
1 = [
6.333333333333333,
2.6666666666666665,
2.0
],
2 = [
3.5,
6.5,
4.5
],
3 = [
5.0,
3.75,
6.0
],
4 = [
1.0,
9.0,
0.0
]
}
However, if you prefer using a customer Collector
, here is how to achieve this. I choose to use a Map
instead of a List
here for conveniency, but you can of course use a List
too without changing the essence of this method
public static Map<Integer, Map<GradeType, Double>> averageInt(List<Student> students) {
return students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
new CustomCollector(Map.of(
GradeType.MATH, Student::getMathGrade,
GradeType.ENGLISH, Student::getEngGrade,
GradeType.PHYSICS, Student::getPhysicGrade
))
));
}
private enum GradeType {
MATH, ENGLISH, PHYSICS
}
private static class CustomCollector implements Collector<Student, Map<GradeType, List<Double>>, Map<GradeType, Double>> {
private final Map<GradeType, Function<Student, Integer>> functionsPerGradeType;
public CustomCollector(Map<GradeType, Function<Student, Integer>> functionsPerGradeType) {
this.functionsPerGradeType = functionsPerGradeType;
}
@Override
public Supplier<Map<GradeType, List<Double>>> supplier() {
return HashMap::new;
}
@Override
public BiConsumer<Map<GradeType, List<Double>>, Student> accumulator() {
return (map, student) -> {
for (Map.Entry<GradeType, Function<Student, Integer>> entry : functionsPerGradeType.entrySet()) {
GradeType gradeType = entry.getKey();
Double gradeForStudent = entry.getValue().apply(student).doubleValue();
map.computeIfAbsent(gradeType, gt -> new ArrayList<>());
map.get(gradeType).add(gradeForStudent);
}
};
}
@Override
public BinaryOperator<Map<GradeType, List<Double>>> combiner() {
return (mapOne, mapTwo) -> {
mapOne.forEach((k, v) -> {
mapTwo.merge(k, v, (listOne, listTwo) -> {
listOne.addAll(listTwo);
return listOne;
});
});
return mapTwo;
};
}
@Override
public Function<Map<GradeType, List<Double>>, Map<GradeType, Double>> finisher() {
return map -> {
Map<GradeType, Double> finishedMap = new HashMap<>();
for (var entry : map.entrySet()) {
GradeType gradeType = entry.getKey();
double gradeTypeAverage = entry.getValue().stream().mapToDouble(x -> x).average().orElse(0d);
finishedMap.put(gradeType, gradeTypeAverage);
}
return finishedMap;
};
}
@Override
public Set<Characteristics> characteristics() {
return Set.of(UNORDERED);
}
}
Providing the following result
{1={PHYSICS=2.0, ENGLISH=6.333333333333333, MATH=2.6666666666666665}, 2={PHYSICS=4.5, ENGLISH=3.5, MATH=6.5}, 3={PHYSICS=6.0, ENGLISH=5.0, MATH=3.75}, 4={PHYSICS=0.0, ENGLISH=1.0, MATH=9.0}}
Upvotes: 4
Reputation: 2890
Steps described in the code:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.HashMap;
import java.util.List;
import java.util.function.Supplier;
public class Main {
/**
* Student holder
*/
@Data
@AllArgsConstructor
static class Student {
private int course;
private int mathGrade;
private int engGrade;
private int physicGrade;
}
/**
* Hold average grades and total count of students
*/
@Data
@NoArgsConstructor
static class AverageGrades {
private int count = 0;
private double mathGrade;
private double engGrade;
private double physicGrade;
/**
* Add new math grade
* @param mathGrade Student grade or Average students grade
* @param count Number of students
*/
void addMathGrade(double mathGrade, int count) {
this.mathGrade = (this.mathGrade * this.count + mathGrade * count) / (this.count + count);
}
/**
* Add new eng grade
* @param engGrade Student grade or Average students grade
* @param count Number of students
*/
void addEngGrade(double engGrade, int count) {
this.engGrade = (this.engGrade * this.count + engGrade * count) / (this.count + count);
}
/**
* Add new physic grade
* @param physicGrade Student grade or Average students grade
* @param count Number of students
*/
void addPhysicGrade(double physicGrade, int count) {
this.physicGrade = (this.physicGrade * this.count + physicGrade * count) / (this.count + count);
}
/**
* Combine average grades
* @param grades Other grades
*/
void addGrades(AverageGrades grades) {
if (grades.getCount() > 0) {
addEngGrade(grades.getEngGrade(), grades.getCount());
addMathGrade(grades.getMathGrade(), grades.getCount());
addPhysicGrade(grades.getPhysicGrade(), grades.getCount());
setCount(getCount() + grades.getCount());
}
}
}
/**
* Combine output from multiple threads to left side
* @param left One thread output
* @param right Second thread output
*/
static void gradeCombiner(HashMap<Integer, AverageGrades> left, HashMap<Integer, AverageGrades> right) {
right.forEach((course, rightGrades) -> {
if (left.containsKey(course)) {
left.get(course).addGrades(rightGrades);
} else {
left.put(course, rightGrades);
}
});
}
/**
* Student consumer
* @param map Initialized map
* @param student Next student
*/
static void studentConsumer(HashMap<Integer, AverageGrades> map, Student student) {
if (!map.containsKey(student.getCourse())) {
map.put(student.getCourse(), new AverageGrades());
}
var grades = map.get(student.getCourse());
grades.addEngGrade(student.getEngGrade(), 1);
grades.addMathGrade(student.getMathGrade(), 1);
grades.addPhysicGrade(student.getPhysicGrade(), 1);
grades.setCount(grades.getCount() + 1);
}
static List<Student> mockStudents() {
return List.of(
new Student(1, 2, 3, 4),
new Student(1, 3, 5, 2),
new Student(2, 3, 5, 2),
new Student(3, 3, 5, 2),
new Student(3, 3, 5, 2),
new Student(3, 3, 5, 2),
new Student(3, 3, 5, 8),
new Student(3, 3, 11, 2),
new Student(3, 9, 5, 2)
);
}
public static void main(String[] args) {
var students = mockStudents(); // Mock some students
Supplier<HashMap<Integer, AverageGrades>> supplier = HashMap::new; // Create new map as supplier
var courseGrades = students.stream().collect(// Standard stream
supplier, // Init response
Main::studentConsumer, // Apply every student
Main::gradeCombiner); // Combine in case of multiple outputs, skipped in this case
courseGrades.forEach((course, grades) -> {
var output = String.format("course: %s average grades: %s", course, grades);
System.out.println(output);
});
courseGrades = students.parallelStream().collect(// Parallel stream
supplier, // Init response
Main::studentConsumer, // Apply every student
Main::gradeCombiner); // Combine outputs from different threads
courseGrades.forEach((course, grades) -> {
var output = String.format("course: %s average grades: %s", course, grades);
System.out.println(output);
});
}
}
Output:
// course: 1 average grades: Main.AverageGrades(count=2, mathGrade=2.5, engGrade=4.0, physicGrade=3.0)
// course: 2 average grades: Main.AverageGrades(count=1, mathGrade=3.0, engGrade=5.0, physicGrade=2.0)
// course: 3 average grades: Main.AverageGrades(count=6, mathGrade=4.0, engGrade=6.0, physicGrade=3.0)
// course: 1 average grades: Main.AverageGrades(count=2, mathGrade=2.5, engGrade=4.0, physicGrade=3.0)
// course: 2 average grades: Main.AverageGrades(count=1, mathGrade=3.0, engGrade=5.0, physicGrade=2.0)
// course: 3 average grades: Main.AverageGrades(count=6, mathGrade=4.0, engGrade=6.0, physicGrade=3.0)
Upvotes: 2
Reputation: 19545
An object needs to be implemented to collect the stats per course for each student, it should have a constructor accepting Student
instance to populate relevant fields, and a merge function to calculate total/count:
static class GradeStats {
int count = 1;
int course;
int totalMath;
int totalEng;
int totalPhysic;
GradeStats(Student student) {
this.course = student.getCourse();
this.totalMath = student.getMath();
this.totalEng = student.getEng();
this.totalPhysic = student.getPhysic();
}
GradeStats merge(GradeStats stats) {
count++;
this.totalMath += stats.totalMath;
this.totalEng += stats.totalEng;
this.totalPhysic += stats.totalPhysic;
return this;
}
public String toString() {
return String.format("course: %d, avg.math: %s; avg.eng: %s; avg.physics: %s",
course, (double) totalMath / count, (double) totalEng / count, (double) totalPhysic / count);
}
}
Then the stats can be collected using Collectors.toMap
:
// using simpler Student version with math, eng, and physics grades
List<Student> students = Arrays.asList(
new Student(1, 80, 80, 80), new Student(1, 88, 86, 92), new Student(2, 93, 88, 87));
Map<Integer, GradeStats> statMap = students.stream()
.collect(Collectors.toMap(Student::getCourse, GradeStats::new, GradeStats::merge));
statMap.forEach((k, v) -> System.out.println(k + " -> " + v));
Output
1 -> course: 1, avg.math: 84.0; avg.eng: 83.0; avg.physics: 86.0
2 -> course: 2, avg.math: 93.0; avg.eng: 88.0; avg.physics: 87.0
Upvotes: 3
Reputation: 1128
I propose to use this method
public static Map<Integer, Double> averageInt(List<Student> students, ToIntFunction<? super Student> mapper) {
Map<Integer, Double> map2 = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.averagingInt(mapper)));
return map2;
}
And use it like this
Student.averageInt(students, Student::getMathGrade);
Student.averageInt(students, Student::getProgrammingGrade);
Upvotes: 10
Reputation: 59
You could use a HashMap of Hashmap. The outer hashmap key is the course number and the value is a EnumMap which holds enums of specific course averages you want.
public enum Averages {
MATHAVG, PROGRAMMINGAVG, etc..
}
To retrieve from it would be something like
courseMap.get(courseNumber).get(Averages.MATHAVG);
Upvotes: 2