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.
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:
| Class | Fields | Methods |
|---|---|---|
Student | name, grade array | average, highest grade, letter grade, toString |
GradeBook | course name, student list | add 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.
%-6sleft-aligns the name in a field of 6 characters,%6.2fwrites 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: BOSandClass average: 0.00and then crashed with a NullPointerException on the linetopStudent().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 idea | The truth |
|---|---|
| Write the whole program first, then run it | Write class by class, test in main at each step |
this.scores = scores; in the constructor is enough | The outside array is shared; copy it |
Average is sum / scores.length | (double) sum / scores.length |
| letterFor must be an instance method | It depends only on a number; static is enough |
| GradeBook accesses the grade array directly | It asks the student: s.average() |
students.get(0) on an empty list is no problem | IndexOutOfBoundsException; isEmpty() first |
| find always returns a student | null if not found; the caller must check |
| println inside toString | Return a String with return |
%6.2f is 6 decimal places | Total width 6, 2 decimals |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
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
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.
What is printed?
Student s = new Student("B", new int[]{89, 90, 91});
System.out.println(s.letter() + " " + s.highest());Show answer
The average is exactly 90.0, >= 90 → A. The highest grade is 91.
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
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.
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
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.
What is printed?
System.out.println(Student.letterFor(59.99) + Student.letterFor(60)
+ Student.letterFor(89.5) + Student.letterFor(90));Show answer
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.
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
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.
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
find walks the list from the start and returns at the first match. The second Ali is never looked at.
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
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).
What happens when printReport() is called on an empty GradeBook?
Show answer
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) ....
For the four students of this page (Deniz B, Ece A, Can F, Mert D), what does printHistogram() print?
Show answer
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.
Write the countPassing(double limit) method of GradeBook (field ArrayList<Student> students).
Show answer
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.
Write the topStudent() method of GradeBook: return the student with the highest average, or null if the list is empty.
Show answer
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.