Lesson 9 of 930 min2 challenges

Project: A Student Grade Manager

Student + StudentManager + Main, split into three layers, running as a real command-line app.

A student grade manager

Time to put all of it together — classes, encapsulation, collections, loops, input — into one program that actually runs and does something useful.

Step one: design the data

One student is a Student object. A group of students is a List<Student>. The operations that manage that group go in a separate class, StudentManager.

Splitting "the data" from "the management of the data" is the most basic layering in real software. Student describes one student and knows nothing about lists. StudentManager handles adding, finding, removing, and reporting, and knows nothing about how a grade is calculated. Each class has one job, so when something breaks you know which file to open.

Student.java

public class Student {
    private final String id;
    private String name;
    private double score;

    public Student(String id, String name, double score) {
        this.id = id;
        this.name = name;
        setScore(score);
    }

    public String getId() { return id; }
    public String getName() { return name; }
    public double getScore() { return score; }

    public void setScore(double score) {
        if (score < 0 || score > 100) {
            throw new IllegalArgumentException("Score must be between 0 and 100");
        }
        this.score = score;
    }

    public String getGrade() {
        if (score >= 90) return "Excellent";
        if (score >= 80) return "Good";
        if (score >= 60) return "Pass";
        return "Fail";
    }

    @Override
    public String toString() {
        return String.format("%-6s %-8s %6.1f  %s", id, name, score, getGrade());
    }
}

id is declared final, so it's assigned once in the constructor and can never change afterward. That's a business rule — a student ID is permanent — written in a way the compiler enforces rather than a comment nobody reads.

getGrade() uses early returns instead of else-if. Once a return fires, the method is over, so the later checks can't run. It reads more cleanly than a nested chain, and the order still matters for the same reason it did in lesson 4.

String.format works exactly like printf but hands you the string instead of printing it. %-6s is a left-aligned string padded to 6 characters, %6.1f is a number right-aligned in 6 characters with 1 decimal place. That's what makes the output line up in columns.

StudentManager.java

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class StudentManager {
    private final List<Student> students = new ArrayList<>();

    public void add(Student s) {
        if (findById(s.getId()) != null) {
            System.out.println("ID already exists: " + s.getId());
            return;
        }
        students.add(s);
        System.out.println("Added " + s.getName());
    }

    public Student findById(String id) {
        for (Student s : students) {
            if (s.getId().equals(id)) {
                return s;
            }
        }
        return null;
    }

    public boolean remove(String id) {
        Student s = findById(id);
        if (s == null) return false;
        students.remove(s);
        return true;
    }

    public void listAll() {
        if (students.isEmpty()) {
            System.out.println("No students yet");
            return;
        }
        System.out.println("ID     Name       Score  Grade");
        for (Student s : students) {
            System.out.println(s);
        }
    }

    public void statistics() {
        if (students.isEmpty()) {
            System.out.println("No students yet");
            return;
        }
        double total = 0;
        for (Student s : students) {
            total += s.getScore();
        }
        System.out.printf("Count %d, average %.2f%n", students.size(), total / students.size());

        List<Student> sorted = new ArrayList<>(students);
        sorted.sort(Comparator.comparingDouble(Student::getScore).reversed());
        System.out.println("Top score: " + sorted.get(0));
    }
}

Several things here are worth slowing down on.

add reuses findById to check for duplicates. The "how do I locate a student" logic exists in one method. If you later make lookups case-insensitive, you change one line and both features get it.

findById compares with .equals(), not ==. This is lesson 4 showing up in production. The id the user typed came from Scanner — it's a brand-new String object built at runtime. The id sitting in your list is a different object holding the same characters. == compares addresses and returns false. .equals() compares the text and returns true. Get this wrong and your search "works" in testing with hardcoded data and fails the moment a real person types something.

listAll prints s directly. That's the toString() you wrote in Student, doing its job.

total is a double, not an int. Look at total / students.size(). If total were an int, this would be integer division and your average of 88, 92, and 79 would come out as 86.00 instead of 86.33. The lesson 2 trap, in its natural habitat. This is genuinely how it bites people — not in a puzzle, but in a real line of real code that looks fine.

new ArrayList<>(students) makes a copy before sorting. sort rearranges the list in place, and you don't want reporting statistics to permanently reshuffle everyone's data. Sorting a copy leaves the original alone.

The Comparator.comparingDouble(Student::getScore).reversed() line is the most advanced thing in this file. Read it as "compare students by their score, then flip it so highest comes first." The :: is a method reference — a compact way to say "use this method to get the value." You don't need to fully understand it today; you need to recognize it, because modern Java is full of it.

Main.java

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        StudentManager manager = new StudentManager();
        Scanner sc = new Scanner(System.in);

        while (true) {
            System.out.println("\n1 add  2 list  3 stats  4 remove  0 quit");
            System.out.print("Choice: ");
            String choice = sc.nextLine().trim();

            switch (choice) {
                case "1":
                    System.out.print("ID: ");
                    String id = sc.nextLine().trim();
                    System.out.print("Name: ");
                    String name = sc.nextLine().trim();
                    System.out.print("Score: ");
                    try {
                        double score = Double.parseDouble(sc.nextLine().trim());
                        manager.add(new Student(id, name, score));
                    } catch (NumberFormatException e) {
                        System.out.println("Score has to be a number");
                    } catch (IllegalArgumentException e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                case "2":
                    manager.listAll();
                    break;
                case "3":
                    manager.statistics();
                    break;
                case "4":
                    System.out.print("ID to remove: ");
                    System.out.println(manager.remove(sc.nextLine().trim()) ? "Removed" : "Not found");
                    break;
                case "0":
                    System.out.println("Bye");
                    sc.close();
                    return;
                default:
                    System.out.println("No such option");
            }
        }
    }
}

Notice the first line of main: it creates a StudentManager object. It has to — main is static, and static code can't reach instance methods without an object to reach them on. That's the rule from lesson 8, in practice.

Every input goes through nextLine() and gets converted by hand. No nextInt(), no nextDouble() anywhere. That completely sidesteps the leftover-newline bug from lesson 3. In a menu loop that mixes numbers and text, mixing Scanner methods would break within about three keystrokes.

switch on a String works in Java 7 and later. The cases are "1" in quotes, not 1, because choice is a String.

try/catch is new. The shape is: run the risky code, and if it throws, jump to the matching catch instead of crashing. Two things can go wrong here, and each gets its own handler — NumberFormatException if they typed abc, IllegalArgumentException if they typed 150 and your setScore guard rejected it. Note that the second one prints e.getMessage(), which is the message you wrote back in Student. The validation you built in lesson 8 is now speaking directly to the user.

return inside main ends the program. break would only exit the switch and leave you in the while (true) loop forever.

Running it

Put all three files in one directory:

javac *.java
java Main

javac *.java compiles all three at once. You run Main because that's the one with the entry point.

A quick session:

1 add  2 list  3 stats  4 remove  0 quit
Choice: 1
ID: S01
Name: Maya
Score: 88
Added Maya

Take it further

  1. Print everyone sorted by score. (Hint: the sorting line in statistics() already does the hard part — move that idea into listAll.)
  2. Search by partial name. (Hint: s.getName().toLowerCase().contains(keyword.toLowerCase()).)
  3. Save to a file so the data survives a restart. (Hint: java.io.PrintWriter to write, Scanner on a File to read.)
  4. Ask your AI tutor: "What is :: doing in Comparator.comparingDouble(Student::getScore)?" That's a method reference, one of the most important additions in Java 8.

Ten-second check

What happens in findById if you write s.getId() == id instead of s.getId().equals(id)?

It will almost always find nothing. == asks whether the two variables point at the same object. The ID the user typed is a fresh String object created at runtime; the one in your list is a different object with identical characters. It compiles without a single warning, which is precisely what makes it dangerous. Compare strings with equals(), every time.

Ad slot (AdSense not configured; ads appear here in production)

Hands-on

Reading it isn't knowing it. Writing it is.

0/2 passed

Challenges and grading are completely free, and a wrong answer costs you nothing. Stuck? Hit "Ask the AI": it can see the code you are writing.

Level 01Write code+25 pts · +40 XP

Compute an average (watch the integer division)

Given `int[] scores = {88, 92, 79}`, print the average rounded to two decimal places. Expected output: `86.33`

Sample input

(none)

Expected output

86.33
javaTab = 4 spaces
Level 02Multiple choice+10 pts · +15 XP

The trap hiding in findById

What happens if you write `s.getId() == id` instead of `s.getId().equals(id)`?

  • Might be multiple choice. Wrong answers cost nothing, so just try.

Finished? Mark it

Marking it done saves your progress, gives you 50 XP and keeps your streak alive.