Student Enrollment System in Java

Build a student enrollment system using Java Collections and OOP.

Introduction

30 Second Summary

Every university needs a system to track which students are enrolled in which courses. Behind the scenes, picking the wrong data structure means slow lookups, duplicate records, and searches that mysteriously return nothing.

In this project, you will build a command-line Student Enrollment System using the Java Collections Framework. You'll manage students, courses, and enrollments with HashMap, ArrayList, HashSet, and TreeMap. Along the way, you'll learn exactly why interviewers drill on these concepts.

What You'll Build

You'll run a single Java program from the terminal and watch it enroll students, catch a broken lookup, fix it on the spot, block duplicate enrollments, and print a ranked GPA leaderboard.

By the end of this project, you'll have:

  • A student roster stored in an ArrayList that you can print, search by index, and modify on the fly.
  • A HashMap enrollment lookup that returns null for a student you just added, then works perfectly after you fix the equals/hashCode contract.
  • Duplicate-proof enrollments powered by HashSet, plus a self-sorting GPA leaderboard built with TreeMap using the Comparable and Comparator interfaces.
  • Secret Mission: Trigger a ConcurrentModificationException during iteration, then fix it with safe removal techniques.

Are there any prerequisites?

This project assumes you have basic Java knowledge including variables, loops, methods, and classes. You'll also need JDK 17 or higher and an IDE like IntelliJ IDEA, Eclipse, or VS Code already installed on your machine.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

This project builds a Student Enrollment System that uses the core Java Collections types interviewers love to drill on. You will use HashMap, ArrayList, HashSet, and TreeMap to manage students, courses, and enrollments, and you will experience firsthand why the equals/hashCode contract matters by watching a lookup fail before you fix it.

Set Up Your Project Files

Every Java project starts with a solid foundation of well-structured classes. For your enrollment system, you need three files that work together: a class for students, a class for courses, and a main entry point to run everything.

In this step, you will create your project folder and build out all three Java files. Each class follows proper encapsulation with private fields and public getters. By the end, you will compile and run the system to confirm everything is wired up.

In this step, get ready to:
  • Create a project folder with three Java files.
  • Build Student and Course classes with encapsulation.
  • Compile and run the system to verify the setup works.
Create your project folder and files
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder, move into it, and create the three Java files by running these commands:

macOS

mkdir enrollment-system
cd enrollment-system
touch Student.java Course.java EnrollmentSystem.java

Windows

mkdir enrollment-system
cd enrollment-system
New-Item Student.java, Course.java, EnrollmentSystem.java
  • Confirm the files exist by running:
ls

You should see Course.java, EnrollmentSystem.java, and Student.java listed in the output.

Not seeing the files?

  • Make sure you ran cd enrollment-system before running ls. You need to be inside the folder to see its contents.
  • On Windows, if New-Item produced an error, check that PowerShell is running (not Command Prompt).

Help me create my Java project files.

  • Now open your IDE. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type the name of your IDE (e.g. IntelliJ, Eclipse, or VS Code) and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder (or Open), then select the enrollment-system folder on your Desktop.
Build the Student and Course classes

Both classes use encapsulation: every field is private so nothing outside the class can change it directly. Public getter methods provide controlled, read-only access.

  • Open Student.java in your IDE and paste the following code:
public class Student {
    // Private fields for encapsulation
    private String studentId;
    private String name;
    private double gpa;

    public Student(String studentId, String name, double gpa) {
        this.studentId = studentId;
        this.name = name;
        this.gpa = gpa;
    }

    public String getStudentId() {
        return studentId;
    }

    public String getName() {
        return name;
    }

    public double getGpa() {
        return gpa;
    }

    // Formats the student for readable printing
    @Override
    public String toString() {
        return name + " (" + studentId + ") - GPA: " + gpa;
    }
}

What does this code do?

  • studentId, name, and gpa are private fields. Outside code must go through the getters to read them.
  • The constructor takes all three values and assigns them with this. to distinguish between the field and the parameter.
  • toString() is overridden so that printing a Student object shows something readable instead of a memory address.
  • Save Student.java.
  • Compile it to check for errors by running this command in your terminal:
javac Student.java

If the compile succeeds, you will see no output at all. No news is good news from javac.

Seeing a compilation error?

  • Check that every line inside the class ends with a semicolon. Missing semicolons are the most common Java compile error.
  • Make sure the filename matches the class name exactly: Student.java for public class Student. Java is case-sensitive.
  • Make sure your terminal is inside the enrollment-system folder. Run ls to confirm you can see the .java files.

Help me fix my Java compilation error.

✔️ Awesome, I've got everything!

Great. Make sure Student.java is saved before moving on.

ⓧ I'd like to double check the full code

public class Student {
    private String studentId;
    private String name;
    private double gpa;

    public Student(String studentId, String name, double gpa) {
        this.studentId = studentId;
        this.name = name;
        this.gpa = gpa;
    }

    public String getStudentId() {
        return studentId;
    }

    public String getName() {
        return name;
    }

    public double getGpa() {
        return gpa;
    }

    @Override
    public String toString() {
        return name + " (" + studentId + ") - GPA: " + gpa;
    }
}
  • Now open Course.java in your IDE and paste the following code:
public class Course {
    // Private fields for encapsulation
    private String courseCode;
    private String courseName;
    private int credits;

    public Course(String courseCode, String courseName, int credits) {
        this.courseCode = courseCode;
        this.courseName = courseName;
        this.credits = credits;
    }

    public String getCourseCode() {
        return courseCode;
    }

    public String getCourseName() {
        return courseName;
    }

    public int getCredits() {
        return credits;
    }

    // Formats the course for readable printing
    @Override
    public String toString() {
        return courseName + " (" + courseCode + ") - " + credits + " credits";
    }
}

What does this code do?

The Course class follows the same encapsulated pattern as Student. It stores a course code, name, and credit count behind private fields with public getters.

The toString() method formats the output so you can print a course and instantly see its code and credit value.

  • Save Course.java.
  • Compile it by running:
javac Course.java

Again, no output means the file compiled successfully.

Compile failed for Course.java?

  • Double check that the field type for credits is int, not Integer or String. The constructor parameter type must match.
  • Make sure every curly brace has a matching pair. A missing closing } at the end of the class is easy to miss.

Help me fix my Course.java compilation error.

✔️ Awesome, I've got everything!

Great. Make sure Course.java is saved before moving on.

ⓧ I'd like to double check the full code

public class Course {
    private String courseCode;
    private String courseName;
    private int credits;

    public Course(String courseCode, String courseName, int credits) {
        this.courseCode = courseCode;
        this.courseName = courseName;
        this.credits = credits;
    }

    public String getCourseCode() {
        return courseCode;
    }

    public String getCourseName() {
        return courseName;
    }

    public int getCredits() {
        return credits;
    }

    @Override
    public String toString() {
        return courseName + " (" + courseCode + ") - " + credits + " credits";
    }
}

What is encapsulation?

Encapsulation means hiding a class's internal data behind private fields and exposing only what is needed through public methods. This prevents outside code from putting an object into an invalid state.

Both Student and Course follow this pattern. No one can accidentally set a GPA to -5 because there is no setter for gpa.

Add the main method and run your system

The last file is the entry point. It holds the main method that Java looks for when you run the program.

  • Open EnrollmentSystem.java in your IDE and paste the following code:
public class EnrollmentSystem {
    // Entry point for the enrollment system
    public static void main(String[] args) {
        System.out.println("Enrollment System ready!");
    }
}

What does this code do?

public static void main(String[] args) is the standard entry point that Java looks for when you run a class. Every Java application needs exactly one of these.

Right now it prints a simple message. In the next steps, you will replace this with the full enrollment system logic.

  • Save EnrollmentSystem.java.

✔️ Awesome, I've got everything!

Great. Make sure EnrollmentSystem.java is saved before moving on.

ⓧ I'd like to double check the full code

public class EnrollmentSystem {
    public static void main(String[] args) {
        System.out.println("Enrollment System ready!");
    }
}

Before you run the next command, what do you think you will see in the terminal?

  • Compile all three files and run the system by running this command:
javac EnrollmentSystem.java Student.java Course.java && java EnrollmentSystem

You should see Enrollment System ready! printed to the console. That confirms all three files compile and the main method runs correctly.

What does this command do?

  • javac EnrollmentSystem.java Student.java Course.java compiles all three files at once, producing .class bytecode files.
  • The && means the second command only runs if the compilation succeeds.
  • java EnrollmentSystem runs the compiled class by looking for its main method.

Not seeing the ready message?

  • If you see a compilation error, check that all three files are saved and that each filename matches its class name exactly (case-sensitive).
  • If you see a message about not being able to find or load the main class, make sure you are running java EnrollmentSystem without the .java extension. The java command takes a class name, not a filename.
  • Confirm your terminal is inside the enrollment-system folder by running ls. You should see the three .java files listed.

Help me troubleshoot my Java compile or run error.

Your project skeleton is in place and all three files compile cleanly. Next up, you will fill in the main method with a real student roster using ArrayList.

Build a Student Roster with ArrayList

In the last step, you set up three Java files with encapsulated Student and Course classes and confirmed everything compiles.

Now you need a way to store and display a list of students. ArrayList is the go-to Java collection when you need ordered, indexed storage with fast random access.

In this step, get ready to:
  • Create an ArrayList of five students and print the full roster.
  • Test ArrayList operations like index-based access and search.
Create the student roster

An ArrayList keeps elements in the order you add them and lets you access any element by its position (index). This makes it perfect for a student roster you want to display and iterate over.

  • Open EnrollmentSystem.java in your editor.
  • At the very top of the file, above the public class line, add this import:
import java.util.ArrayList;
  • Inside the main method, replace the System.out.println("Enrollment System ready!"); line with this code:
        // --- ArrayList: Student Roster ---
        ArrayList<Student> studentRoster = new ArrayList<>();
        studentRoster.add(new Student("S101", "Alice", 3.8));
        studentRoster.add(new Student("S102", "Bob", 3.5));
        studentRoster.add(new Student("S103", "Charlie", 3.9));
        studentRoster.add(new Student("S104", "Diana", 3.2));
        studentRoster.add(new Student("S105", "Eve", 3.7));

        System.out.println("=== Student Roster (ArrayList) ===");
        for (Student s : studentRoster) {
            System.out.println("  " + s);
        }
        System.out.println("Total students: " + studentRoster.size());

What does this code do?

  • ArrayList<Student> studentRoster = new ArrayList<>() creates a new typed list that only holds Student objects. The angle brackets are Java generics, which enforce type safety at compile time.
  • .add() appends each new student to the end of the list, preserving insertion order.
  • The for-each loop (for (Student s : studentRoster)) iterates through every student in order. Each student prints using the toString() method you wrote in Step 1.
  • .size() returns the number of elements currently in the list.
  • Save EnrollmentSystem.java.
  • Compile and run all three files by running this command in your terminal:
javac EnrollmentSystem.java Student.java Course.java && java EnrollmentSystem

You should see all five students printed in the order you added them, followed by Total students: 5.

Not seeing the roster?

  • Make sure the import java.util.ArrayList; line is at the very top of the file, above public class EnrollmentSystem.
  • Check that you removed the old System.out.println("Enrollment System ready!"); line. The new roster code replaces it entirely.
  • If you see a compilation error about Student not being found, make sure all three .java files are in the same folder and you are compiling all three at once.
  • Help me debug my ArrayList compilation error.
Explore ArrayList operations

ArrayList gives you more than just storage and iteration. Interviewers often ask about index-based access and search operations to test whether you understand how ArrayList differs from other collections.

  • In EnrollmentSystem.java, add the following code below the System.out.println("Total students: " + studentRoster.size()); line:
        // ArrayList operations
        System.out.println("\nFirst student (index 0): " + studentRoster.get(0));
        System.out.println("Contains Alice? " + studentRoster.contains(new Student("S101", "Alice", 3.8)));

What do these operations do?

  • .get(0) retrieves the student at index 0 (the first student added). This is O(1) random access, one of ArrayList's main strengths.
  • .contains() searches the list for a matching object. Under the hood, it calls .equals() on each element to check for a match.

What about .remove()?

ArrayList also supports .remove(index) to delete an element by its position. This shifts all elements after it down by one index. It is a common interview topic because that shifting makes removal O(n) for large lists.

  • Save EnrollmentSystem.java.

Before you compile and run, think about this: you are creating a brand new Student object with the same ID, name, and GPA as Alice and passing it to .contains(). Do you think it will return true or false?

  • Compile and run the program:
javac EnrollmentSystem.java Student.java Course.java && java EnrollmentSystem

You should see the full roster printed, followed by First student (index 0): Alice (S101) - GPA: 3.8. That confirms .get(0) works as expected.

But look at the .contains() result. It says Contains Alice? false. Alice is clearly in the roster, so why does Java say she is not there?

Why does .contains() return false?

You created a new Student object with the same ID, name, and GPA as Alice. To a human, those two objects are "the same student." But Java's default .equals() method compares memory addresses, not field values.

Since the Alice in the roster and the Alice you passed to .contains() are two separate objects in memory, Java considers them unequal. This is the equals/hashCode contract problem, and you will fix it in a later step.

Seeing a compilation error?

  • Check that the new lines are inside the main method's curly braces, after the roster printing code.
  • Make sure the backslash-n (\n) in the first println is inside the quotes. It adds a blank line before the output.
  • Help me debug my ArrayList operations code.

✔️ Awesome, I've got everything!

Great. Double check you have saved EnrollmentSystem.java before moving on.

ⓧ I'd like to double check the full code

Here is the complete EnrollmentSystem.java file at this point. Student.java and Course.java have not changed from Step 1.

import java.util.ArrayList;

public class EnrollmentSystem {
    public static void main(String[] args) {

        // --- ArrayList: Student Roster ---
        ArrayList<Student> studentRoster = new ArrayList<>();
        studentRoster.add(new Student("S101", "Alice", 3.8));
        studentRoster.add(new Student("S102", "Bob", 3.5));
        studentRoster.add(new Student("S103", "Charlie", 3.9));
        studentRoster.add(new Student("S104", "Diana", 3.2));
        studentRoster.add(new Student("S105", "Eve", 3.7));

        System.out.println("=== Student Roster (ArrayList) ===");
        for (Student s : studentRoster) {
            System.out.println("  " + s);
        }
        System.out.println("Total students: " + studentRoster.size());

        // ArrayList operations
        System.out.println("\nFirst student (index 0): " + studentRoster.get(0));
        System.out.println("Contains Alice? " + studentRoster.contains(new Student("S101", "Alice", 3.8)));
    }
}

Your student roster is alive and printing. Next up, you will use a HashMap to map each student to their enrolled courses. That same .equals() problem is about to get much more dramatic.

Map Enrollments with HashMap and Hit the Wall

You have a working student roster stored in an ArrayList. Now you need to track which courses each student is enrolled in.

A HashMap lets you associate a key (like a Student) with a value (like a list of Courses) and look it up in constant time. In this step, you'll build a HashMap-based enrollment registry. But you'll also discover a critical gap that makes HashMap break with custom objects.

In this step, get ready to:
  • Create Course objects for the enrollment system.
  • Build a HashMap enrollment registry mapping students to courses.
  • Attempt a student lookup and discover why it fails.
Create courses and build the enrollment registry

Your enrollment system needs courses for students to enroll in. You'll create four Course objects and then use a HashMap to map each student to the courses they're taking.

  • In EnrollmentSystem.java, add two new imports below the existing import java.util.ArrayList; line:
import java.util.Arrays;
import java.util.HashMap;
  • Then add the following code at the bottom of the main method, below the Contains Alice? line:
        // --- Courses ---
        Course java = new Course("CS201", "Java Programming", 4);
        Course dataStructures = new Course("CS202", "Data Structures", 3);
        Course algorithms = new Course("CS301", "Algorithms", 3);
        Course databases = new Course("CS303", "Database Systems", 4);

        // --- HashMap: Enrollment Registry ---
        HashMap<Student, ArrayList<Course>> enrollments = new HashMap<>();
        enrollments.put(studentRoster.get(0), new ArrayList<>(Arrays.asList(java, dataStructures, algorithms)));
        enrollments.put(studentRoster.get(1), new ArrayList<>(Arrays.asList(java, databases)));
        enrollments.put(studentRoster.get(2), new ArrayList<>(Arrays.asList(dataStructures, algorithms, databases)));
        enrollments.put(studentRoster.get(3), new ArrayList<>(Arrays.asList(java)));
        enrollments.put(studentRoster.get(4), new ArrayList<>(Arrays.asList(algorithms, databases)));

What does this code do?

You created four Course objects with codes, names, and credit values. These are the courses students can enroll in.

The HashMap<Student, ArrayList<Course>> maps each Student key to a list of Course values. Each put() call enrolls one student by associating them with their courses. Arrays.asList() is a shortcut for creating a list from individual items.

  • Save EnrollmentSystem.java.
  • Compile and run to verify everything still works by running:
javac EnrollmentSystem.java Student.java Course.java && java EnrollmentSystem

You should see the same student roster output from Step 2. The enrollments are stored in memory but nothing prints them yet.

Seeing a compilation error?

  • Check that both import java.util.Arrays; and import java.util.HashMap; are at the top of the file, before the class declaration.
  • Make sure the variable name java in your Course declaration matches what you pass to Arrays.asList(). Java is case-sensitive.
  • Verify the new code is inside the main method's curly braces, not outside the method or outside the class.

Help me debug my HashMap enrollment code

Attempt to look up a student and hit the wall

Your enrollment registry is loaded. Now try to look up a specific student's courses by creating a new Student object with the same ID.

  • Add this lookup code at the bottom of the main method, below the enrollment put() calls:
        // --- Lookup using a NEW object with same ID ---
        Student lookupStudent = new Student("S101", "Alice", 3.8);
        ArrayList<Course> aliceCourses = enrollments.get(lookupStudent);
        System.out.println("\n=== Lookup: Alice's Courses ===");
        if (aliceCourses != null) {
            for (Course c : aliceCourses) {
                System.out.println("  " + c);
            }
        } else {
            System.out.println("  Student not found!");
        }

What does this code do?

You create a brand new Student object with the exact same ID (S101), name (Alice), and GPA (3.8) as the original. Then you pass it to enrollments.get(lookupStudent) to retrieve Alice's courses.

The if/else block checks whether the result is null. If the lookup succeeds, it prints the courses. If not, it prints a failure message.

  • Save EnrollmentSystem.java.

Before you run this, think about it: the lookup Student has the same ID, name, and GPA as the one you added to the HashMap. Do you think the lookup will find Alice's courses, or will it return null?

  • Compile and run the program:
javac EnrollmentSystem.java Student.java Course.java && java EnrollmentSystem

You'll see the roster print as before, but the lookup section prints:

=== Lookup: Alice's Courses ===
  Student not found!

Even though lookupStudent has the exact same ID, name, and GPA as the original Alice, HashMap could not find a match. This is intentional and expected.

Why does the lookup fail?

HashMap uses two methods to match keys: hashCode() to find the right bucket, then equals() to confirm the key matches. You have not overridden either method in Student.

Java's default equals() (inherited from Object) compares memory addresses, not field values. Two different Student objects with identical data are treated as completely different keys. This is the single most asked Java Collections interview question.

Did you also notice that Contains Alice? printed false back in Step 2? That was the same bug. ArrayList's contains() also relies on equals() under the hood.

Not seeing the Student not found message?

  • Make sure lookupStudent is created with new Student("S101", "Alice", 3.8), not by referencing an existing object from the roster like studentRoster.get(0). The whole point is that a new object with the same data still fails the lookup.
  • Verify the lookup code is placed after the put() calls. If it runs before the enrollments are added, the HashMap would be empty.

Help me understand my HashMap lookup result

✔️ Awesome, I've got everything!

Great. Double check you've saved EnrollmentSystem.java. Student.java and Course.java are unchanged in this step.

ⓧ I'd like to double check the full code

Scroll through your EnrollmentSystem.java and confirm each of these sections is present, in order:

  • Three imports at the top: ArrayList, Arrays, and HashMap.
  • The ArrayList student roster with five students, the for-each print loop, and the .size() count (from Step 2).
  • The .get(0) and .contains() lines (from Step 2).
  • Four Course objects: java, dataStructures, algorithms, databases.
  • The HashMap<Student, ArrayList<Course>> with five put() calls enrolling each student.
  • The lookup block creating a new lookupStudent and calling enrollments.get(lookupStudent) with the if/else print block.

You've just witnessed the most common Java Collections pitfall firsthand. Your HashMap has the data, but it cannot find it because your Student class does not tell Java what makes two students "equal." In the next step, you'll fix this by overriding equals() and hashCode(), and then use a HashSet to prevent duplicate enrollments.

Fix the equals/hashCode Contract and Enforce Uniqueness with HashSet

In the last step, you hit a wall. You created a brand new Student object with the exact same ID as Alice, but HashMap returned null. The lookup failed because Java's default equals() method compares memory addresses, not field values.

The fix is to override two methods that work together as a contract: equals() and hashCode(). Once these methods tell Java how to compare your custom objects by their fields, every hash-based collection starts working correctly with them.

In this step, get ready to:
  • Override equals() and hashCode() in Student.java and Course.java to fix HashMap lookups.
  • Swap ArrayList for HashSet to prevent duplicate course enrollments.
  • Build a course popularity counter using HashMap.merge().
Override equals() and hashCode() in both classes

HashMap finds keys by calling hashCode() to narrow down which bucket to look in, then equals() to confirm the match. Without overriding both, two Student objects with the same ID are treated as completely different keys.

  • In Student.java, add this import as the very first line of the file, above the class declaration:
import java.util.Objects;
  • Add these two methods after your toString() method, still inside the Student class:
// Check if two Students are the same by comparing studentId
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Student student = (Student) o;
    return Objects.equals(studentId, student.studentId);
}

// Generate a hash based on studentId (must match equals)
@Override
public int hashCode() {
    return Objects.hash(studentId);
}

What is the equals/hashCode contract?

These two methods form a strict contract: if two objects are equals(), they must return the same hashCode(). Breaking this contract means HashMap can put a key in one bucket but look for it in a completely different one.

Here, two Students are considered equal if they share the same studentId. The hash is derived from studentId too, so the contract holds.

  • Save Student.java.

✔️ Awesome, I've got everything!

Great. Double check you have saved Student.java.

ⓧ I'd like to double check the full code

Scroll up and confirm your Student.java has these sections, in order:

  • import java.util.Objects; at the very top.
  • Three private fields: studentId, name, gpa with constructor and getters.
  • toString() method.
  • equals() comparing by studentId.
  • hashCode() using Objects.hash(studentId).

Now apply the same pattern to Course.java so that HashSet can detect duplicate courses later.

  • In Course.java, add this import as the very first line of the file:
import java.util.Objects;
  • Add these two methods after your toString() method, still inside the Course class:
// Check if two Courses are the same by comparing courseCode
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Course course = (Course) o;
    return Objects.equals(courseCode, course.courseCode);
}

// Generate a hash based on courseCode (must match equals)
@Override
public int hashCode() {
    return Objects.hash(courseCode);
}

What does this code do?

The Course version follows the same pattern. Two courses are considered equal if they share the same courseCode. The hash is derived from courseCode so that HashSet can correctly identify duplicate courses when you add them later.

  • Save Course.java.

Before you run this, what do you think will happen when the program reaches the Alice lookup? Will it still print "Student not found!" or will the fix change the result?

  • Compile and run all three files by running this command:
javac Student.java Course.java EnrollmentSystem.java && java EnrollmentSystem

The lookup now prints Alice's courses instead of "Student not found!". You also see "Contains Alice? true" instead of false. Both fixes come from the same change: equals() and hashCode() now compare by studentId instead of memory address.

Still seeing "Student not found!"?

  • Make sure you saved Student.java before compiling. Unsaved changes are not picked up by javac.
  • Check that your equals() method compares studentId, not name or gpa. The field used in equals() must also be the field passed to Objects.hash().
  • Confirm the methods are inside the Student class (between its opening and closing braces), not outside it.

Why is my HashMap lookup still returning null after overriding equals and hashCode?

✔️ Awesome, I've got everything!

Great. Double check you have saved Course.java.

ⓧ I'd like to double check the full code

Scroll up and confirm your Course.java has these sections, in order:

  • import java.util.Objects; at the very top.
  • Three private fields: courseCode, courseName, credits with constructor and getters.
  • toString() method.
  • equals() comparing by courseCode.
  • hashCode() using Objects.hash(courseCode).
Swap ArrayList for HashSet and test duplicate enrollment

Right now, each student's course list is an ArrayList. That means nothing stops you from adding the same course twice. HashSet solves this by using equals() and hashCode() to silently reject duplicate entries.

  • In EnrollmentSystem.java, add this import at the top of the file with the other imports:
import java.util.HashSet;
  • Find the enrollment map declaration:
HashMap<Student, ArrayList<Course>> enrollments = new HashMap<>();
  • Change ArrayList<Course> to HashSet<Course> so the line reads:
HashMap<Student, HashSet<Course>> enrollments = new HashMap<>();
  • In each of the five enrollments.put() lines below it, change new ArrayList<> to new HashSet<>.
  • Scroll down to the lookup section and find this line:
ArrayList<Course> aliceCourses = enrollments.get(lookupStudent);
  • Change ArrayList<Course> to HashSet<Course> so it reads:
HashSet<Course> aliceCourses = enrollments.get(lookupStudent);

Now add the duplicate enrollment test. This will try to enroll Bob in Java Programming a second time and see if HashSet allows it.

  • Add this code at the end of the main method, after the closing brace of the lookup's if/else block:
// --- HashSet: Duplicate Prevention ---
System.out.println("\n=== Duplicate Enrollment Test (HashSet) ===");
HashSet<Course> bobCourses = enrollments.get(studentRoster.get(1));
System.out.println("Bob's courses before: " + bobCourses.size());
// Try to add Java Programming again (Bob already has it)
boolean added = bobCourses.add(java);
System.out.println("Added duplicate? " + added);
System.out.println("Bob's courses after: " + bobCourses.size());

What does this code do?

  • enrollments.get(studentRoster.get(1)) retrieves Bob's course set from the enrollment map.
  • bobCourses.add(java) tries to add Java Programming, which Bob already has.
  • HashSet.add() returns a boolean: true if the element was new, false if it was already present. This is how you detect duplicates.

What do you think added will print? Will the course count change?

  • Save EnrollmentSystem.java and compile and run by running this command:
javac Student.java Course.java EnrollmentSystem.java && java EnrollmentSystem

You should see "Added duplicate? false" and Bob's course count stays the same before and after. HashSet silently rejected the duplicate because Course.equals() recognized it as a match.

Seeing "Added duplicate? true"?

  • Make sure you saved Course.java with the equals() and hashCode() overrides from earlier. Without them, HashSet treats every Course object as unique.
  • Confirm your Course equals() compares courseCode, not courseName.

Why is HashSet allowing duplicate entries after I overrode equals and hashCode?

Build a course popularity counter with HashMap.merge()

You have five students, each enrolled in a set of courses. A common interview question is: how do you count how many students are in each course? HashMap.merge() handles this in a single line per entry.

  • In EnrollmentSystem.java, add this import at the top of the file with the other imports:
import java.util.Map;
  • Add this code at the end of the main method, after the duplicate enrollment test:
// --- Course Popularity with HashMap.merge() ---
System.out.println("\n=== Course Popularity (HashMap.merge) ===");
HashMap<Course, Integer> coursePopularity = new HashMap<>();
// Loop through every student's course set
for (HashSet<Course> courses : enrollments.values()) {
    for (Course c : courses) {
        // Add 1 to the count for this course
        coursePopularity.merge(c, 1, Integer::sum);
    }
}
// Print the popularity of each course
for (Map.Entry<Course, Integer> entry : coursePopularity.entrySet()) {
    System.out.println("  " + entry.getKey().getCourseName() + ": " + entry.getValue() + " students");
}

What does HashMap.merge() do?

merge(course, 1, Integer::sum) works like this: if the course is not yet in the map, insert it with a count of 1. If it already exists, apply the function Integer::sum to combine the old value with 1 (adding 1 to the existing count).

This pattern replaces the longer getOrDefault() approach and is a common interview pattern for frequency counting.

  • Save EnrollmentSystem.java and compile and run by running this command:
javac Student.java Course.java EnrollmentSystem.java && java EnrollmentSystem

You should see a "Course Popularity" section listing each course with its enrollment count. For example, Java Programming and Algorithms should each show 3 students.

Seeing a compilation error on Map.Entry?

  • Make sure you added import java.util.Map; at the top of EnrollmentSystem.java. Without it, the compiler does not know what Map.Entry refers to.
  • Check that the for-each loop uses Map.Entry<Course, Integer> with the correct generic types. A mismatch causes a type error.

Help me fix a compilation error with Map.Entry in my Java enrollment system.

✔️ Awesome, I've got everything!

Great. Double check you have saved EnrollmentSystem.java.

ⓧ I'd like to double check the full code

Scroll up and confirm your EnrollmentSystem.java has these sections in the main method, in order:

  • Imports for ArrayList, Arrays, HashMap, HashSet, and Map at the top.
  • ArrayList student roster with five students, printed with a for-each loop.
  • Four Course objects: java, dataStructures, algorithms, databases.
  • HashMap<Student, HashSet<Course>> enrollment registry with five put() calls using new HashSet<>.
  • Lookup section with HashSet<Course> aliceCourses.
  • Duplicate enrollment test getting Bob's courses and calling .add(java).
  • Course popularity counter using HashMap.merge() with a print loop.

Your enrollment system can now look up students by value, reject duplicate courses, and count enrollment popularity. Next up, you will sort students and build a ranked leaderboard using Comparable, Comparator, and TreeMap.

Sort and Rank with Comparable, Comparator, and TreeMap

In the last step, you fixed the equals/hashCode contract and used HashSet to block duplicate enrollments. Your enrollment system can now look up students by logical identity and enforce uniqueness.

Interviewers test whether you know the difference between Comparable (natural ordering baked into the class) and Comparator (external, reusable ordering). In this step, you'll implement both and use a TreeMap to build a GPA leaderboard that sorts itself automatically.

In this step, get ready to:
  • Implement Comparable<Student> for alphabetical sorting by name.
  • Sort students by GPA in descending order using a Comparator.
  • Build a GPA leaderboard with TreeMap and computeIfAbsent().
Implement Comparable and sort by name

The Comparable interface defines a class's "natural ordering." When Student implements Comparable, any sorting method knows how to compare two students without extra instructions.

  • In Student.java, find the class declaration line:
public class Student {
  • Replace it so the class implements Comparable<Student>:
public class Student implements Comparable<Student> {

The class now promises to provide a compareTo() method. You need to add that method next, or the compiler will complain.

  • Add the compareTo() method at the bottom of the Student class, after the hashCode() method and before the closing }:
    @Override
    public int compareTo(Student other) {
        return this.name.compareTo(other.name);
    }

What does this code do?

  • compareTo() returns a negative number, zero, or positive number depending on whether this student comes before, equals, or comes after the other student.
  • It delegates to String.compareTo() on the name field, so students sort alphabetically by name.
  • Save Student.java.

✔️ Awesome, I've got everything!

Great. Make sure you saved Student.java with both changes: the implements Comparable<Student> in the class declaration and the new compareTo() method.

ⓧ I'd like to double check the full code

Your Student.java file has grown across multiple steps. Scroll through it and confirm these sections are present, in order:

  • The import java.util.Objects; line at the top.
  • The class declaration: public class Student implements Comparable<Student>.
  • Three private fields (studentId, name, gpa), a constructor, and three getters.
  • The toString(), equals(), and hashCode() overrides from the previous steps.
  • The new compareTo() method at the bottom, before the closing brace.

Now you need to use this natural ordering in EnrollmentSystem.java.

  • At the top of EnrollmentSystem.java, add this import below your existing imports:
import java.util.Collections;
  • At the bottom of the main method, after the course popularity section, add this code to sort and print the roster alphabetically:
        // --- Comparable: Sort by name (natural ordering) ---
        System.out.println("\n=== Students Sorted by Name (Comparable) ===");
        Collections.sort(studentRoster);
        for (Student s : studentRoster) {
            System.out.println("  " + s);
        }

What does this code do?

  • Collections.sort(studentRoster) sorts the list using Student's natural ordering. Because you implemented Comparable, Java knows to call your compareTo() method to decide the order.
  • Without Comparable, this line would fail at compile time because Java has no way to compare two Student objects.
  • Save EnrollmentSystem.java.
  • Compile and run your program by running this command in your terminal:
javac EnrollmentSystem.java Student.java Course.java && java EnrollmentSystem

Scroll down to the new output section. You should see the five students listed in alphabetical order: Alice, Bob, Charlie, Diana, Eve.

Seeing a compile error?

  • If you see "is not abstract and does not override abstract method compareTo," make sure you added the compareTo() method inside the Student class body.
  • If you see "cannot find symbol: method sort," check that you added import java.util.Collections; at the top of EnrollmentSystem.java.
  • help me fix a compile error with Comparable in my Student Enrollment System
Sort by GPA with a Comparator

Comparable gives you one default ordering baked into the class. But what if you want to sort students by GPA instead of by name?

Comparator is a separate object that defines a custom ordering. You can create as many as you need without touching the Student class.

  • At the top of EnrollmentSystem.java, add this import below the Collections import you just added:
import java.util.Comparator;
  • At the bottom of the main method, after the Comparable sort section you just added, add this code to sort by GPA in descending order:
        // --- Comparator: Sort by GPA descending ---
        System.out.println("\n=== Students Sorted by GPA Descending (Comparator) ===");
        studentRoster.sort(Comparator.comparingDouble(Student::getGpa).reversed());
        for (Student s : studentRoster) {
            System.out.println("  " + s);
        }

What does this code do?

  • Comparator.comparingDouble(Student::getGpa) builds a comparator that extracts each student's GPA and sorts by it.
  • .reversed() flips the ordering so the highest GPA appears first.
  • studentRoster.sort() accepts any Comparator, overriding the natural ordering for this one call.

Why two sorting approaches?

Comparable is for one default ordering that belongs to the class. Comparator is for any number of custom orderings you define externally. Interviewers expect you to explain when you would use each.

  • Save EnrollmentSystem.java.
  • Compile and run your program:
javac EnrollmentSystem.java Student.java Course.java && java EnrollmentSystem

Scroll to the bottom of the output. You should see two new sections: students sorted alphabetically by name, followed by students sorted by GPA from highest (Charlie, 3.9) to lowest (Diana, 3.2).

GPA order looks wrong?

  • If students appear from lowest to highest GPA, make sure you included .reversed() at the end of the comparator chain.
  • If you see a compile error mentioning Comparator, confirm the import java.util.Comparator; line is present at the top of your file.
  • help me fix my Comparator sorting in the Student Enrollment System
Build a GPA leaderboard with TreeMap

Sorting an ArrayList is a one-time operation. If you add a new student later, the list is no longer sorted. TreeMap solves this by maintaining sorted key order automatically on every insertion.

  • At the top of EnrollmentSystem.java, add this import below your existing imports:
import java.util.TreeMap;
  • At the bottom of the main method, after the Comparator sort section, add this code to build and print the leaderboard:
        // --- TreeMap: GPA Leaderboard ---
        System.out.println("\n=== GPA Leaderboard (TreeMap) ===");
        TreeMap<Double, ArrayList<Student>> leaderboard = new TreeMap<>(Collections.reverseOrder());
        for (Student s : studentRoster) {
            leaderboard.computeIfAbsent(s.getGpa(), k -> new ArrayList<>()).add(s);
        }
        int rank = 1;
        for (Map.Entry<Double, ArrayList<Student>> entry : leaderboard.entrySet()) {
            for (Student s : entry.getValue()) {
                System.out.println("  Rank " + rank + ": " + s.getName() + " - GPA: " + entry.getKey());
                rank++;
            }
        }

What does this code do?

  • new TreeMap<>(Collections.reverseOrder()) creates a map that stores keys (GPAs) in descending order, so the highest GPA appears first.
  • computeIfAbsent(s.getGpa(), k -> new ArrayList<>()) checks if the GPA key already exists. If not, it creates a new list for that GPA. Then .add(s) adds the student to that list. This groups students who share the same GPA together.
  • The nested for-each loops iterate through the TreeMap entries (already sorted by GPA descending) and assign a rank number to each student.
  • Save EnrollmentSystem.java.

Before you run the program, what do you think the leaderboard output will look like? Which student should be ranked first?

  • Compile and run your program:
javac EnrollmentSystem.java Student.java Course.java && java EnrollmentSystem

Scroll to the very bottom of the output. You should see the GPA leaderboard with Charlie (3.9) at Rank 1, followed by Alice (3.8), Eve (3.7), Bob (3.5), and Diana (3.2).

Leaderboard not showing or ranks look off?

  • If the leaderboard prints from lowest GPA to highest, make sure you passed Collections.reverseOrder() to the TreeMap constructor.
  • If you see a compile error mentioning TreeMap, confirm the import java.util.TreeMap; line is present at the top of your file.
  • If you see an error about computeIfAbsent, double check the lambda syntax: k -> new ArrayList<>(). The arrow -> must have no spaces between the hyphen and the greater-than sign.
  • help me fix my TreeMap GPA leaderboard in the Student Enrollment System

✔️ Awesome, I've got everything!

Great. Make sure you saved both Student.java and EnrollmentSystem.java with all the changes from this step.

ⓧ I'd like to double check the full code

EnrollmentSystem.java has grown across multiple steps. Scroll through it and confirm these sections are present in order:

  • Imports at the top: ArrayList, Arrays, Collections, Comparator, HashMap, HashSet, Map, and TreeMap.
  • The ArrayList student roster with five students and the roster printout.
  • The ArrayList operations (get(0) and contains()).
  • Four Course objects and the HashMap enrollment registry with HashSet values.
  • The lookup section with lookupStudent and Alice's courses printout.
  • The HashSet duplicate enrollment test.
  • The course popularity counter with HashMap.merge().
  • The Comparable sort by name section (added in this step).
  • The Comparator sort by GPA descending section (added in this step).
  • The TreeMap GPA leaderboard section with computeIfAbsent() and rank printing (added in this step).

Your enrollment system now sorts, ranks, and organizes students with three different strategies. You have the full toolkit that interviewers test: natural ordering with Comparable, custom ordering with Comparator, and auto-sorted storage with TreeMap. Next up, tackle the Secret Mission to handle one of Java's trickiest runtime exceptions.

Secret mission

Handle ConcurrentModificationException with Safe Iteration

What happens when you try to remove elements from a collection while you're looping over it? In this mission, you'll trigger a real ConcurrentModificationException, understand why it happens, and learn the fix that interviewers expect you to know.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your project files, or delete them entirely.

Resources you used:

  • The enrollment-system project folder on your Desktop containing Student.java, Course.java, and EnrollmentSystem.java.
  • Compiled .class files generated by javac inside the same folder.

Keep everything running

No action needed. Choose this if you want to keep experimenting with the enrollment system or revisit the code later.

  • Your enrollment-system folder stays on your Desktop with all source and compiled files intact.
  • There are no running processes or background services to manage. Everything stays dormant until you compile and run again.

Pause - I'll come back to this later

Since this is a local project with no running processes, pausing is the same as keeping. Your files sit quietly on disk until you return.

  • Optionally, delete the compiled .class files to save a small amount of disk space. You can recompile from the .java source files any time.
  • Remove the compiled files on macOS/Linux by running:
cd ~/Desktop/enrollment-system
rm *.class
  • On Windows, run:
cd ~\Desktop\enrollment-system
del *.class

Delete - I don't want to use this again

Remove the entire project folder and all its contents to start fresh.

  • On macOS/Linux, delete the project folder by running:
rm -rf ~/Desktop/enrollment-system
  • On Windows, run:
rmdir /s /q %USERPROFILE%\Desktop\enrollment-system
  • Confirm the folder is gone by checking your Desktop. The enrollment-system folder should no longer appear.

Nice Work!

Nice Work!

You just built a fully working Student Enrollment System that covers the core Java Collections concepts mid-level interviewers love to test. You can now explain why each collection was chosen, because you experienced the alternative failing first.

What you learned:

  • Built proper OOP classes with encapsulation, then used ArrayList, HashMap, and HashSet to manage students, courses, and enrollments, including duplicate prevention and frequency counting with HashMap.merge().
  • Witnessed a HashMap lookup fail because equals/hashCode were missing, then fixed the contract and saw hash-based collections work correctly with custom objects.
  • Implemented Comparable for natural ordering and Comparator for custom ordering, then built an automatically sorted GPA leaderboard with TreeMap and computeIfAbsent().
  • Secret Mission: Triggered a ConcurrentModificationException, fixed it with Iterator.remove(), and learned the modern removeIf() alternative.

Ready to quiz yourself?