Java Student Management System

In the last lesson you built a Java calculator application. Now let’s build a Student Management System, a classic console app to add, view, search, update, and remove students. We will build it slowly, one method at a time, so you can type along.

🎯 What we are building

Picture a small school office. People enrol, get looked up, get their marks fixed, and leave. Each action is a small separate job. Our program does them all through a text menu.

This is a CRUD app. CRUD stands for Create, Read, Update, Delete. Almost every app sits on top of it:

  • Contacts app: create, read, update, delete contacts.
  • Shopping cart: same, but with items.
  • Learn it once here and you see it everywhere.

What our app lets the user do:

  • Add a new student (id, name, marks).
  • View all students.
  • Search for a student by id.
  • Search for a student by name.
  • Update a student’s marks.
  • Remove a student.
  • Exit.

We combine three building blocks:

  • A Student class holds one student’s data.
  • An ArrayList holds many students and resizes itself.
  • A do-while menu loop keeps showing options until the user quits.

Let’s start with the data.

🧱 Step 1: design the Student class

First we need a clear shape for what a student is. A class bundles related data into one object.

Here is the Student class with fields, a constructor, getters, a setter for marks, and a toString.

class Student {
private int id; // unique number for each student
private String name;
private double marks;
public Student(int id, String name, double marks) {
this.id = id;
this.name = name;
this.marks = marks;
}
public int getId() { return id; }
public String getName() { return name; }
public double getMarks() { return marks; }
public void setMarks(double marks) { // βœ… marks can change, id and name stay fixed
this.marks = marks;
}
@Override
public String toString() {
return "ID " + id + " | " + name + " | marks: " + marks;
}
}

The parts:

  • The three fields are private. That is encapsulation: outside code cannot change them directly. It must go through our methods.
  • The constructor takes id, name, and marks and stores them. Every Student starts with all three set.
  • The getters let other code read the values safely.
  • There is a setMarks but no setId or setName. An id and name should never change. Only marks can, so only marks gets a setter.
  • toString builds one tidy line. Java calls it automatically when we print a Student, so the list prints nicely.

The id matters. Two students can share a name. An id is unique, so it points at exactly one record. We use it for updating and removing.

πŸ“¦ Step 2: store students in an ArrayList

We need a container whose size changes as people are added and removed. A plain array has a fixed size, so it is a poor fit. An ArrayList resizes itself.

This line creates the list that holds every student.

import java.util.ArrayList;
ArrayList<Student> students = new ArrayList<>();

What to notice:

  • ArrayList<Student> means β€œa list that holds Student objects only.” The compiler blocks the wrong type.
  • It starts empty. We add and remove as the user works. We never set a size.
  • We keep it in main and pass it to each method, so every part shares the same list.

🍽️ Step 3: the menu loop

The heart of the app is a menu that shows again and again until the user exits. A do-while loop fits, because the menu should show at least once before we check whether to stop.

Here is the skeleton of main. The operations are method calls for now. We write each one next.

import java.util.ArrayList;
import java.util.Scanner;
public class StudentManagementSystem {
public static void main(String[] args) {
ArrayList<Student> students = new ArrayList<>();
Scanner scanner = new Scanner(System.in);
int choice;
do {
printMenu();
choice = readInt(scanner, "Choose an option: ");
switch (choice) {
case 1: addStudent(students, scanner); break;
case 2: viewAll(students); break;
case 3: searchById(students, scanner); break;
case 4: searchByName(students, scanner); break;
case 5: updateMarks(students, scanner); break;
case 6: removeStudent(students, scanner); break;
case 7: System.out.println("Goodbye!"); break;
default: System.out.println("Invalid option. Pick 1 to 7.");
}
} while (choice != 7);
scanner.close();
}
}

Each time around the loop:

  • printMenu() shows the options.
  • readInt reads the choice as a number. A small helper keeps input handling in one place.
  • The switch calls the matching method. Each method gets the students list and the scanner.
  • The loop runs until choice is 7, then stops and closes the scanner.

Notice how main says what happens, not how. Each β€œhow” lives in its own method: one method, one job.

Here is printMenu, just a block of prints.

static void printMenu() {
System.out.println("\n--- Student Management ---");
System.out.println("1. Add student");
System.out.println("2. View all students");
System.out.println("3. Search by id");
System.out.println("4. Search by name");
System.out.println("5. Update marks");
System.out.println("6. Remove student");
System.out.println("7. Exit");
}

Now we build the operations one at a time.

βž• Step 4a: add a student

Read an id, a name, and marks, then create a Student and add it to the list.

static void addStudent(ArrayList<Student> students, Scanner scanner) {
int id = readInt(scanner, "Enter id: ");
// ❌ Guard against a duplicate id before adding
if (findById(students, id) != null) {
System.out.println("An id like that already exists. Try another.");
return;
}
System.out.print("Enter name: ");
String name = scanner.nextLine();
double marks = readDouble(scanner, "Enter marks: ");
students.add(new Student(id, name, marks)); // βœ… create the object, store it
System.out.println("Student added.");
}

The parts:

  • We read the id first and check it is not taken. A duplicate id would break searching and updating later.
  • findById (written below) returns the matching student or null. null means β€œno match.”
  • We read the name with nextLine, then the marks with another helper.
  • students.add(new Student(...)) builds and stores the object in one line.
  • return after the duplicate message ends the method early, so we never add a bad record.

πŸ“‹ Step 4b: view all students

Viewing is the β€œRead” in CRUD. Loop over the list and print each student. Handle the empty case so the user sees a message instead of blank space.

static void viewAll(ArrayList<Student> students) {
if (students.isEmpty()) {
System.out.println("No students yet.");
return;
}
System.out.println("All students:");
for (Student s : students) {
System.out.println("- " + s); // βœ… toString runs automatically
}
}

The parts:

  • students.isEmpty() catches the empty list. We print a message and return early.
  • The for-each loop visits each Student.
  • Printing s calls our toString, so each line comes out neatly. No digging out fields by hand.

πŸ”Ž Step 4c: search by id and by name

Searching is where the id pays off:

  • An id is unique, so searching by id finds at most one match.
  • A name can repeat, so searching by name might find several. We collect all matches.

First, a shared helper that finds a student by id. Many methods reuse it.

static Student findById(ArrayList<Student> students, int id) {
for (Student s : students) {
if (s.getId() == id) {
return s; // βœ… found it, hand it back
}
}
return null; // ❌ no match, signal "nothing found"
}

This helper returns the first student whose id matches, or null if none match. null is the common Java way to say β€œfound nothing.” The caller checks for it and reacts.

Now search by id, which uses that helper.

static void searchById(ArrayList<Student> students, Scanner scanner) {
int id = readInt(scanner, "Enter id to find: ");
Student found = findById(students, id);
if (found == null) {
System.out.println("No student with id " + id + ".");
} else {
System.out.println("Found: " + found);
}
}

The parts:

  • Read an id from the user.
  • Call findById, which gives back a Student or null.
  • null means tell the user nothing was found. Otherwise print the match.

Search by name is different. Names can repeat, so we check every student and print each match.

static void searchByName(ArrayList<Student> students, Scanner scanner) {
System.out.print("Enter name to find: ");
String target = scanner.nextLine();
boolean found = false;
for (Student s : students) {
// βœ… equalsIgnoreCase compares text and ignores capitals
if (s.getName().equalsIgnoreCase(target)) {
System.out.println("Found: " + s);
found = true;
}
}
if (!found) {
System.out.println("No student named " + target + ".");
}
}

The parts:

  • We compare names with equalsIgnoreCase, not ==. == checks whether two variables point to the same object, which we do not want. equalsIgnoreCase checks the text and treats β€œalex” and β€œAlex” as a match.
  • A found flag starts false and flips to true on any match. If it is still false after the loop, nothing matched.

✏️ Step 4d: update a student’s marks

Updating is the β€œU” in CRUD. Find the student by id, then change marks through setMarks. This is why the field is private with a setter: the change goes through one controlled door.

static void updateMarks(ArrayList<Student> students, Scanner scanner) {
int id = readInt(scanner, "Enter id to update: ");
Student s = findById(students, id);
if (s == null) {
System.out.println("No student with id " + id + ".");
return;
}
double newMarks = readDouble(scanner, "Enter new marks: ");
s.setMarks(newMarks); // βœ… update through the setter
System.out.println("Updated: " + s);
}

The parts:

  • Read the id and reuse findById. No need to write the loop again.
  • If no student is found, say so and return early.
  • Otherwise read the new marks and call setMarks. s points to the real object in the list, so the list updates too. There is one object, and we hold a handle to it.

πŸ—‘οΈ Step 4e: remove a student

Removing is the β€œD” in CRUD. Delete the student whose id matches, then check whether anything was actually removed so we give honest feedback.

static void removeStudent(ArrayList<Student> students, Scanner scanner) {
int id = readInt(scanner, "Enter id to remove: ");
// removeIf deletes every element that matches the test
boolean removed = students.removeIf(s -> s.getId() == id);
if (removed) {
System.out.println("Student removed.");
} else {
System.out.println("No student with id " + id + ".");
}
}

The parts:

  • removeIf removes any element where the test is true. Here the test is β€œthis student’s id equals the id the user typed.”
  • removeIf returns true if it removed anything, false if not. We use that to report honestly instead of always claiming success.

⌨️ Step 5: input handling and the Scanner pitfall

Reading from a Scanner has one trap that catches almost everyone:

  • nextInt() reads the number but leaves the Enter key (a newline) in the input.
  • The next nextLine() reads that leftover newline and returns an empty string.
  • It looks like your program skipped a question.

Small helper methods read a number cleanly and fix this in one place.

static int readInt(Scanner scanner, String prompt) {
System.out.print(prompt);
while (!scanner.hasNextInt()) { // ❌ guard against non-number input
scanner.next(); // throw away the bad token
System.out.print("Please enter a whole number. " + prompt);
}
int value = scanner.nextInt();
scanner.nextLine(); // βœ… clear the leftover newline
return value;
}
static double readDouble(Scanner scanner, String prompt) {
System.out.print(prompt);
while (!scanner.hasNextDouble()) {
scanner.next();
System.out.print("Please enter a number. " + prompt);
}
double value = scanner.nextDouble();
scanner.nextLine(); // βœ… clear the leftover newline
return value;
}

Why these helpers are worth it:

  • The scanner.nextLine() after reading a number clears the leftover newline. Inside the helper, we never forget it.
  • The while (!scanner.hasNextInt()) loop checks the input is a number first. Type β€œabc” and we throw it away and ask again, instead of crashing.
  • Every place that needs a number calls readInt or readDouble, so input handling stays consistent.

The classic Scanner gotcha

After nextInt() or nextDouble(), a newline stays in the input. The next nextLine() reads it as an empty string and seems to skip a question. The fix is a single scanner.nextLine() right after reading the number. Keeping that fix inside a helper method means you write it once and never miss it.

🧩 The finished structure

Putting it together, your file has this shape. The Student class sits beside StudentManagementSystem, and main calls the operation methods.

StudentManagementSystem.java
β”œβ”€β”€ class Student
β”‚ β”œβ”€β”€ fields: id, name, marks
β”‚ β”œβ”€β”€ constructor
β”‚ β”œβ”€β”€ getId / getName / getMarks
β”‚ β”œβ”€β”€ setMarks
β”‚ └── toString
└── class StudentManagementSystem
β”œβ”€β”€ main (the do-while menu loop + switch)
β”œβ”€β”€ printMenu
β”œβ”€β”€ addStudent
β”œβ”€β”€ viewAll
β”œβ”€β”€ searchById
β”œβ”€β”€ searchByName
β”œβ”€β”€ updateMarks
β”œβ”€β”€ removeStudent
β”œβ”€β”€ findById (shared helper)
β”œβ”€β”€ readInt (input helper)
└── readDouble (input helper)

Each method does one job, main ties them together, and Student holds the data. That separation makes the program easy to read and grow.

▢️ Step 6: a full sample run

Let’s run it end to end: add two students, view, search, update marks, remove one, and exit.

Output

--- Student Management ---
1. Add student
2. View all students
3. Search by id
4. Search by name
5. Update marks
6. Remove student
7. Exit
Choose an option: 1
Enter id: 101
Enter name: Alex
Enter marks: 78
Student added.
--- Student Management ---
Choose an option: 1
Enter id: 102
Enter name: Riya
Enter marks: 91
Student added.
--- Student Management ---
Choose an option: 2
All students:
- ID 101 | Alex | marks: 78.0
- ID 102 | Riya | marks: 91.0
--- Student Management ---
Choose an option: 4
Enter name to find: riya
Found: ID 102 | Riya | marks: 91.0
--- Student Management ---
Choose an option: 5
Enter id to update: 101
Enter new marks: 85
Updated: ID 101 | Alex | marks: 85.0
--- Student Management ---
Choose an option: 6
Enter id to remove: 102
Student removed.
--- Student Management ---
Choose an option: 2
All students:
- ID 101 | Alex | marks: 85.0
--- Student Management ---
Choose an option: 7
Goodbye!

Each option maps to one method, and the same students list carries data between choices. Add changes it, view reads it, update edits in place, remove takes one out. That is CRUD running live.

πŸ› οΈ Practice Challenge

Try extending the project before checking the answers. Each builds on a method you already have.

Challenge 1: Add a menu option that prints the class average marks. Handle the empty list so you never divide by zero.

Challenge 2: Add an option that finds and prints the top student, the one with the highest marks.

⚠️ Common Mistakes

A few project slip-ups to watch for.

  • Forgetting to clear the Scanner newline. After nextInt() or nextDouble(), add scanner.nextLine() before reading a String, or the text input gets skipped. Putting it in a helper solves it once.
  • Not handling an empty list. Viewing, searching, averaging, or finding the top student with no records should show a friendly message, not blank output or a crash.
  • Comparing names with ==. Use .equals() or .equalsIgnoreCase() to compare the text. == checks whether two variables point to the same object, which is rarely what you mean for text.
  • Letting duplicate ids in. If two students share an id, search, update, and remove get confused about which one you mean. Check for a duplicate id before adding.
  • Assuming a remove always worked. removeIf might match nothing. Check its true/false result and tell the user honestly.

βœ… Best Practices

Habits that make console projects easy to read and grow.

  • Use a class for your data. A Student class keeps each record’s fields together and protects them with encapsulation.
  • Use an ArrayList for the collection. It resizes as records are added and removed, so you never manage a fixed size.
  • Give each operation its own method. addStudent, viewAll, searchById, and the rest each do one job. main stays short and readable.
  • Share helpers, do not repeat code. findById, readInt, and readDouble are written once and reused everywhere.
  • Validate input and guard edge cases. Handle empty lists, non-number input, duplicate ids, and the Scanner newline issue from the start.

🧩 What You’ve Learned

Nicely done. You built a real, complete app from the ground up. Let’s recap.

  • βœ… A Student Management System is a CRUD app: Create, Read, Update, Delete.
  • βœ… A Student class holds each record’s data (id, name, marks) using encapsulation, a constructor, getters, a setter, and toString.
  • βœ… An ArrayList stores the students and resizes as they are added or removed.
  • βœ… A do-while menu loop with a switch drives the whole program, calling one method per option.
  • βœ… A shared findById helper powers search, update, and remove without repeating the loop.
  • βœ… Real touches matter: clearing the Scanner newline, validating input, guarding empty lists, blocking duplicate ids, and comparing names with equalsIgnoreCase.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    Why use an ArrayList to store the students?

    Why: An ArrayList resizes automatically, which suits a list that grows and shrinks.

  2. 2

    Which loop is ideal for a menu that must show at least once?

    Why: A do-while runs its body before checking, so the menu always shows at least once.

  3. 3

    Why call scanner.nextLine() after nextInt()?

    Why: nextInt() leaves a newline that would make the next nextLine() read an empty string.

  4. 4

    How should you compare a searched name to a student's name?

    Why: Use equals/equalsIgnoreCase to compare the text; == compares references.

πŸš€ What’s Next?

Next we build another real project that organises records and applies CRUD on a larger scale. Let’s build it.

Java Library Management System

Share & Connect