SEN1001 · Week 14 · Case Study

Case study: a step-by-step gradebook (GradeBook)

We combine all the pieces of the semester in one program: the Student class (array field, average, letter grade), the GradeBook class (ArrayList, top student, passing count, search), a printf report and a star histogram. A rehearsal for the final writing questions.

case studyproblem decompositionclass designdefensive copyArrayList of objectsaggregate methodstatic helper methodreport formattinghistogramnull checkincremental development

There is no new topic in the last week. We combine everything you learned during the semester in a single realistic program: the gradebook of a course. The program is small, but it contains a class, a constructor, a private field, an array field, a static helper method, an ArrayList, toString, printf, nested loops and a null check. The long writing question on the final exam looks exactly like this: “Write this class, then write this class that uses it.”

Strategy: do not try to write the program in one go. Write a small piece, try it in main, see that it works, then grow it (incremental development).

Step 0: Break the problem down

The request: “Keep a course’s students and their grades. Compute each student’s average and letter grade. Find the class average, the most successful student and the number of passing students. Print a report and a letter grade histogram.”

Nouns are candidates for classes and fields, verbs are candidates for methods:

ClassFieldsMethods
Studentname, grade arrayaverage, highest grade, letter grade, toString
GradeBookcourse name, student listadd student, class average, top student, count passing, search, report, histogram
+-------------------------------------+       +----------------------------------------+
|              Student                |       |               GradeBook                |
+-------------------------------------+       +----------------------------------------+
| - created : int  (static)           |       | - courseName : String                  |
| - name : String  (final)            |  0..* | - students : ArrayList<Student>        |
| - scores : int[] (final)            |<------|                                        |
+-------------------------------------+       +----------------------------------------+
| + Student(name : String,            |       | + GradeBook(courseName : String)       |
|           scores : int[])           |       | + addStudent(s : Student) : void       |
| + getName() : String                |       | + classAverage() : double              |
| + setScore(i : int, v : int) : void |       | + topStudent() : Student               |
| + average() : double                |       | + countPassing(limit : double) : int   |
| + highest() : int                   |       | + find(name : String) : Student        |
| + letterFor(avg : double) : String  |       | + printReport() : void                 |
|   (static)                          |       | + printHistogram() : void              |
| + letter() : String                 |       +----------------------------------------+
| + toString() : String               |
+-------------------------------------+

GradeBook has a list of students (composition). The arrow shows that GradeBook uses Students.

Step 1: The Student class

class Student {
    private static int created = 0;          // how many students were created
    private final String name;
    private final int[] scores;

    public Student(String name, int[] scores) {
        this.name = name;
        this.scores = new int[scores.length];            // its own array
        for (int i = 0; i < scores.length; i++) this.scores[i] = scores[i];
        created++;
    }

    public String getName() { return name; }
    public static int getCreated() { return created; }

    public void setScore(int index, int value) {
        if (index >= 0 && index < scores.length && value >= 0 && value <= 100) {
            scores[index] = value;
        }
    }

    public double average() {
        int sum = 0;
        for (int sc : scores) sum += sc;
        return (double) sum / scores.length;             // no int division trap
    }

    public int highest() {
        int max = scores[0];
        for (int sc : scores) if (sc > max) max = sc;
        return max;
    }

    public static String letterFor(double avg) {        // no object needed
        if (avg >= 90) return "A";
        else if (avg >= 80) return "B";
        else if (avg >= 70) return "C";
        else if (avg >= 60) return "D";
        else return "F";
    }

    public String letter() { return letterFor(average()); }

    @Override
    public String toString() {
        return String.format("%-6s %6.2f %s", name, average(), letter());
    }
}

Design decisions:

  • The fields are private final. The references to the name and the grade array are assigned once. (The elements of a final array can still change; setScore does this in a controlled way.)
  • The constructor copies the array. If we had written this.scores = scores;, we would share the same array with the outside, and every change made outside would change the student’s grades (the constructor version of the getter trap in week 13). This is called a defensive copy.
  • setScore checks two things: that the index is valid (otherwise ArrayIndexOutOfBoundsException) and that the grade is between 0 and 100.
  • letterFor is static. You do not need a student object to compute a letter; a number is enough. letter(), on the other hand, is an instance method and passes its own average to letterFor.
  • toString uses a printf format. %-6s left-aligns the name in a field of 6 characters, %6.2f writes the average right-aligned with 2 decimals. This way the report comes out aligned.

Test it:

Student s = new Student("Deniz", new int[]{70, 85, 90});
System.out.println(s.getName() + " " + s.average() + " " + s.highest() + " " + s.letter());
System.out.println(s);
Deniz 81.66666666666667 90 B
Deniz   81.67 B

new int[]{70, 85, 90} is the way to create an array and pass it directly as an argument without declaring an array variable.

Step 2: The GradeBook class

class GradeBook {
    private String courseName;
    private ArrayList<Student> students = new ArrayList<>();

    public GradeBook(String courseName) { this.courseName = courseName; }

    public void addStudent(Student s) { students.add(s); }
    public int size() { return students.size(); }

    public double classAverage() {
        if (students.isEmpty()) return 0.0;
        double total = 0;
        for (Student s : students) total += s.average();
        return total / students.size();
    }

    public Student topStudent() {
        if (students.isEmpty()) return null;
        Student top = students.get(0);
        for (Student s : students) {
            if (s.average() > top.average()) top = s;
        }
        return top;
    }

    public int countPassing(double limit) {
        int count = 0;
        for (Student s : students) if (s.average() >= limit) count++;
        return count;
    }

    public Student find(String name) {
        for (Student s : students) {
            if (s.getName().equals(name)) return s;
        }
        return null;
    }
}

Notice: GradeBook never touches the grade array anywhere. It asks the student itself for the average (s.average()). Each class is responsible for its own data; this is what we called encapsulation.

Test it:

GradeBook book = new GradeBook("SEN1001");
book.addStudent(new Student("Deniz", new int[]{70, 85, 90}));
book.addStudent(new Student("Ece", new int[]{95, 88, 92}));
book.addStudent(new Student("Can", new int[]{40, 55, 49}));
book.addStudent(new Student("Mert", new int[]{60, 72, 66}));
System.out.println(book.size());
System.out.printf("%.2f%n", book.classAverage());
System.out.println(book.topStudent().getName());
System.out.println(book.countPassing(60));
System.out.println(book.find("Ece").letter() + " " + book.find("Zeynep"));
4
71.83
Ece
3
A null

For Zeynep, who could not be found, find returned null; in String concatenation it appeared as the text null. If you had written book.find("Zeynep").letter() you would get a NullPointerException.

Step 3: Report and histogram

public void printReport() {
    System.out.println("Course: " + courseName);
    for (Student s : students) System.out.println(s);          // toString automatically
    System.out.printf("Class average: %.2f%n", classAverage());
    System.out.println("Top: " + topStudent().getName());
}

public void printHistogram() {
    String[] labels = {"A", "B", "C", "D", "F"};
    for (String lab : labels) {                                // outer: letters
        System.out.print(lab + ": ");
        for (Student s : students) {                           // inner: students
            if (s.letter().equals(lab)) System.out.print("*");
        }
        System.out.println();
    }
}
Course: SEN1001
Deniz   81.67 B
Ece     91.67 A
Can     48.00 F
Mert    66.00 D
Class average: 71.83
Top: Ece
A: *
B: *
C: 
D: *
F: *

PITFALL · using a method that may return null blindly. On an empty gradebook, printReport printed Course: BOS and Class average: 0.00 and then crashed with a NullPointerException on the line topStudent().getName(). Take a result that may be null into a variable first and check it: Student top = topStudent(); if (top != null) System.out.println("Top: " + top.getName());.

Where is each topic of the semester in this program?

Topic (week)Where in the program
printf, String.format (2)toString and the report alignment
if / else if chain (3)letterFor
Loops, nested loops (4)average, highest, histogram
static method (5)letterFor, getCreated
Arrays, copying (6-7)the scores field, the defensive copy
ArrayList (7)the students list
Class, private, get/set (10)both classes in full, the validation in setScore
Constructor, UML, list of objects (11)the constructors, the diagram, ArrayList<Student>
this, static field, final, toString (12)this.name, created, final fields, toString
Composition, null, references (13)GradeBook has-a Student, find/topStudent null

On a final question, any row of this table can be asked on its own. If you can put them all together, you have reached the goal of the course.

Common mistakes

Wrong ideaThe truth
Write the whole program first, then run itWrite class by class, test in main at each step
this.scores = scores; in the constructor is enoughThe outside array is shared; copy it
Average is sum / scores.length(double) sum / scores.length
letterFor must be an instance methodIt depends only on a number; static is enough
GradeBook accesses the grade array directlyIt asks the student: s.average()
students.get(0) on an empty list is no problemIndexOutOfBoundsException; isEmpty() first
find always returns a studentnull if not found; the caller must check
println inside toStringReturn a String with return
%6.2f is 6 decimal placesTotal width 6, 2 decimals

Test yourself

Exam-style questions. Solve them on paper first, then reveal the answer.

Question 1medium

Using the Student class of this page, what is printed?

Student s = new Student("A", new int[]{50, 60, 80});
System.out.println(s.average() + " " + s.letter());
Show answer
63.333333333333336 D

The sum is 190, (double) 190 / 3 = 63.333... the double is printed in full (the 6 in the last digit comes from floating-point rounding). 63.3 ≥ 60 but < 70 → D.

Question 2easy

What is printed?

Student s = new Student("B", new int[]{89, 90, 91});
System.out.println(s.letter() + " " + s.highest());
Show answer
A 91

The average is exactly 90.0, >= 90 → A. The highest grade is 91.

Question 3hard

The Student constructor copies the array it receives into its own new array. What is printed?

int[] marks = {10, 20, 30};
Student s = new Student("C", marks);
marks[0] = 100;
System.out.println(s.highest());
Show answer
30

The constructor opened its own array with this.scores = new int[...] and copied the elements one by one (defensive copy). Changing marks afterwards does not affect the student's grades. If it had not copied (this.scores = scores;), the answer would be 100.

Question 4medium

What is printed?

GradeBook g = new GradeBook("X");
g.addStudent(new Student("P", new int[]{100}));
g.addStudent(new Student("Q", new int[]{0, 100}));
System.out.println(g.classAverage() + " " + g.countPassing(50));
Show answer
75.0 2

P's average is 100.0, Q's is 50.0. The class average is the average of the students' averages: (100 + 50) / 2 = 75.0. Because the passing limit is >= 50, Q passes too: 2.

Question 5medium

What is printed?

System.out.println(Student.letterFor(59.99) + Student.letterFor(60)
        + Student.letterFor(89.5) + Student.letterFor(90));
Show answer
FDBA

letterFor is static, called with the class name. 59.99 < 60 → F. 60 → D (the boundary is included). 89.5 < 90 → B; there is no rounding. 90 → A. Four Strings are concatenated.

Question 6medium

setScore(index, value) changes a score only if the index is valid and the value is between 0 and 100. What is printed?

Student s = new Student("D", new int[]{80, 80});
s.setScore(1, 100);
s.setScore(5, 100);
s.setScore(0, -10);
System.out.println(s.average());
Show answer
90.0

The first call is valid: {80, 100}. Index 5 does not exist, so it is rejected (no exception is thrown either, because the check comes first). -10 is an invalid value. The average is (80 + 100) / 2 = 90.0.

Question 7easy

Two students have the same name. What is printed?

GradeBook h = new GradeBook("Y");
h.addStudent(new Student("Ali", new int[]{70}));
h.addStudent(new Student("Ali", new int[]{90}));
System.out.println(h.find("Ali").average());
Show answer
70.0

find walks the list from the start and returns at the first match. The second Ali is never looked at.

Question 8medium

What do classAverage() and topStudent() return for a GradeBook with no students?

GradeBook empty = new GradeBook("BOS");
System.out.println(empty.classAverage() + " " + empty.topStudent());
Show answer
0.0 null

Both methods do an isEmpty() check first. Without the check, classAverage would be 0.0 / 0 = NaN, and topStudent would throw IndexOutOfBoundsException with students.get(0).

Question 9hard

What happens when printReport() is called on an empty GradeBook?

Show answer
Prints the course line and Class average: 0.00, then throws NullPointerException

printReport calls topStudent().getName(). On an empty gradebook topStudent returns null, and calling getName on null crashes. In verification Course: BOS and Class average: 0.00 were printed first, then NullPointerException. The fix: Student top = topStudent(); if (top != null) ....

Question 10medium

For the four students of this page (Deniz B, Ece A, Can F, Mert D), what does printHistogram() print?

Show answer
A: *
B: *
C:
D: *
F: *

The outer loop walks the letters, the inner loop walks the students. For each letter, one star per student who got that letter. Nobody got C; the line has only C: . A typical use of nested loops.

Question 11easy

Write the countPassing(double limit) method of GradeBook (field ArrayList<Student> students).

Show answer
for (Student s : students) if (s.average() >= limit) count++; return count;

The counter pattern, this time over a list of objects. Each student's own method computes the average.public int countPassing(double limit) { int count = 0; for (Student s : students) { if (s.average() >= limit) count++; } return count;}For the four students on the page, countPassing(60) → 3.

Question 12medium

Write the topStudent() method of GradeBook: return the student with the highest average, or null if the list is empty.

Show answer
if (students.isEmpty()) return null; Student top = students.get(0); ... if (s.average() > top.average()) top = s; return top;

The object version of the max pattern, plus an empty list check.public Student topStudent() { if (students.isEmpty()) return null; Student top = students.get(0); for (Student s : students) { if (s.average() > top.average()) top = s; } return top;}For the four students on the page it returns Ece (91.67).

Verification: The whole program was run with JDK 26 (verify/SEN1001/W14.java). The behavior of printReport on an empty gradebook was tried separately with verify/SEN1001/W14b.java. The spaces in the report and histogram output were checked with cat -A.

Review card

Design first: which classes? the fields and methods of each class? (draw with UML) Student: private final String name; private final int[] scores; COPY the array in the constructor (defensive copy) → changes outside do not leak in average: (double) sum / scores.length highest: max = scores[0] letterFor(double) static helper: >=90 A, >=80 B, >=70 C, >=60 D, else F toString: String.format("%-6s %6.2f %s", name, average(), letter()) GradeBook: private ArrayList<Student> students = new ArrayList<>(); Aggregate methods: classAverage, topStudent, countPassing, find → all for (Student s : students) Empty list: isEmpty() check; topStudent/find return null if not found The caller of a method that may return null: check if (x != null), otherwise NullPointerException Histogram: outer loop categories, inner loop students, * on a match Develop step by step: write one class → test it in main → then move on to the next