DEV_ANI
DEV_ANI

Reputation: 31

Get maximum of average subject marks using java 8 stream

I have class student as below

 class Student{
        Map<String,Integer> subjectMarks;
        String name;
        
        
        public Student(Map<String,Integer> subject, String name) {
            super();
            this.subjectMarks = subject;
            this.name = name;
        }
        public Map<String,Integer> getSubjectMarks() {
            return subjectMarks;
        }
        public void setSubjectMarks(Map<String,Integer> subject) {
            this.subjectMarks = subject;
        }
        
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
     }

In main method we add student object in arraylist as follow.

        ArrayList<Student> arr = new ArrayList<Student>();
        Map m1 = new HashedMap();
        m1.put("Maths",40);
        m1.put("Science",50);
        Map m2 = new HashedMap();
        m2.put("Maths",60);
        m2.put("Science",20);
        arr.add(new Student(m1, "RAJ"));
        arr.add(new Student(m2, "AMIT"));

can some help/guide me to find average marks of subject for each student and then get maximum from averge. I want help to write this snippet in java8

Upvotes: 2

Views: 4045

Answers (3)

Felix Seifert
Felix Seifert

Reputation: 602

Do not limit yourself to the idea of streams in Java 8 where you have to process a stream result directly into the next stream and so on... The efficiency might not be the best but consider to nest loops.

Start to think what you have: several marks for every Student. You want to find the average of these marks for every Student. You can reduce the problem to first think on how to get the average for one Student.

double average = student.getSubjectMarks().values().stream()
        .mapToInt(Integer::valueOf).average().orElse(0.0);

Even though your examples show only integer numbers, averages can also be floating point numbers.

Then, you would have to loop through all students and execute the procedure above for every student.

Map<String, Double> studentAverages = new HashMap<>();

arr.forEach(student -> {
    double average = student.getSubjectMarks().values().stream()
            .mapToInt(Integer::valueOf).average().orElse(0.0);
    studentAverages.put(student.getName(), average);
});

In the described implementation, the required averages are saved in the Map studentAverages which has the names of the students as a key and the average mark as a value.

You can then simply get the maximum integer from your list.

studentAverages.values().stream().mapToDouble(Double::doubleValue).max();

Some answers provide more sophisticated usages of streams. However, the code above is more readable. Furthermore, the data type Object is very general, difficult for further usage and error prone.

Upvotes: 1

WJS
WJS

Reputation: 40062

There is nothing wrong with using streams to solve this as you requested.

  • stream the list of maps.
  • put the name of the student and the average in an object array.
    • stream the values of the map and use the summaryStatistics to get the average.
  • Then use maxBy with a comparator to get the maximum average.
  • Then display it.
Object[] result = arr.stream().map(s -> new Object[] {
        s.getName(),
        s.getSubjectMarks().values().stream()
                .mapToInt(Integer::valueOf)
                .summaryStatistics().getAverage() })
        .max(Comparator.comparing(obj -> (double) obj[1]))
        .get();

System.out.println(Arrays.toString(result));

Prints

[RAJ, 45.0]

        

Upvotes: 0

Rangi Keen
Rangi Keen

Reputation: 945

As @Felix noted in his answer, since you have a nested collection it is difficult to process in a single stream. You can use one stream to calculate the average for each student and another to calculate the max of the averages.

Start with a separate function to calculate the average for a student:

private OptionalDouble calculateAverageMarks(Student student) {
    return student.getSubjectMarks().values().stream()
        .mapToInt(Integer::intValue)
        .average();
}

Note that you could add .orElse(0.0) (or some other value) to the pipeline if you wanted to return a double, but that wouldn't allow you to later distinguish between a student with all 0's and one not enrolled in any subjects.

Then you can collect the averages into a map.

Map<String, OptionalDouble> averageMarks = arr.stream()
    .collect(Collectors.toMap(Student::getName, this::calculateAverageMarks));

Note that using Student::getName in the collector will throw if two students share the same name. You could use Function.identity() instead to ensure that each key will be distinct as long as you don't override equals in Student.

If you want, you can then remove students with no subjects

averageMarks.values().removeIf(v -> !v.isPresent());

In Java 11, you can use OptionalDouble::isEmpty instead of the lambda.

Then you can map the values to a double stream and get the max

OptionalDouble max = averageMarks.values().stream()
    .filter(OptionalDouble::isPresent) // In case you didn't remove the empty marks
    .mapToDouble(OptionalDouble::getAsDouble)
    .max();

Upvotes: 0

Related Questions