Project Based Learning in Java (24CSH-301 / 24ITH-301) - Lab 3

Experiment 1.3: Input Validation, ATM Withdrawal System, and University Enrolment

Develop Java programs with exception handling for user input validation, ATM systems, and university enrolment management.

Aim Design Java programs showcasing exception handling through square root calculations, an ATM withdrawal system, and a university enrolment system with custom exceptions.
Objectives To learn about the concept of inheritance, and about abstract classes and exception handling.
Mapped CO CO2, CO3

Lab Brief

What You Need To Build

Easy Level

Square Root Calculator

Write a Java program to calculate the square root of a number entered by the user. Use try-catch to handle invalid inputs (for example, negative numbers or non-numeric values).

  • Read the value with Scanner.nextDouble().
  • throw new IllegalArgumentException for a negative number.
  • Catch the specific type before the general one.
  • Close the scanner inside finally.
Medium Level

ATM Withdrawal System

Write a Java program to simulate an ATM withdrawal system. Ask for a PIN, allow the withdrawal only if the PIN is correct and the balance is sufficient, throw exceptions otherwise, and always show the remaining balance even if an exception occurs.

  • Write two custom checked exceptions.
  • Keep CORRECT_PIN = 1234 and an opening balance of 3000.0.
  • Handle both custom types with one multi-catch clause.
  • finally prints the balance on every path.
Hard Level

University Enrolment System

Create a Java program for a university enrolment system with exception handling. Throw a CourseFullException when the maximum enrolment limit is reached, and a PrerequisiteNotMetException when the student has not completed the prerequisite course.

  • Read enrolment requests with Scanner.
  • Student tracks completed courses in a Set<String>.
  • Course.enrollStudent(Student) declares both exceptions with throws.
  • Use finally to show seats left after every attempt.
Input/Apparatus used: Hardware - minimum 384 MB RAM, 100 GB hard disk. Software - JDK with any Java IDE such as Eclipse, NetBeans, or IntelliJ IDEA, or a plain terminal with javac and java.

From the Lab Manual

Reading Material

Abstract Classes and Methods

Data abstraction is the process of hiding certain details and showing only essential information to the user.

Abstraction can be achieved with either abstract classes or interfaces. The abstract keyword is a non-access modifier, used for classes and methods:

  • Abstract class - a restricted class that cannot be used to create objects. To access it, it must be inherited from another class.
  • Abstract method - can only be used in an abstract class, and it does not have a body. The body is provided by the subclass.

An abstract class can have both abstract and regular methods:

abstract class Animal {
    public abstract void animalSound();

    public void sleep() {
        System.out.println("Zzz");
    }
}

From the example above, it is not possible to create an object of the Animal class:

Animal myObj = new Animal();   // will generate an error

To access the abstract class, it must be inherited from another class. Remember from the Inheritance chapter that we use the extends keyword to inherit from a class:

// Abstract class
abstract class Animal {
    // Abstract method (does not have a body)
    public abstract void animalSound();

    // Regular method
    public void sleep() {
        System.out.println("Zzz");
    }
}

// Subclass (inherit from Animal)
class Pig extends Animal {
    public void animalSound() {
        // The body of animalSound() is provided here
        System.out.println("The pig says: wee wee");
    }
}

class Main {
    public static void main(String[] args) {
        Pig myPig = new Pig();   // Create a Pig object
        myPig.animalSound();
        myPig.sleep();
    }
}

Why and when to use abstract classes and methods? To achieve security - hide certain details and only show the important details of an object.

Java Exceptions - Try...Catch

When executing Java code, different errors can occur: coding errors made by the programmer, errors due to wrong input, or other unforeseeable things.

When an error occurs, Java will normally stop and generate an error message. The technical term for this is: Java will throw an exception.

The try statement allows you to define a block of code to be tested for errors while it is being executed. The catch statement allows you to define a block of code to be executed if an error occurs in the try block. The try and catch keywords come in pairs:

try {
    // Block of code to try
}
catch (Exception e) {
    // Block of code to handle errors
}

The following statement attempts to access an invalid array index and therefore throws an ArrayIndexOutOfBoundsException:

public class Main {
    public static void main(String[] args) {
        int[] myNumbers = {1, 2, 3};
        System.out.println(myNumbers[10]);   // error!
    }
}

Sample output:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 10
        at Main.main(Main.java:4)

A try-catch block can catch the exception and execute suitable handling code:

public class Main {
    public static void main(String[] args) {
        try {
            int[] myNumbers = {1, 2, 3};
            System.out.println(myNumbers[10]);
        } catch (Exception e) {
            System.out.println("Something went wrong.");
        }
    }
}

Sample output:

Something went wrong.

Finally

The finally statement lets you execute code after try...catch, regardless of the result:

public class Main {
    public static void main(String[] args) {
        try {
            int[] myNumbers = {1, 2, 3};
            System.out.println(myNumbers[10]);
        } catch (Exception e) {
            System.out.println("Something went wrong.");
        } finally {
            System.out.println("The 'try catch' is finished.");
        }
    }
}

Sample output:

Something went wrong.
The 'try catch' is finished.

The throw Keyword

The throw statement allows you to create a custom error. It is used together with an exception type. There are many exception types available in Java: ArithmeticException, FileNotFoundException, ArrayIndexOutOfBoundsException, SecurityException, and so on.

Throw an exception if age is below 18 (print "Access denied"). If age is 18 or older, print "Access granted":

public class Main {
    static void checkAge(int age) {
        if (age < 18) {
            throw new IllegalArgumentException("Access denied - You must be at least 18 years old.");
        } else {
            System.out.println("Access granted - You are old enough!");
        }
    }

    public static void main(String[] args) {
        checkAge(15);   // Set age to 15 (which is below 18...)
    }
}

Sample output:

Exception in thread "main" java.lang.IllegalArgumentException: Access denied - You must be at least 18 years old.
        at Main.checkAge(Main.java:4)
        at Main.main(Main.java:12)

If age was 20, you would not get an exception:

checkAge(20);

Sample output:

Access granted - You are old enough!
Why abstraction and exceptions sit in the same experiment: both are about contracts that the compiler enforces for you. An abstract method is a promise that every subclass will supply the behaviour; a throws clause is a promise that every caller will deal with the failure. In each case the compiler refuses to build the program until the promise is kept.

Before Coding

Prerequisite Concepts

In Experiments 1.1 and 1.2 a bad input simply crashed the program. This experiment is about the opposite: the program stays in control and decides what happens when something goes wrong. Revise these ideas before writing any code.

The Throwable Hierarchy

Everything Java can throw descends from Throwable. Below it, Error covers problems your program should not try to recover from, while Exception covers the ones it should. All three custom types in this experiment are built under Exception.

Throwable
  +-- Error                  // OutOfMemoryError - do not catch
  +-- Exception              // recoverable
        +-- IOException      // checked
        +-- RuntimeException // unchecked
              +-- ArithmeticException
              +-- IllegalArgumentException

Checked vs Unchecked

A checked exception must be declared with throws or caught - the compiler refuses to build the program otherwise. An unchecked one (anything under RuntimeException) carries no such obligation. That single choice decides how a custom exception behaves.

class InvalidPinException extends Exception { }
// checked: callers are forced to handle it

class BadInput extends RuntimeException { }
// unchecked: the compiler stays silent

try, catch, finally

try holds the code that might fail, catch handles the failure, and finally runs afterwards on every path - normal completion, handled exception, even an uncaught one. Cleanup belongs there and nowhere else.

try {
    risky();
} catch (Exception e) {
    System.out.println(e.getMessage());
} finally {
    scanner.close();   // always runs
}

Catch Order and Multi-Catch

Clauses are tested top to bottom, so the specific type must come before the general one; putting catch (Exception e) first makes every later clause unreachable and is a compile error. When two unrelated types need the same handling, join them with |.

catch (IllegalArgumentException e) { ... }  // specific first
catch (Exception e) { ... }                 // general last

catch (InvalidPinException | InsufficientBalanceException e) { ... }

throw vs throws

throw is a statement: it raises one exception object, right now. throws is part of a method signature: it warns callers that this method may fail in that way. One does the raising, the other does the declaring.

public void enrollStudent(Set<String> done)
        throws CourseFullException {      // declares
    if (full) {
        throw new CourseFullException(msg);  // raises
    }
}

Writing a Custom Exception

A custom exception is an ordinary class that extends Exception. Its constructor takes a message and hands it to super(message), which stores it in the inherited Throwable so that getMessage() can return it later. The class name itself is half the documentation.

class CourseFullException extends Exception {
    public CourseFullException(String message) {
        super(message);   // hand it to Throwable
    }
}

Scanner and InputMismatchException

Typing abc at a nextDouble() prompt does not return zero - it throws InputMismatchException, an unchecked exception from java.util. Since it is not an IllegalArgumentException, it falls through to the general catch (Exception e) clause.

double n = scanner.nextDouble();
// user types: abc
// -> java.util.InputMismatchException
// caught by catch (Exception e)

getMessage() and Cleanup

e.getMessage() returns exactly the string that was passed to the constructor, which is why a well-written message is worth the effort. And because a thrown exception skips the rest of the try block, scanner.close() has to sit in finally or it will be missed on the failure path.

throw new InvalidPinException("Error: Invalid PIN. Please try again.");
...
catch (InvalidPinException e) {
    System.out.println(e.getMessage());
    // prints: Error: Invalid PIN. Please try again.
}

Easy Level

Square Root Calculator: Validating User Input

Problem statement: Write a Java program to calculate the square root of a number entered by the user. Use try-catch to handle invalid inputs (e.g., negative numbers or non-numeric values).

What Each Block Does

Block Responsibility
try Wraps the read, the validation, and the calculation - everything that can fail
if (number < 0) throw Raises IllegalArgumentException with the message the user will see
catch (IllegalArgumentException e) Specific clause - prints e.getMessage(), the exact string given to the constructor
catch (Exception e) General safety net - catches InputMismatchException and anything else
finally Closes the scanner on all three paths

Which Input Triggers What

Input What happens
625 No exception - prints Square root: 25.0
-49 Your own throw fires - caught by the first clause
abc Scanner throws InputMismatchException - caught by the second clause
Try, catch, and finally paths in the square root program try { scanner.nextDouble() } read the number Not a valid number? Scanner raises InputMismatchException Yes No catch (Exception e) general safety net - second clause in the code number < 0 ? your own guard clause throw No catch (IllegalArgumentException e) specific - first clause in the code Math.sqrt(number) prints Square root: 25.0 finally { scanner.close(); } runs on every one of the three paths above
Figure 1. Three ways out of the try block, one shared finally block.

Algorithm

  1. Start the program and create a Scanner object.
  2. Open a try block and prompt for a number, reading it with nextDouble().
  3. If the number is negative, throw new IllegalArgumentException with an explanatory message.
  4. Otherwise compute Math.sqrt(number) and print the result.
  5. Catch IllegalArgumentException first and print e.getMessage().
  6. Catch Exception next to cover non-numeric input, and print the invalid-input message.
  7. Close the scanner in finally so it happens on every path, then end the program.

Java Program

import java.util.Scanner;

public class SquareRootCalculator {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        try {
            // Taking user input
            System.out.print("Enter a number: ");
            double number = scanner.nextDouble();

            // Handling negative numbers
            if (number < 0) {
                throw new IllegalArgumentException("Error: Cannot calculate the square root of a negative number.");
            }

            // Calculating square root
            double result = Math.sqrt(number);
            System.out.println("Square root: " + result);
        } catch (IllegalArgumentException e) {
            // Specific clause: must come before catch (Exception e)
            System.out.println(e.getMessage());
        } catch (Exception e) {
            // Reached when Scanner throws InputMismatchException
            System.out.println("Error: Invalid input. Please enter a valid number.");
        } finally {
            scanner.close();
        }
    }
}

Sample Output

Run 1 - a valid number:

Enter a number: 625
Square root: 25.0

Run 2 - a negative number (your own throw fires):

Enter a number: -49
Error: Cannot calculate the square root of a negative number.

Run 3 - non-numeric input (Scanner throws):

Enter a number: abc
Error: Invalid input. Please enter a valid number.
Lab note: Swapping the two catch clauses does not just change the message - it stops the program compiling. Java tests clauses in order, so with catch (Exception e) first the IllegalArgumentException clause could never be reached, and the compiler reports it as unreachable code. Always order catch blocks from the most specific type to the most general.

Medium Level

ATM Withdrawal System: Custom Exceptions and Multi-Catch

Problem statement: Write a Java program to simulate an ATM withdrawal system. The program should ask the user to enter their PIN, allow the withdrawal if the PIN is correct and the balance is sufficient, throw exceptions for an invalid PIN or insufficient balance, and ensure the system always shows the remaining balance, even if an exception occurs.

Class Responsibilities

Class Responsibility
InvalidPinException Checked exception - extends Exception, passes its message to super
InsufficientBalanceException Checked exception with the same shape, raised when the amount exceeds the balance
ATMWithdrawalSystem Holds CORRECT_PIN and balance as static members, runs both checks, handles both exceptions

The Two Validation Gates

Situation Result
PIN is not 1234 InvalidPinException - the amount is never even asked for
Amount is greater than the balance InsufficientBalanceException - the balance is left untouched
Something non-numeric is typed InputMismatchException - handled by the general clause
Every case above, plus success finally prints Current Balance: and closes the scanner
ATM withdrawal validation gates and the guaranteed finally block Enter PIN scanner.nextInt() Gate 1: PIN != 1234 ? checked before anything else Yes No throw new InvalidPinException Error: Invalid PIN. Please try again. Withdraw Amount scanner.nextDouble() Gate 2: amount > balance ? balance is not changed yet Yes No throw new InsufficientBalanceException balance -= amount Withdrawal Successful! catch (InvalidPinException | InsufficientBalanceException e) finally prints Current Balance on every path, then closes the scanner
Figure 2. Two gates in series; whichever one fails, the finally block still reports the balance.

Algorithm

  1. Declare CORRECT_PIN as a static final int and balance as a static double set to 3000.0.
  2. Write InvalidPinException and InsufficientBalanceException, each extending Exception and passing its message to super.
  3. Inside a try block, read the PIN with nextInt().
  4. If the PIN does not match, throw new InvalidPinException - the amount is never requested.
  5. Read the withdrawal amount with nextDouble().
  6. If the amount exceeds the balance, throw new InsufficientBalanceException and leave the balance untouched.
  7. Otherwise subtract the amount and print the success message with the new balance.
  8. Handle both custom types in a single multi-catch clause and print e.getMessage().
  9. Add a general catch (Exception e) for non-numeric input.
  10. In finally, print Current Balance: and close the scanner.

Java Program

import java.util.Scanner;

// Custom exception for invalid PIN
class InvalidPinException extends Exception {
    public InvalidPinException(String message) {
        super(message);   // hand the message to Throwable so getMessage() can return it
    }
}

// Custom exception for insufficient balance
class InsufficientBalanceException extends Exception {
    public InsufficientBalanceException(String message) {
        super(message);
    }
}

public class ATMWithdrawalSystem {

    private static final int CORRECT_PIN = 1234;   // Predefined PIN
    private static double balance = 3000.0;        // Initial balance

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        try {
            // Asking user to enter PIN
            System.out.print("Enter PIN: ");
            int enteredPin = scanner.nextInt();

            // Validate PIN
            if (enteredPin != CORRECT_PIN) {
                throw new InvalidPinException("Error: Invalid PIN. Please try again.");
            }

            // Asking user for withdrawal amount
            System.out.print("Withdraw Amount: ");
            double withdrawAmount = scanner.nextDouble();

            // Check for sufficient balance
            if (withdrawAmount > balance) {
                throw new InsufficientBalanceException("Error: Insufficient balance.");
            }

            // Perform withdrawal
            balance -= withdrawAmount;
            System.out.println("Withdrawal Successful! Remaining Balance: " + balance);

        } catch (InvalidPinException | InsufficientBalanceException e) {
            // Multi-catch: one clause, two unrelated exception types
            System.out.println(e.getMessage());
        } catch (Exception e) {
            System.out.println("Error: Invalid input. Please enter numeric values.");
        } finally {
            // Runs whether the withdrawal succeeded or an exception was thrown
            System.out.println("Current Balance: " + balance);
            scanner.close();
        }
    }
}

Sample Output

Run 1 - correct PIN, sufficient balance:

Enter PIN: 1234
Withdraw Amount: 1200
Withdrawal Successful! Remaining Balance: 1800.0
Current Balance: 1800.0

Run 2 - wrong PIN (gate 1 fails, the amount is never asked for):

Enter PIN: 9999
Error: Invalid PIN. Please try again.
Current Balance: 3000.0

Run 3 - correct PIN, amount larger than the balance:

Enter PIN: 1234
Withdraw Amount: 5000
Error: Insufficient balance.
Current Balance: 3000.0
Why the balance is still correct after a failure: the subtraction balance -= withdrawAmount sits after the check, so when the exception is thrown the rest of the try block is abandoned and the field is never touched. The finally block then reports the untouched value. Validate first, mutate second - if the order were reversed, a failed withdrawal would still drain the account.

Hard Level

University Enrolment System: User Input, Capacity, and Prerequisite Checks

Problem statement: Create a Java program for a university enrolment system with exception handling. The program should read enrolment requests from the user, throw a CourseFullException when the maximum enrolment limit is reached, and throw a PrerequisiteNotMetException when the student has not completed the prerequisite course.

Class and Method Design

Class / Method Purpose
CourseFullException(String message) Checked exception thrown when no seats are left in the course.
PrerequisiteNotMetException(String message) Checked exception thrown when the student has not completed the required prerequisite.
Student.completeCourse(String courseName) Adds a completed course to the student's Set<String>.
Student.hasCompletedCourse(String courseName) Returns true if the prerequisite exists in the completed-course set.
Course.enrollStudent(Student student) Checks capacity first, checks prerequisite second, then records the enrolment.
readEnrollmentCount(Scanner) Reads and validates the number of enrolment requests.
readStudent(Scanner, int) Reads student id, name, and whether the prerequisite course is completed.
attemptEnrollment(Course, Student) Calls enrollStudent inside try, catches both custom exceptions, and prints seats left in finally.

Exception Triggers

Condition inside enrollStudent Exception thrown
enrolledStudents.size() >= maxEnrollment CourseFullException
Prerequisite is set and student.hasCompletedCourse(...) is false PrerequisiteNotMetException
Both checks pass No exception; the student is added to the enrolment list.

Flow Chart

Flow chart for user-driven university enrolment exception handling Start Create course and read total students Scanner input More students? loop until count ends No End Yes Read student id, name, prerequisite status Build Student object Course full? size >= maxEnrollment Yes Throw CourseFullException No Prerequisite missing? not in completedCourses Yes Throw Prerequisite NotMetException No Add student and print success catch and finally
Figure 3. User input creates each enrolment request; failed attempts are caught and do not consume a seat.

Algorithm

  1. Define CourseFullException and PrerequisiteNotMetException as checked exceptions by extending Exception.
  2. Create a Student class with student id, name, and a Set<String> for completed courses.
  3. Add completeCourse and hasCompletedCourse methods in Student.
  4. Create a Course class with course code, course name, maximum enrolment limit, prerequisite course, and an enrolled-student list.
  5. Declare enrollStudent(Student student) with throws CourseFullException, PrerequisiteNotMetException.
  6. In enrollStudent, throw CourseFullException if the enrolled-student list has already reached the maximum limit.
  7. If seats are available, check the prerequisite. Throw PrerequisiteNotMetException when the prerequisite is required but absent from the student's completed-course set.
  8. Read the number of enrolment requests from the user.
  9. For each request, read student id, student name, and whether the student has completed the prerequisite course.
  10. Call attemptEnrollment for every student. It calls enrollStudent inside try, catches both custom exceptions in one multi-catch block, and prints available seats in finally.
  11. Close the scanner in finally after all enrolment requests have been processed.

Java Program

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Scanner;
import java.util.Set;

class CourseFullException extends Exception {
    public CourseFullException(String message) {
        super(message);
    }
}

class PrerequisiteNotMetException extends Exception {
    public PrerequisiteNotMetException(String message) {
        super(message);
    }
}

class Student {
    private final String studentId;
    private final String name;
    private final Set<String> completedCourses;

    public Student(String studentId, String name) {
        this.studentId = studentId;
        this.name = name;
        this.completedCourses = new HashSet<>();
    }

    public void completeCourse(String courseName) {
        completedCourses.add(courseName);
    }

    public boolean hasCompletedCourse(String courseName) {
        return completedCourses.contains(courseName);
    }

    public String getName() {
        return name;
    }

    public String getStudentId() {
        return studentId;
    }
}

class Course {
    private final String courseCode;
    private final String courseName;
    private final int maxEnrollment;
    private final String prerequisiteCourse;
    private final List<Student> enrolledStudents;

    public Course(String courseCode, String courseName,
                  int maxEnrollment, String prerequisiteCourse) {
        this.courseCode = courseCode;
        this.courseName = courseName;
        this.maxEnrollment = maxEnrollment;
        this.prerequisiteCourse = prerequisiteCourse;
        this.enrolledStudents = new ArrayList<>();
    }

    public void enrollStudent(Student student)
            throws CourseFullException, PrerequisiteNotMetException {

        if (enrolledStudents.size() >= maxEnrollment) {
            throw new CourseFullException(courseCode + " - " + courseName
                    + " is full. Maximum enrolment limit is " + maxEnrollment + ".");
        }

        if (prerequisiteCourse != null
                && !prerequisiteCourse.trim().isEmpty()
                && !student.hasCompletedCourse(prerequisiteCourse)) {
            throw new PrerequisiteNotMetException(student.getName()
                    + " must complete " + prerequisiteCourse
                    + " before enrolling in " + courseName + ".");
        }

        enrolledStudents.add(student);
        System.out.println(student.getName() + " (" + student.getStudentId()
                + ") enrolled in " + courseName + ".");
    }

    public String getCourseName() {
        return courseName;
    }

    public String getPrerequisiteCourse() {
        return prerequisiteCourse;
    }

    public int getAvailableSeats() {
        return maxEnrollment - enrolledStudents.size();
    }
}

public class UniversityEnrollmentSystem {
    private static int readEnrollmentCount(Scanner scanner) {
        while (true) {
            System.out.print("How many students want to enroll? ");
            try {
                int count = Integer.parseInt(scanner.nextLine().trim());
                if (count > 0) {
                    return count;
                }
                System.out.println("Enter a number greater than zero.");
            } catch (NumberFormatException e) {
                System.out.println("Enter a valid whole number.");
            }
        }
    }

    private static boolean readYesNo(Scanner scanner, String prompt) {
        while (true) {
            System.out.print(prompt);
            String answer = scanner.nextLine().trim().toLowerCase();
            if (answer.equals("yes") || answer.equals("y")) {
                return true;
            }
            if (answer.equals("no") || answer.equals("n")) {
                return false;
            }
            System.out.println("Please type yes or no.");
        }
    }

    private static Student readStudent(Scanner scanner, int studentNumber, Course course) {
        System.out.println("\nStudent " + studentNumber + " details");
        System.out.print("Enter student id: ");
        String id = scanner.nextLine().trim();

        System.out.print("Enter student name: ");
        String name = scanner.nextLine().trim();

        Student student = new Student(id, name);
        String prerequisite = course.getPrerequisiteCourse();

        if (prerequisite != null && !prerequisite.trim().isEmpty()) {
            boolean completed = readYesNo(scanner,
                    "Has the student completed " + prerequisite + "? (yes/no): ");
            if (completed) {
                student.completeCourse(prerequisite);
            }
        }

        return student;
    }

    private static void attemptEnrollment(Course course, Student student) {
        System.out.println("Trying to enroll " + student.getName()
                + " in " + course.getCourseName());

        try {
            course.enrollStudent(student);
        } catch (CourseFullException | PrerequisiteNotMetException e) {
            System.out.println("Enrollment failed: " + e.getMessage());
        } finally {
            System.out.println("Seats left in " + course.getCourseName()
                    + ": " + course.getAvailableSeats());
        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        Course advancedJava = new Course("CS302", "Advanced Java", 2, "Core Java");

        try {
            int totalStudents = readEnrollmentCount(scanner);

            for (int i = 1; i <= totalStudents; i++) {
                Student student = readStudent(scanner, i, advancedJava);
                attemptEnrollment(advancedJava, student);
            }
        } finally {
            scanner.close();
        }
    }
}

Sample Output

How many students want to enroll? 4

Student 1 details
Enter student id: S101
Enter student name: Ananya
Has the student completed Core Java? (yes/no): yes
Trying to enroll Ananya in Advanced Java
Ananya (S101) enrolled in Advanced Java.
Seats left in Advanced Java: 1

Student 2 details
Enter student id: S102
Enter student name: Ravi
Has the student completed Core Java? (yes/no): no
Trying to enroll Ravi in Advanced Java
Enrollment failed: Ravi must complete Core Java before enrolling in Advanced Java.
Seats left in Advanced Java: 1

Student 3 details
Enter student id: S103
Enter student name: Meera
Has the student completed Core Java? (yes/no): yes
Trying to enroll Meera in Advanced Java
Meera (S103) enrolled in Advanced Java.
Seats left in Advanced Java: 0

Student 4 details
Enter student id: S104
Enter student name: Kabir
Has the student completed Core Java? (yes/no): yes
Trying to enroll Kabir in Advanced Java
Enrollment failed: CS302 - Advanced Java is full. Maximum enrolment limit is 2.
Seats left in Advanced Java: 0
Why validation happens before enrolment: the course list changes only after the capacity and prerequisite checks pass. If either check throws an exception, control jumps to the matching catch block and enrolledStudents.add(student) is skipped, so failed attempts never consume a seat.

Review

Quiz and Viva Questions

1. What is exception handling and what is the need for it?

Exception handling is the mechanism that lets a program respond to a runtime error instead of terminating. Without it, one bad input ends the process and the remaining work - including cleanup - is lost. With it, the error is separated from the normal logic, reported meaningfully, and the program keeps control, as the ATM program does when it still prints the balance after a failed withdrawal.

2. What are the types of exceptions in Java?

Two broad kinds. Error represents serious JVM-level problems such as OutOfMemoryError that a program should not attempt to handle. Exception represents recoverable conditions and splits into checked exceptions (the compiler forces you to handle or declare them) and unchecked exceptions, everything under RuntimeException.

3. What is the difference between throw, throws, and finally?

throw is a statement that raises one exception object at that point in the code. throws is part of a method signature declaring that the method may raise those types, so callers must deal with them. finally is a block attached to try that runs regardless of the outcome and is used for cleanup.

4. What is an abstract class?

A class declared with the abstract keyword. It cannot be instantiated with new and exists only to be extended. It may declare abstract methods - methods with no body that every concrete subclass must implement.

5. Can an abstract class have non-abstract methods?

Yes. An abstract class can mix both, as Animal does in the reading material: animalSound() is abstract and left to the subclass, while sleep() is a fully implemented method that every subclass inherits as-is. It can also have fields and constructors.

6. What is the Throwable hierarchy?

Throwable is the root of everything that can be thrown or caught. Its two direct subclasses are Error and Exception; RuntimeException sits under Exception. Catching Exception therefore catches both checked and unchecked exceptions, but not Error.

7. What is the difference between checked and unchecked exceptions?

A checked exception must be caught or declared with throws, and the compiler enforces this - InvalidPinException and CourseFullException are checked because they extend Exception directly. An unchecked exception extends RuntimeException and carries no compile-time obligation; IllegalArgumentException and InputMismatchException are examples.

8. Does the finally block always run?

Practically always - on normal completion, on a caught exception, on an uncaught one, and even after a return inside the try. The exceptions are calling System.exit(), the thread being killed, or the JVM crashing. That reliability is why scanner.close() belongs there.

9. Can you write try without catch?

Yes, provided there is a finally block: try...finally is legal and is used when you want guaranteed cleanup but intend the exception to propagate to the caller. What is not legal is a bare try block on its own - it needs at least one catch or a finally.

10. What is multi-catch and what are its rules?

Multi-catch handles several exception types in one clause, written catch (A | B e). The types must not be in a subclass relationship with each other, and the parameter e is implicitly final, so it cannot be reassigned inside the block. The ATM program uses it for its two unrelated custom types.

11. Why must catch blocks go from specific to general?

Clauses are tested in the order written and the first matching one wins. If catch (Exception e) came first it would match everything, leaving later clauses unreachable - and Java treats unreachable catch blocks as a compile-time error rather than a warning.

12. How do you create a custom exception, and should it extend Exception or RuntimeException?

Declare a class that extends one of them and give it a constructor taking a message and calling super(message). Extend Exception when the caller can reasonably be expected to recover and you want the compiler to insist on handling - the case for all three custom types in this experiment. Extend RuntimeException for programming errors that should not be routinely caught.

13. What is InputMismatchException and when does Scanner throw it?

It is an unchecked exception in java.util, thrown when the next token does not match the type requested - for example typing abc at a nextDouble() call. Because it is not an IllegalArgumentException, the square root program catches it in the general catch (Exception e) clause.

14. What does getMessage() return, and how does super(message) supply it?

getMessage() returns the detail message string stored inside the Throwable. A custom exception's constructor passes its argument up the chain with super(message), which is where that string is stored - so whatever text you pass to new CourseFullException(...) is exactly what e.getMessage() prints later.

Before Submission

Lab Submission Checklist