-1

I have a list of Student objects. Each of them contains a map of subjects and points earned as Double.

public class Student {
    public int student_id;
    public String name;
    Map<String, Double> examPoints = new HashMap<String, Double>();

    public Student(int student_id, String name) {
        this.student_id = student_id;
        this.name = name;
    }

    public void setExamPoints(String name, Double points){

        examPoints.put(name, points);
    }
}

I'm trying to calculate the average score of all the students in a specific subject using Java Streams.

main()

Student studentOne = new Student(1, "Ole");
studentOne.setExamPoints("Programmering", 20.0);
studentOne.setExamPoints("Math", 10.0);
studentOne.setExamPoints("Algdat", 25.5);

Student studentTwo = new Student(2, "Åge");
studentTwo.setExamPoints("Fysikk", 12.0);
studentTwo.setExamPoints("Math", 15.0);
studentTwo.setExamPoints("Algdat", 65.5);

Student studentThree = new Student(3, "Per");
studentThree.setExamPoints("Matte", 24.2);
studentThree.setExamPoints("Gym", 45.0);
studentThree.setExamPoints("Algdat", 29.3);

Student studentFour = new Student(4, "Svein");
studentFour.setExamPoints("Programmering", 90.0);
studentFour.setExamPoints("AdvJava", 99.0);
studentFour.setExamPoints("Science", 29.5);

ArrayList<Student> allStudents = new ArrayList<Student>();
allStudents.add(studentOne);
allStudents.add(studentTwo);
allStudents.add(studentThree);
allStudents.add(studentFour);


double algdatAverage = allStudents.stream()
    .mapToDouble(s -> s.examPoints.get("Algdat"))
    .average()
    .getAsDouble();

double mathAverage = allStudents.stream()
    .mapToDouble(s -> s.examPoints.get("Math"))
    .average()
    .getAsDouble();

I'm getting a NullPointerException:

 Exception in thread "main" java.lang.NullPointerException
    at Main.lambda$main$0(Main.java:39)
    at java.base/java.util.stream.ReferencePipeline$6$1.accept(ReferencePipeline.java:246)
    at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1655)
    at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
    at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913)
    at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.base/java.util.stream.DoublePipeline.collect(DoublePipeline.java:514)
    at java.base/java.util.stream.DoublePipeline.average(DoublePipeline.java:467)
    at Main.main(Main.java:39)

I'm using Java 11.

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
catfish
  • 43
  • 3
  • 1
    I would recommend you to debug your application and to use [peek](https://www.baeldung.com/java-streams-peek-api) for sole purpose to debug your streams – Pawan.Java Jul 28 '22 at 12:39
  • 3
    Not everyone of your Student objects has a Map entry for the key `"Algdat"` so s.examPoints.get("Algdat") will return `null` for your 4th student. You can either first filter your stream to only include those that have this key in their ExamPoints map, or you use a map function that returns a default value if the key isn't found instead of the standard get method. – OH GOD SPIDERS Jul 28 '22 at 12:39
  • 3
    `studentFour` has no entry for `"Algdat"`. – f1sh Jul 28 '22 at 12:39
  • oh, so i would first filter all the students to remove the ones that dont have algdat, then i would do ```mapToDouble(s -> s.examPoints.get("Algdat")).average().getAsDouble();``` on the result of that instead, that makes more sense – catfish Jul 28 '22 at 12:43
  • 1
    like @f1sh mentioned. so you are trying to get an average on an array with a null value and getting a NullPointerException. Add "Algdat" to StudentThree or use a condition in .mapToDouble like if(!s.examPoints.get("Algdat")) { return 0;} else {return s.examPoints.get("Algdat")) – Stormtrooper CWR Jul 28 '22 at 12:44
  • 2
    @catfish to me another tricky part in your question is *average score of all the students in a specific subject* here do you need to count other students also who do not have that specific subject? If not then given and is already solid , if yes then you can use this `allStudents.stream().mapToDouble(s -> s.examPoints.getOrDefault("Algdat", 0.0))` – Sayan Bhattacharya Jul 28 '22 at 12:59
  • Please improve your title, this question will never be found by googling – Dexygen Jul 28 '22 at 13:26

2 Answers2

3

Filter first:

double asDouble = allStudents.stream()
.filter(s -> s.examPoints.containsKey("Algdat"))
.mapToDouble(s -> s.examPoints.get("Algdat"))
.average()
.getAsDouble();
pholak
  • 158
  • 1
  • 8
2

Avoid using public fields. And also avoid manipulating with data structures that belong to a particular object outside this object, learn about the Information expert principle.

The map containing exam points has to be encapsulated in the Student object. Instead of accessing its values directly, it would be cleaner to introduce a method which returns points for a particular exam, let's say getExamPoints().

Sidenote: follow the Java Naming Conventions, avoid using names like student_id.

public class Student {
    private int studentId;
    private String name;
    private Map<String, Double> examPoints = new HashMap<>();
    
    public Student(int studentId, String name) {
        this.studentId = studentId;
        this.name = name;
    }
    
    public void setExamPoints(String examName, Double points) {
        examPoints.put(examName, points);
    }
    
    public Double getExamPoints(String examName) {
        return examPoints.get(examName);
    }
}

getExamPoints() will return null if the map examPoints doesn't contain the entry for the given exam. While streaming over a list of students, we can use filter() operation to ensure that there wouldn't be NPE while calculating the average.

And that's how the method that returns the average score for a particular exam might be implemented:

public static double getAverageExamPoints(List<Student> students,
                                          String examName) {
    return students.stream()
        .mapToDouble(student -> student.getExamPoints(examName))
        .filter(Objects::nonNull) 
        .average()
        .orElse(0); // or .orElseThrow() to throw NoSuchElementException if there's no data in the stream
}

Note: getAsDouble() will throw NoSuchElementException if the stream is empty, which is not obvious to reader of the code. If throwing the exception in such a case is a desired behavior - use orElseThrow() instead. See the API note.

API Note:

The preferred alternative to this method is orElseThrow().

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46