Back to blog
Backend Systemsbeginner

Java Project: Grade Tracker App

Build a console-based student grade tracker in Java using OOP, collections, file I/O, and exceptions. A complete beginner project that ties everything together.

Asma HafeezApril 17, 20266 min read
javaprojectoopcollectionsfile-io
Share:𝕏

Java Project: Grade Tracker App

This project applies everything from the Java beginner series: OOP, generics, collections, streams, file I/O, and exception handling.


What We're Building

Grade Tracker
─────────────────────────
1. Add student
2. Add grade
3. List students
4. Show statistics
5. Save to file
6. Load from file
0. Exit
> 1
Enter student name: Alice
Student Alice added.

> 2
Enter student name: Alice
Enter subject: Math
Enter grade (0-100): 92
Grade added.

> 4
--- Statistics ---
Alice  | Math: 92  | Avg: 92.0 | Top Grade: 92

Project Structure

grade-tracker/
├── src/
│   ├── model/
│   │   ├── Student.java
│   │   └── GradeEntry.java
│   ├── service/
│   │   ├── GradeBook.java
│   │   └── FileStorage.java
│   └── Main.java

Step 1: Model Classes

JAVA
// src/model/GradeEntry.java
package model;

public record GradeEntry(String subject, double grade) {
    public GradeEntry {
        if (grade < 0 || grade > 100) {
            throw new IllegalArgumentException("Grade must be between 0 and 100, got: " + grade);
        }
        if (subject == null || subject.isBlank()) {
            throw new IllegalArgumentException("Subject cannot be blank");
        }
    }

    public String letterGrade() {
        if (grade >= 90) return "A";
        if (grade >= 80) return "B";
        if (grade >= 70) return "C";
        if (grade >= 60) return "D";
        return "F";
    }

    @Override
    public String toString() {
        return String.format("%s: %.1f (%s)", subject, grade, letterGrade());
    }
}
JAVA
// src/model/Student.java
package model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.OptionalDouble;

public class Student {
    private final String name;
    private final List<GradeEntry> grades;

    public Student(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Student name cannot be blank");
        }
        this.name = name;
        this.grades = new ArrayList<>();
    }

    public String getName() { return name; }

    public List<GradeEntry> getGrades() {
        return Collections.unmodifiableList(grades);
    }

    public void addGrade(GradeEntry entry) {
        grades.add(entry);
    }

    public OptionalDouble average() {
        return grades.stream()
            .mapToDouble(GradeEntry::grade)
            .average();
    }

    public OptionalDouble highestGrade() {
        return grades.stream()
            .mapToDouble(GradeEntry::grade)
            .max();
    }

    public OptionalDouble lowestGrade() {
        return grades.stream()
            .mapToDouble(GradeEntry::grade)
            .min();
    }

    public String summary() {
        if (grades.isEmpty()) return name + " (no grades)";

        String gradeList = grades.stream()
            .map(GradeEntry::toString)
            .reduce((a, b) -> a + ", " + b)
            .orElse("");

        return String.format("%-12s | %-40s | Avg: %.1f",
            name, gradeList, average().orElse(0));
    }
}

Step 2: GradeBook Service

JAVA
// src/service/GradeBook.java
package service;

import model.GradeEntry;
import model.Student;

import java.util.*;
import java.util.stream.Collectors;

public class GradeBook {
    private final Map<String, Student> students = new LinkedHashMap<>();

    public void addStudent(String name) {
        if (students.containsKey(name)) {
            throw new IllegalArgumentException("Student already exists: " + name);
        }
        students.put(name, new Student(name));
    }

    public void addGrade(String studentName, String subject, double grade) {
        Student student = getStudent(studentName);
        student.addGrade(new GradeEntry(subject, grade));
    }

    public Student getStudent(String name) {
        Student student = students.get(name);
        if (student == null) {
            throw new NoSuchElementException("Student not found: " + name);
        }
        return student;
    }

    public Collection<Student> getAllStudents() {
        return Collections.unmodifiableCollection(students.values());
    }

    public List<Student> topStudents(int n) {
        return students.values().stream()
            .filter(s -> s.average().isPresent())
            .sorted(Comparator.comparingDouble(s -> -s.average().orElse(0)))
            .limit(n)
            .collect(Collectors.toList());
    }

    public Map<String, Double> subjectAverages() {
        Map<String, List<Double>> bySubject = new HashMap<>();
        for (Student student : students.values()) {
            for (GradeEntry entry : student.getGrades()) {
                bySubject.computeIfAbsent(entry.subject(), k -> new ArrayList<>())
                         .add(entry.grade());
            }
        }
        Map<String, Double> result = new LinkedHashMap<>();
        bySubject.forEach((subject, grades) ->
            result.put(subject, grades.stream().mapToDouble(Double::doubleValue).average().orElse(0))
        );
        return result;
    }

    public int studentCount() { return students.size(); }
    public boolean isEmpty() { return students.isEmpty(); }
}

Step 3: File Storage

JAVA
// src/service/FileStorage.java
package service;

import model.GradeEntry;
import model.Student;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public class FileStorage {
    private static final String HEADER = "student,subject,grade";

    public void save(GradeBook book, Path path) throws IOException {
        StringBuilder sb = new StringBuilder(HEADER).append("\n");
        for (Student student : book.getAllStudents()) {
            for (GradeEntry entry : student.getGrades()) {
                sb.append(student.getName()).append(",")
                  .append(entry.subject()).append(",")
                  .append(entry.grade()).append("\n");
            }
        }
        Files.writeString(path, sb.toString());
        System.out.println("Saved to " + path);
    }

    public void load(GradeBook book, Path path) throws IOException {
        List<String> lines = Files.readAllLines(path);
        int loaded = 0;
        for (int i = 1; i < lines.size(); i++) {  // skip header
            String line = lines.get(i).trim();
            if (line.isBlank()) continue;
            String[] parts = line.split(",");
            if (parts.length != 3) continue;

            String studentName = parts[0].trim();
            String subject     = parts[1].trim();
            double grade       = Double.parseDouble(parts[2].trim());

            if (!studentExists(book, studentName)) {
                book.addStudent(studentName);
            }
            book.addGrade(studentName, subject, grade);
            loaded++;
        }
        System.out.println("Loaded " + loaded + " grade(s) from " + path);
    }

    private boolean studentExists(GradeBook book, String name) {
        try {
            book.getStudent(name);
            return true;
        } catch (java.util.NoSuchElementException e) {
            return false;
        }
    }
}

Step 4: Main — Console UI

JAVA
// src/Main.java
import service.FileStorage;
import service.GradeBook;
import model.Student;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Scanner;

public class Main {
    private static final GradeBook book = new GradeBook();
    private static final FileStorage storage = new FileStorage();
    private static final Scanner scanner = new Scanner(System.in);
    private static final Path SAVE_FILE = Path.of("grades.csv");

    public static void main(String[] args) {
        System.out.println("=== Grade Tracker ===");
        boolean running = true;
        while (running) {
            printMenu();
            String choice = scanner.nextLine().trim();
            running = handleChoice(choice);
        }
        System.out.println("Goodbye!");
        scanner.close();
    }

    private static void printMenu() {
        System.out.println("""

            1. Add student
            2. Add grade
            3. List students
            4. Show statistics
            5. Save to file
            6. Load from file
            0. Exit
            > """);
    }

    private static boolean handleChoice(String choice) {
        switch (choice) {
            case "1" -> addStudent();
            case "2" -> addGrade();
            case "3" -> listStudents();
            case "4" -> showStatistics();
            case "5" -> saveToFile();
            case "6" -> loadFromFile();
            case "0" -> { return false; }
            default -> System.out.println("Invalid option. Try again.");
        }
        return true;
    }

    private static void addStudent() {
        System.out.print("Enter student name: ");
        String name = scanner.nextLine().trim();
        try {
            book.addStudent(name);
            System.out.println("Student " + name + " added.");
        } catch (IllegalArgumentException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }

    private static void addGrade() {
        System.out.print("Enter student name: ");
        String name = scanner.nextLine().trim();
        System.out.print("Enter subject: ");
        String subject = scanner.nextLine().trim();
        System.out.print("Enter grade (0-100): ");
        String gradeStr = scanner.nextLine().trim();

        try {
            double grade = Double.parseDouble(gradeStr);
            book.addGrade(name, subject, grade);
            System.out.println("Grade added.");
        } catch (NumberFormatException e) {
            System.out.println("Invalid grade: must be a number.");
        } catch (Exception e) {
            System.out.println("Error: " + e.getMessage());
        }
    }

    private static void listStudents() {
        if (book.isEmpty()) {
            System.out.println("No students yet.");
            return;
        }
        System.out.println("\n--- Students ---");
        book.getAllStudents().forEach(s -> System.out.println(s.summary()));
    }

    private static void showStatistics() {
        if (book.isEmpty()) {
            System.out.println("No data yet.");
            return;
        }
        System.out.println("\n--- Statistics ---");

        System.out.println("\nTop 3 students:");
        book.topStudents(3).forEach(s ->
            System.out.printf("  %-12s  Avg: %.1f%n", s.getName(), s.average().orElse(0))
        );

        System.out.println("\nSubject averages:");
        book.subjectAverages().forEach((subject, avg) ->
            System.out.printf("  %-15s  %.1f%n", subject, avg)
        );
    }

    private static void saveToFile() {
        try {
            storage.save(book, SAVE_FILE);
        } catch (IOException e) {
            System.out.println("Save failed: " + e.getMessage());
        }
    }

    private static void loadFromFile() {
        try {
            storage.load(book, SAVE_FILE);
        } catch (IOException e) {
            System.out.println("Load failed: " + e.getMessage());
        }
    }
}

Running the App

Bash
# Compile
javac -d out src/model/*.java src/service/*.java src/Main.java

# Run
java -cp out Main

What This Project Demonstrates

  • RecordsGradeEntry as an immutable value object
  • EncapsulationStudent controls its own grade list
  • CollectionsLinkedHashMap for insertion-ordered students, ArrayList for grades
  • Streams — averages, filtering, sorting with lambdas
  • Custom exceptions — validated constructors with IllegalArgumentException
  • File I/O — CSV save/load with Files.writeString / Files.readAllLines
  • Switch expressions — clean menu dispatch

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.