简体   繁体   中英

Java stream averagingInt by multiple parameters

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

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

I see two ways of achieving this


Collectors#teeing

If you're using 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
    ]
}

Using a customer collector

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

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

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

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)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM