Java for C++ and Python Developers
A fast-paced, hands-on Java tutorial for senior CS students who already know C++ and Python. Master Java's type system, OOP model, and design idioms — with UML diagrams to guide your thinking.
From C++/Python to Java: Your First Class
Why this matters
You already know how to program. This tutorial won’t re-teach loops or variables — instead, it focuses on what’s different about Java and why Java made those choices. Starting with a clear map between languages keeps your prior knowledge useful and prevents silent transfer errors later.
🎯 You will learn to
- Apply Java’s “everything in a class” rule by writing static methods and a
mainentry point - Analyze how Java’s syntax maps to constructs you already know in C++ and Python
The Big Picture
| Feature | Python | C++ | Java |
|---|---|---|---|
| Entry point | if __name__ == "__main__": |
int main() (free function) |
public static void main(String[] args) (method in a class) |
| Typing | Dynamic (x = 42) |
Static (int x = 42;) |
Static (int x = 42;) |
| Memory | GC + reference counting | Manual (new/delete) or RAII |
GC (generational) |
| Free functions | Yes | Yes | No — everything lives in a class |
| Multiple inheritance | Yes (MRO) | Yes | No — single class inheritance + interfaces |
Decoding public static void main(String[] args)
Every word in Java’s entry point has a purpose:
public— accessible from outside the class (the JVM needs to call it)static— no instance of the class is needed (the JVM won’tnewyour class)void— returns nothing (exit codes go throughSystem.exit())main— the name the JVM looks for (by convention, like C’smain)String[] args— command-line arguments (like C++’sargv)
Why so verbose? Java was designed for large-scale, multi-team systems. Explicit declarations make code self-documenting and enable powerful IDE tooling. The verbosity is a tradeoff for safety and readability at scale.
Quick Syntax Mapping
// Variables — like C++, you declare the type
int count = 10;
double pi = 3.14159;
String name = "Alice"; // String is a class, not a primitive
boolean done = false; // not 'bool' (C++) or True/False (Python)
// Printing — not cout, not print()
System.out.println("Count: " + count); // + concatenates with strings
// Arrays — similar to C++, but .length is a field (no parentheses!)
int[] scores = {90, 85, 92};
System.out.println(scores.length); // 3 — NOT .length() or len()
// For loop — identical to C++
for (int i = 0; i < scores.length; i++) {
System.out.println(scores[i]);
}
// Enhanced for — like Python's "for x in list" or C++'s range-for
for (int s : scores) {
System.out.println(s);
}
Task
Edit Welcome.java to implement two static methods and call them from main:
calculateAverage(int[] grades)— returns the average as adouble- Hint:
sum / (double) grades.length— the cast prevents integer division
- Hint:
gradesAbove(int[] grades, double threshold)— returns anint[]of grades strictly above the threshold
Then in main, call both methods on {88, 95, 72, 91, 84} and print:
Average: 86.0
88
95
91
public class Welcome {
// Return the average of the grades array as a double
public static double calculateAverage(int[] grades) {
// TODO: sum all grades, then divide by grades.length
// Remember: sum / (double) grades.length to avoid integer division
return 0.0;
}
// Return a new array containing only grades above the threshold
public static int[] gradesAbove(int[] grades, double threshold) {
// TODO: count how many grades are above threshold
// Then create a new int[] of that size and fill it
return new int[0];
}
public static void main(String[] args) {
int[] grades = {88, 95, 72, 91, 84};
double avg = calculateAverage(grades);
System.out.println("Average: " + avg);
int[] above = gradesAbove(grades, avg);
for (int g : above) {
System.out.println(g);
}
}
}
Step 1 — Knowledge Check
Min. score: 80%
1. Why does Java’s main method signature include static?
The JVM needs to call main as the program entry point. Since no object exists yet when the program starts, main must be static — callable on the class itself, not on an instance.
2. What does int[] grades = {88, 95, 72}; create in Java?
Java arrays are fixed-size, typed containers — similar to C++ arrays. Unlike Python lists, they cannot grow after creation. For a resizable collection, use ArrayList<Integer>.
3. In Java, scores.length has no parentheses but name.length() does. Why?
This is a well-known Java inconsistency. Arrays have a length field (no parentheses). Strings have a length() method. Collections use size(). You’ll memorize this quickly.
The Identity Trap: == vs .equals()
Why this matters
Comparing objects with == is one of the most common Java bugs for newcomers from Python or C++. Two strings that look identical can be unequal — or sneakily equal because of caching. Mastering identity vs. value equality is non-negotiable for writing Java code that is correct and portable across JVMs.
🎯 You will learn to
- Apply
.equals()for value comparison and reserve==for primitives and identity checks - Analyze why string interning and the Integer cache make
==sometimes “work” by accident
⚠ False Friend — Unlearn This: In Python,
==compares values. In C++,operator==can be overloaded for value equality. In Java,==on objects compares identity (are these the exact same object in memory?), NOT value equality.
Predict Before You Code
Before running the code, predict the output of each comparison:
String a = "hello";
String b = "hello";
System.out.println(a == b); // Prediction: ____
String c = new String("hello");
String d = new String("hello");
System.out.println(c == d); // Prediction: ____
System.out.println(c.equals(d)); // Prediction: ____
Reveal the answers
a == b→true— but only because Java interns string literals (puts them in a pool). Don’t rely on this!c == d→false—newcreates separate objects.==checks identity, not content.c.equals(d)→true—.equals()checks value equality.
The Rule
Always use .equals() for object comparison in Java. Use == only for primitives (int, double, boolean, char).
The Integer Cache Trap
Java caches Integer objects for values -128 to 127. This means == on boxed integers sometimes works and sometimes doesn’t:
Integer x = 127;
Integer y = 127;
System.out.println(x == y); // true (cached — same object!)
Integer p = 128;
Integer q = 128;
System.out.println(p == q); // false (not cached — different objects!)
System.out.println(p.equals(q)); // true (value equality)
This is one of the most dangerous bugs in Java — it works in testing (small numbers) and fails in production (large numbers).
Task — Fix the Bug
The IdentityTrap.java file contains a student registry that uses == to compare strings and integers. It has three bugs caused by using == instead of .equals(). Find and fix all three.
The program should output:
Found: Alice
Found: Bob
Same course: true
But currently it prints wrong results because of identity comparison.
public class IdentityTrap {
// Fix these three methods — they use == instead of .equals()
public static boolean findStudent(String input, String stored) {
return input == stored; // BUG: should use .equals()
}
public static boolean sameCourse(Integer a, Integer b) {
return a == b; // BUG: should use .equals()
}
public static void main(String[] args) {
// Bug 1: String comparison
String input = new String("Alice");
String stored = new String("Alice");
System.out.println("Found Alice: " + findStudent(input, stored));
// Bug 2: Another string comparison
String name1 = "B" + "ob";
String name2 = new String("Bob");
System.out.println("Found Bob: " + findStudent(name1, name2));
// Bug 3: Integer comparison (200 is outside the cache range!)
Integer courseA = 200;
Integer courseB = 200;
System.out.println("Same course: " + sameCourse(courseA, courseB));
}
}
Step 2 — Knowledge Check
Min. score: 80%
1. What does == do when applied to two String objects in Java?
In Java, == on objects always checks reference identity — whether two variables point to the same object. Use .equals() for value comparison.
2. Integer x = 100; Integer y = 100; System.out.println(x == y); prints true. What happens if you change 100 to 200?
Java caches Integer objects for -128 to 127 (the Integer cache). Outside this range, new objects are created, so == returns false. This is why you must always use .equals() for wrapper types.
3. In Python, 'hello' == 'hello' compares values. Which Java expression achieves the same thing?
.equals() is the standard way to compare object values in Java. While compareTo also works for strings, .equals() is the idiomatic choice for equality checks.
Java’s Dual Type System: Primitives & Wrappers
Why this matters
Java pretends primitives and objects are interchangeable, but they are not. Autoboxing makes the boundary invisible until a NullPointerException blows up on what looked like an int. Knowing where the boundary is — and where the JVM silently crosses it — is the difference between code that runs and code that mysteriously crashes in production.
🎯 You will learn to
- Apply the right type (primitive vs. wrapper) for each situation, especially with collections
- Analyze autoboxing pitfalls such as null unboxing and identity caching of small
Integervalues
Partial Transfer: C++ has primitives but no autoboxing. Python has only objects (everything is an object). Java has both — and automatically converts between them, sometimes dangerously.
Two Worlds of Types
Java has 8 primitive types that live on the stack (like C++ value types):
| Primitive | Size | Default | Wrapper Class |
|---|---|---|---|
byte |
8-bit | 0 | Byte |
short |
16-bit | 0 | Short |
int |
32-bit | 0 | Integer |
long |
64-bit | 0L | Long |
float |
32-bit | 0.0f | Float |
double |
64-bit | 0.0 | Double |
char |
16-bit | ‘\u0000’ | Character |
boolean |
1-bit | false | Boolean |
Why Wrappers Exist
Java generics use type erasure (more in Step 7), which means they only work with objects, not primitives. You cannot write ArrayList<int> — you must write ArrayList<Integer>.
// ILLEGAL — generics don't accept primitives
// ArrayList<int> numbers = new ArrayList<>();
// LEGAL — use the wrapper class
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(42); // autoboxing: int 42 → Integer.valueOf(42)
int first = numbers.get(0); // unboxing: Integer → int
Predict Before You Code
Before reading further, predict what each snippet does:
// Snippet 1
Integer count = null;
int n = count;
// What happens? ____
// Snippet 2
Integer sum = 0;
for (int i = 0; i < 5; i++) {
sum += i; // What's happening behind the scenes? ____
}
Reveal the answers
- Snippet 1:
NullPointerException! Java tries to unboxnulltoint, which is impossible. - Snippet 2: Every iteration unboxes
sumtoint, addsi, then boxes the result back toInteger— creating a new object each time. Useint sum = 0instead.
The Autoboxing Traps
Trap 1: Null unboxing causes NullPointerException
Integer count = null;
int n = count; // NullPointerException! Can't unbox null.
Trap 2: Performance in loops
// BAD — creates millions of Integer objects
Integer sum = 0;
for (int i = 0; i < 1_000_000; i++) {
sum += i; // unbox sum, add i, box result — every iteration!
}
// GOOD — use primitive type for accumulation
int sum = 0;
for (int i = 0; i < 1_000_000; i++) {
sum += i; // pure arithmetic, no boxing
}
Task
The TypeSystem.java file has a working countAbove method (read it to understand ArrayList<Integer>). Your job:
- Read the provided
countAbove— understand how it usesArrayList<Integer>with anintthreshold - Implement
sumScores(ArrayList<Integer> list)— return the sum using a primitiveintaccumulator (avoid the autoboxing trap!) - Complete
main: create the ArrayList, add scores, and call both methods
import java.util.ArrayList;
public class TypeSystem {
public static int countAbove(ArrayList<Integer> list, int threshold) {
int count = 0;
for (int val : list) { // auto-unboxing: Integer → int
if (val > threshold) {
count++;
}
}
return count;
}
// Implement this: return the sum of all elements
// Use a primitive int accumulator, NOT Integer!
public static int sumScores(ArrayList<Integer> list) {
return 0; // fix this
}
public static void main(String[] args) {
// Create an ArrayList<Integer> called scores
// and add: 95, 87, 42, 73, 61
// Print "Above 70: " + countAbove(scores, 70)
// Print "Sum: " + sumScores(scores)
}
}
Step 3 — Knowledge Check
Min. score: 80%
1. What happens when you run: Integer x = null; int y = x;?
Auto-unboxing calls .intValue() on the Integer object. If the object is null, this throws NullPointerException. This is a common production bug.
2. Why is Integer sum = 0; in a loop slower than int sum = 0;?
Auto-boxing/unboxing creates temporary wrapper objects on every iteration. With int, the operation is pure arithmetic on the stack — no object allocation.
3. [Spaced Practice: Step 2] You need to check if two Integer variables hold the same value. Which approach is always correct?
As we learned in Step 2, == on objects checks reference identity. The Integer cache makes == work for -128 to 127, but .equals() is the only approach that’s always correct.
Classes & Encapsulation
Why this matters
In professional Java, you almost never start from a blank file — you start from a design (often a UML class diagram) and translate it into idiomatic code. Getting access modifiers right is what makes a class safe to evolve: a leaked field becomes a contract you can never break without breaking callers.
🎯 You will learn to
- Apply Java’s four access levels (
private, package-private,protected,public) when implementing a class from a UML diagram - Create a fully encapsulated class whose fields can only be modified through validated methods
Partial Transfer from C++: Java classes look similar to C++ but differ in key ways: no header files, no destructors (GC handles memory), default access is package-private (not private like C++), and there are four access levels (not three).
Transfer from Python: Python has no real access control — just
_naming conventions. Java enforces access at compile time.
Java’s Four Access Levels
| Modifier | Class | Package | Subclass | World |
|---|---|---|---|---|
private |
✓ | ✗ | ✗ | ✗ |
| (none) = package-private | ✓ | ✓ | ✗ | ✗ |
protected |
✓ | ✓ | ✓ | ✗ |
public |
✓ | ✓ | ✓ | ✓ |
⚠ False Friend from C++: In C++, the default access in a
classisprivate. In Java, the default is package-private — accessible to any class in the same package. Always be explicit.
UML Class Diagram
Implement the following BankAccount class. In UML, - means private, + means public:
Design notes:
withdrawreturnsboolean:trueif successful,falseif insufficient funds (balance cannot go negative)depositshould ignore non-positive amountstoStringshould return"BankAccount[owner=Alice, balance=1000.0]"
Task — Refactor for Encapsulation
The BankAccount.java file has a working but poorly designed class: fields are public, there’s no validation, and anyone can set the balance directly. Refactor it to match the UML diagram:
- Make fields
private - Add getter methods
getBalance()andgetOwner() - Add validation:
depositshould ignore non-positive amounts;withdrawshould returnfalseif insufficient funds (balance cannot go negative) - Add
toString()returning"BankAccount[owner=Alice, balance=1000.0]" - Update
mainto use getters instead of direct field access
public class BankAccount {
public String owner; // BAD: should be private
public double balance; // BAD: should be private
public BankAccount(String owner, double initialBalance) {
this.owner = owner;
this.balance = initialBalance;
}
public void deposit(double amount) {
balance += amount; // BAD: no validation — what if amount is negative?
}
public boolean withdraw(double amount) {
balance -= amount; // BAD: allows overdraft!
return true; // BAD: always returns true
}
// Missing: getBalance(), getOwner(), toString()
public static void main(String[] args) {
BankAccount acct = new BankAccount("Alice", 1000.0);
System.out.println("Owner: " + acct.owner); // Direct field access — fix this!
acct.deposit(500.0);
System.out.println("After deposit: " + acct.balance); // Fix this too
boolean ok = acct.withdraw(200.0);
System.out.println("Withdraw 200: " + ok + ", balance: " + acct.balance);
boolean fail = acct.withdraw(5000.0);
System.out.println("Withdraw 5000: " + fail + ", balance: " + acct.balance);
}
}
Step 4 — Knowledge Check
Min. score: 80%
1. What is the main benefit of making fields private with getter/setter methods, compared to public fields?
Encapsulation’s power is controlling access: you can validate inputs, compute derived values, or change internal representation — all without breaking code that uses your class. If balance were public, any code could set it to -999.
2. [Spaced Practice: Step 2] Why does the test use a.getOwner().equals("Test") instead of a.getOwner() == "Test"?
As we learned in Step 2, == on objects checks if they’re the same object in memory. getOwner() returns a String object, so we must use .equals() to compare its value.
3. In Java, what is the default access level if you omit an access modifier on a field?
Unlike C++ where the default is private, Java’s default is package-private. This is a common source of bugs when transitioning from C++. Always be explicit with access modifiers.
Information Hiding: Beyond Encapsulation
Why this matters
Most Java courses stop at “make fields private and add getters/setters” — but that’s encapsulation, not information hiding. The real win is which decisions a module hides: when those secrets leak through the API, every requirement change ripples through the codebase. Parnas’s 1972 insight — hide the decisions most likely to change — is still the highest-leverage idea in software design.
🎯 You will learn to
- Analyze encapsulated Java code and identify which design “secrets” are actually leaking through the API
- Apply information hiding by refactoring a class so a representation or policy change touches only one module
- Evaluate the trade-offs between exposing convenient getters and protecting the freedom to change implementation
⚠ Common misconception: “If my fields are
privateand I have getters/setters, I’ve achieved information hiding.” This is wrong. Encapsulation and information hiding are orthogonal concepts (Parnas 1972).
What is Information Hiding?
In 1972, David Parnas proposed a radical idea: software modules should not be organized around steps in a flowchart. Instead, each module should hide a “secret” — a design decision that is likely to change. The secret isn’t just data; it’s any volatile decision:
| Secret to Hide | Example | Why Hide It? |
|---|---|---|
| Data representation | int[] vs ArrayList vs database |
Storage format may change |
| Algorithm | Bubble sort vs quicksort | Optimization may change |
| Business rules | Grading thresholds, capacity limits | Policy may change |
| Output format | CSV vs JSON vs text | Reporting needs may change |
| External dependency | Which API or library to call | Vendor may change |
When a secret is properly hidden, changing that decision modifies exactly one module. When a secret leaks, changing it causes cascading modifications across the entire system.
Encapsulation ≠ Information Hiding
| Question | Encapsulation | Information Hiding |
|---|---|---|
| What it is | A language technique — bundling data and methods with access modifiers | A design principle — hiding decisions likely to change behind stable interfaces |
| Mechanism | private, protected, public keywords |
Interface design that exposes what, not how |
| Can exist without the other? | Yes — private fields + public getters leak data types | Yes — a C function with a clean API hides information without access modifiers |
The Getter/Setter Fallacy
class Book {
private int isbn;
public int getIsbn() { return isbn; }
public void setIsbn(int isbn) { this.isbn = isbn; }
}
The field is private — full encapsulation. But the return type int leaks the design decision that ISBN is an integer. When the spec changes to support international ISBNs with hyphens (String), every caller of getIsbn() breaks. The module is encapsulated but hides nothing.
Task — Find and Fix the Leaked Secret
GradeReport.java contains a grading system. All fields are private, getters are present — it looks well-designed. But three design decisions have leaked across the code:
- The grading scale (A/B/C/D/F thresholds) is hardcoded in
main, not inGradeReport. If the professor changes the scale,mainmust change. - The report format (how grades are printed) is built in
mainby manually iterating the internal structure. If the format changes,mainmust change. - The data representation is leaked through
getScores()— callers depend onArrayList<Integer>.
Your job — refactor so that GradeReport hides all three decisions:
- Move the grading logic into
GradeReportby implementinggetLetterGrade(int score)— the grading policy is the module’s secret - Move the formatting into
GradeReportby implementingformatReport()— the output format is the module’s secret - Remove
getScores()— the data representation is the module’s secret - Simplify
mainso it calls high-level methods and knows nothing about thresholds, formats, or storage
After your refactoring, a change to the grading scale, the output format, OR the storage structure should require editing only GradeReport — never main.
import java.util.ArrayList;
public class GradeReport {
private String studentName;
private ArrayList<Integer> scores;
public GradeReport(String name) {
this.studentName = name;
this.scores = new ArrayList<>();
}
public void addScore(int score) {
scores.add(score);
}
public String getStudentName() {
return studentName;
}
// LEAKED SECRET #3: Exposes data representation.
// Callers depend on ArrayList<Integer>.
public ArrayList<Integer> getScores() {
return scores;
}
// Add these methods to HIDE the three secrets:
// getLetterGrade(int score) — hides the grading POLICY
// getAverage() — hides the data REPRESENTATION
// formatReport() — hides the output FORMAT
public static void main(String[] args) {
GradeReport report = new GradeReport("Alice");
report.addScore(92);
report.addScore(85);
report.addScore(78);
report.addScore(95);
// LEAKED SECRET #1: Grading policy is here, not in GradeReport.
// If the professor changes thresholds, main must change.
ArrayList<Integer> scores = report.getScores();
System.out.println("Grade Report: " + report.getStudentName());
for (int s : scores) {
String letter;
if (s >= 90) letter = "A";
else if (s >= 80) letter = "B";
else if (s >= 70) letter = "C";
else if (s >= 60) letter = "D";
else letter = "F";
System.out.println(" " + s + " (" + letter + ")");
}
// LEAKED SECRET #2: Report format is here, not in GradeReport.
// If the format changes (e.g., to CSV), main must change.
int sum = 0;
for (int s : scores) { sum += s; }
double avg = sum / (double) scores.size();
System.out.println("Average: " + avg);
}
}
Step 5 — Knowledge Check
Min. score: 80%
1. A GradeReport class has private fields and a getScores() method returning ArrayList<Integer>. The grading thresholds (A ≥ 90, B ≥ 80…) are in the calling code. How many design decisions are leaked?
Two secrets are leaked: (1) The return type ArrayList<Integer> reveals the storage format — callers break if you switch to int[]. (2) The grading thresholds live outside the module — if the professor changes the scale, calling code must change. Private fields alone don’t achieve information hiding.
2. According to Parnas (1972), which of these is NOT a ‘secret’ that a module should hide?
The public interface is precisely what should be VISIBLE — it’s the stable contract other modules depend on. Everything behind that interface (algorithms, data formats, business rules, external dependencies) should be hidden, because those decisions are likely to change.
3. You refactored a system so that changing the grading scale requires editing only one class. What design principle have you applied?
This is Information Hiding in action. The ‘secret’ (grading policy) is isolated in one module. Changing it requires exactly one edit. This goes beyond encapsulation — even with public fields, if the policy logic is in one place, the decision is hidden. Conversely, private fields with grading logic scattered everywhere provide encapsulation without information hiding.
Interfaces: Design by Contract
Why this matters
Idiomatic Java is interface-driven: List, Map, Comparable, Runnable, and most APIs you’ll consume are interfaces whose concrete implementations you swap freely. Programming to interfaces decouples client code from implementation choices, making your code easier to test, extend, and refactor — and it’s the prerequisite for nearly every design pattern.
🎯 You will learn to
- Apply Java’s
interfaceandimplementskeywords to express a contract that separates what from how - Create polymorphic code that operates on an interface type rather than a specific implementation
Partial Transfer from C++: Java interfaces are like C++ abstract classes with only pure virtual functions — but with key differences: a class can
implementmultiple interfaces, and Java 8+ interfaces can havedefaultmethods with implementations.
Transfer from Python: Python uses duck typing (“if it quacks like a duck…”). Java requires explicit
implements— the compiler enforces the contract at compile time, not runtime.
Why Interfaces First?
In professional Java, you encounter interfaces constantly: List, Map, Comparable, Iterable, Runnable. Java’s design philosophy is:
Program to an interface, not an implementation.
This means: declare variables and parameters as the interface type, not the concrete class. This enables flexibility and testability.
UML Interface Notation
In UML, interfaces are shown with <<interface>> above the name. A dashed line with an open triangle means “implements”:
Interface Syntax
// Defining an interface — only method signatures, no implementation
public interface Shape {
double getArea(); // implicitly public and abstract
double getPerimeter();
String describe();
}
// Implementing an interface — must provide ALL methods
public class Circle implements Shape {
private double radius;
public Circle(double radius) { this.radius = radius; }
public double getArea() { return Math.PI * radius * radius; }
public double getPerimeter() { return 2 * Math.PI * radius; }
public String describe() { return "Circle(r=" + radius + ")"; }
}
Task
Study the provided Shape interface and Circle implementation — they’re complete and working. Then:
- Read
Circle.javato see how a class implements theShapeinterface - Implement
Rectangle.javafollowing the same pattern, using width and height describe()should return"Rectangle(w=4.0, h=6.0)"
The provided ShapeDemo.java tests your implementation using the interface type — notice how it works with Shape references, not Circle or Rectangle directly. That’s the power of programming to an interface.
public interface Shape {
double getArea();
double getPerimeter();
String describe();
}
// COMPLETE EXAMPLE — study this, then implement Rectangle
public class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double getArea() {
return Math.PI * radius * radius;
}
public double getPerimeter() {
return 2 * Math.PI * radius;
}
public String describe() {
return "Circle(r=" + radius + ")";
}
}
public class Rectangle implements Shape {
// Follow the same pattern as Circle:
// private fields, constructor, then implement all three interface methods
}
public class ShapeDemo {
// This method works with ANY Shape — polymorphism via interface
public static void printShape(Shape s) {
System.out.println(s.describe());
System.out.println(" Area: " + s.getArea());
System.out.println(" Perimeter: " + s.getPerimeter());
}
public static void main(String[] args) {
Shape c = new Circle(5.0);
Shape r = new Rectangle(4.0, 6.0);
printShape(c);
printShape(r);
}
}
Step 6 — Knowledge Check
Min. score: 80%
1. In ShapeDemo, the method printShape(Shape s) works with both Circle and Rectangle. What makes this possible?
This is polymorphism through interfaces. printShape only knows about the Shape contract (getArea, getPerimeter, describe). Any class that implements Shape can be passed in — the correct implementation is called at runtime.
2. What happens if you add a new method getColor() to the Shape interface?
The compiler enforces the contract. Adding a method to an interface breaks all implementing classes until they provide an implementation. This is the power of implements — compile-time safety that Python’s duck typing can’t provide.
3. [Spaced Practice: Step 4] Why declare Shape c = new Circle(5.0) instead of Circle c = new Circle(5.0)?
Declaring with the interface type signals intent: ‘I only need Shape behavior here.’ This enables flexibility — you could change new Circle(5.0) to new Triangle(3, 4, 5) without modifying any code that uses c.
Inheritance & Polymorphism
Why this matters
Inheritance in Java is more constrained than in C++ — single inheritance only, no diamond problem — but the rules around abstract, @Override, and dynamic dispatch are strict. Misusing them produces silent bugs (typo in an override name = method shadowing instead of overriding). Understanding what Java enforces, and what’s left to you, is what separates working hierarchies from fragile ones.
🎯 You will learn to
- Apply
extends,abstract, and@Overrideto build a single-inheritance class hierarchy with polymorphic dispatch - Evaluate when sharing implementation via an abstract class is preferable to sharing a contract via an interface
⚠ Key difference from C++: Java supports only single class inheritance — a class can
extendsexactly one parent. Java’s answer to multiple inheritance is interfaces (from Step 5). There is no diamond problem.
Transfer from Python: Python supports multiple inheritance with Method Resolution Order (MRO). Java’s single inheritance is simpler but more restrictive.
Abstract Classes vs Interfaces
| Feature | Interface | Abstract Class |
|---|---|---|
| Methods | Abstract (+ default in Java 8+) | Abstract AND concrete |
| Fields | Only static final constants |
Instance fields allowed |
| Constructor | No | Yes |
| Inheritance | implements (multiple OK) |
extends (single only) |
| Use when… | Defining a contract | Sharing implementation |
Rule of thumb: Use an interface when unrelated classes share behavior. Use an abstract class when classes share both behavior AND state.
UML Class Hierarchy
Key Syntax
// Abstract class — cannot be instantiated directly
public abstract class Vehicle {
private String make;
private int year;
public Vehicle(String make, int year) { // constructors in abstract classes!
this.make = make;
this.year = year;
}
public String getMake() { return make; }
public int getYear() { return year; }
// Abstract methods — subclasses MUST implement these
public abstract String describe();
public abstract String startEngine();
}
// Concrete subclass
public class Car extends Vehicle {
// ...
public Car(String make, int year, int numDoors) {
super(make, year); // MUST call parent constructor first
this.numDoors = numDoors;
}
@Override // annotation — compiler checks you're actually overriding
public String describe() { ... }
}
supervs C++: Java usessuper(args)as the first line of a constructor to call the parent constructor. C++ uses initializer lists:Car(...) : Vehicle(make, year) { }.
Note: In real Java, each class would be in its own file. We combine them here to focus on the inheritance concepts without file-switching overhead.
Task
The Vehicle abstract class and Car subclass are provided and working. Your job:
- Read
VehicleandCarto understand theabstract/extends/superpattern - Implement
Motorcyclefollowing the same pattern asCar Motorcycle.describe()returns"2023 Harley Motorcycle (with sidecar)"or"2023 Harley Motorcycle"depending on the flagMotorcycle.startEngine()returns"BRAP BRAP!"
The main method demonstrates polymorphism — a Vehicle reference can point to either a Car or Motorcycle, and the correct describe() is called at runtime (dynamic dispatch).
// COMPLETE — study this abstract class
abstract class Vehicle {
private String make;
private int year;
public Vehicle(String make, int year) {
this.make = make;
this.year = year;
}
public String getMake() { return make; }
public int getYear() { return year; }
public abstract String describe();
public abstract String startEngine();
}
// COMPLETE EXAMPLE — study this, then implement Motorcycle
class Car extends Vehicle {
private int numDoors;
public Car(String make, int year, int numDoors) {
super(make, year); // MUST call parent constructor first
this.numDoors = numDoors;
}
@Override
public String describe() {
return getYear() + " " + getMake() + " Car (" + numDoors + " doors)";
}
@Override
public String startEngine() {
return "Vroom!";
}
}
// YOUR TURN — implement Motorcycle following Car's pattern
class Motorcycle extends Vehicle {
}
public class Vehicles {
public static void main(String[] args) {
Vehicle[] fleet = {
new Car("Toyota", 2024, 4),
new Motorcycle("Harley", 2023, true),
new Car("Honda", 2022, 2),
new Motorcycle("Ducati", 2025, false)
};
for (Vehicle v : fleet) {
System.out.println(v.describe() + " — " + v.startEngine());
}
}
}
Step 7 — Knowledge Check
Min. score: 80%
1. [Spaced Practice: Step 2] What does == do when comparing two String objects in Java?
As we learned in Step 2, == on objects checks reference identity, not value equality. Always use .equals() for value comparison.
2. In Java, which keyword makes a method virtual (overridable by subclasses)?
Unlike C++, where you must mark methods virtual, Java methods are virtual by default. Only final methods cannot be overridden. @Override is an annotation that asks the compiler to verify you’re actually overriding a parent method.
3. When should you use an abstract class instead of an interface?
Abstract classes can have instance fields and constructors, making them ideal for sharing state across subclasses. Interfaces define contracts (behavior) without state. Use interfaces for unrelated classes that share behavior; abstract classes for related classes that share implementation.
4. [Spaced Practice: Step 4] In the UML class diagram notation, what does a - prefix on a field mean?
In UML: - means private, + means public, # means protected, ~ means package-private. This matches Java’s access modifiers.
Generics: Not C++ Templates
Why this matters
Java generics look like C++ templates and behave nothing like them. Because Java erases generic types at compile time, you cannot do new T(), you cannot have a List<int>, and instanceof List<String> doesn’t compile. Knowing where erasure bites stops you from writing code that the compiler will reject — or worse, code that compiles but fails at runtime.
🎯 You will learn to
- Apply generic syntax to write type-safe classes and methods that work with any reference type
- Analyze how type erasure constrains generics (no
new T(), noT[], no primitive type parameters)
⚠ False Friend from C++: Java’s
List<String>looks exactly like C++’svector<string>, but the underlying mechanism is completely different. C++ templates generate separate code for each type. Java generics are a compile-time fiction — erased at runtime.
Why Type Erasure?
When Java 5 added generics in 2004, billions of lines of pre-generics Java code already existed. To maintain binary compatibility — so old .class files could work with new generic code without recompilation — the designers chose to erase generic types after compilation. The result: generics are a compile-time safety net, not a runtime feature.
C++ Templates vs Java Generics
| Feature | C++ Templates | Java Generics |
|---|---|---|
| Mechanism | Code generation (monomorphization) | Type erasure (single shared code) |
| Runtime type info | Yes — vector<int> ≠ vector<string> |
No — List<String> = List<Integer> at runtime |
| Primitive types | Yes — vector<int> works |
No — must use List<Integer> |
new T() |
Yes | No — type unknown at runtime |
| Code bloat | Yes (separate code per type) | No (single shared implementation) |
Predict Before You Code
Before reading further, predict whether each line compiles:
ArrayList<int> nums; // Compiles? ____
ArrayList<Integer> nums; // Compiles? ____
if (list instanceof ArrayList<String>) {} // Compiles? ____
Reveal the answers
ArrayList<int>— No. Generics only work with objects. UseArrayList<Integer>.ArrayList<Integer>— Yes. Wrapper classes work with generics.instanceof ArrayList<String>— No. Generic types are erased at runtime, so Java can’t check them.instanceof ArrayList(raw type) would work, but that defeats the purpose.
Type Erasure: The Compiler’s Magic Act
When you write:
List<String> names = new ArrayList<>();
names.add("Alice");
String first = names.get(0);
The compiler transforms it to (roughly):
List names = new ArrayList(); // raw type
names.add("Alice");
String first = (String) names.get(0); // inserted cast
The generic <String> vanishes after compilation. This is why you cannot:
- Use primitives:
List<int>→ useList<Integer>instead - Create generic instances:
new T()is illegal - Check generic type at runtime:
if (list instanceof List<String>)is illegal
Writing a Generic Class
// A simple generic class — T is a type parameter
public class Box<T> {
private T item;
public Box(T item) { this.item = item; }
public T getItem() { return item; }
public void setItem(T item) { this.item = item; }
}
// Using it — the compiler ensures type safety
Box<String> nameBox = new Box<>("Alice");
String name = nameBox.getItem(); // no cast needed — compiler knows it's String
Box<Integer> numBox = new Box<>(42);
int num = numBox.getItem(); // unboxing Integer → int
Bounded Type Parameters
You can restrict what types are allowed:
// T must implement Comparable<T>
public static <T extends Comparable<T>> T findMax(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
C++ equivalent: This is like C++20 concepts or pre-concepts SFINAE — constraining template parameters. Java’s syntax is simpler:
<T extends SomeType>.
Task — Refactor to Generics
The Pair.java file has a working but non-generic StringIntPair class — it only works for (String, int) pairs. Your job is to generify it into a Pair<A, B> that works for any two types:
- Replace the concrete types (
String,int) with type parametersAandB - Rename the class from
StringIntPairtoPair<A, B> - Add a static generic method
swapthat returns a newPair<B, A>with elements reversed toString()should return"(first, second)", e.g.,"(Alice, 95)"
// REFACTOR THIS: Replace concrete types with generics <A, B>
// Rename class to Pair<A, B>
public class StringIntPair {
private String first;
private int second;
public StringIntPair(String first, int second) {
this.first = first;
this.second = second;
}
public String getFirst() { return first; }
public int getSecond() { return second; }
public String toString() {
return "(" + first + ", " + second + ")";
}
// Add a static generic method:
// public static <X, Y> Pair<Y, X> swap(Pair<X, Y> pair)
public static void main(String[] args) {
Pair<String, Integer> student = new Pair<>("Alice", 95);
System.out.println(student);
Pair<Integer, String> swapped = Pair.swap(student);
System.out.println("Swapped: " + swapped);
Pair<String, String> coords = new Pair<>("lat", "long");
System.out.println(coords);
}
}
Step 8 — Knowledge Check
Min. score: 80%1. Why did Java’s designers choose type erasure instead of reified generics (like C++ templates)?
When generics were added in Java 5, billions of lines of pre-generics code existed. Type erasure ensures old .class files work with new generic code without recompilation — backwards compatibility was the driving constraint.
2. Which of these is ILLEGAL in Java due to type erasure?
After type erasure, T becomes Object at runtime — Java doesn’t know which constructor to call. new T() is illegal. All other options work because the compiler inserts the right casts.
3. [Spaced Practice: Step 3] Why can’t you write ArrayList<int> in Java?
Java generics use type erasure, erasing to Object at runtime. Primitives like int are not objects and can’t be cast to Object. Use the wrapper class Integer instead.
Collections Framework
Why this matters
Real Java code spends most of its time pushing data through List, Set, and Map. Picking the wrong implementation — LinkedList where you needed random access, HashMap where you needed sorted iteration — is the single most common cause of “code that works but is mysteriously slow.” The Java Collections Framework rewards programmers who think in terms of interfaces first and implementations second.
🎯 You will learn to
- Apply the interface-first idiom: declare variables as
List,Set, orMap, then choose the right concrete implementation - Evaluate trade-offs between
ArrayList/LinkedList,HashSet/TreeSet, andHashMap/TreeMapfor a given task
Transfer from Python:
list→ArrayList,dict→HashMap,set→HashSet. Similar semantics, different API.
Transfer from C++:
vector→ArrayList,unordered_map→HashMap,map→TreeMap,unordered_set→HashSet.
The Interface Hierarchy (simplified UML)
Java Collections are organized by interfaces — you program to the interface and choose the implementation:
Choosing the Right Collection
| Need | Interface | Implementation | Python Equivalent |
|---|---|---|---|
| Ordered sequence, access by index | List<T> |
ArrayList<T> |
list |
| Unique elements, fast lookup | Set<T> |
HashSet<T> |
set |
| Key-value pairs | Map<K,V> |
HashMap<K,V> |
dict |
| Sorted unique elements | Set<T> |
TreeSet<T> |
sorted(set) |
| Sorted key-value pairs | Map<K,V> |
TreeMap<K,V> |
dict + sorting |
Common Operations
// List — like Python list or C++ vector
List<String> names = new ArrayList<>();
names.add("Alice"); // append
names.add(0, "Bob"); // insert at index
String first = names.get(0); // index access
names.size(); // NOT .length — that's arrays!
// Map — like Python dict or C++ unordered_map
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95); // insert/update
int grade = scores.get("Alice"); // lookup (returns null if missing!)
scores.containsKey("Alice"); // check existence
// Set — like Python set or C++ unordered_set
Set<String> unique = new HashSet<>();
unique.add("Alice");
unique.add("Alice"); // ignored — already present
unique.contains("Alice"); // true
unique.size(); // 1
⚠ Size inconsistency: Arrays use
.length(field). Strings use.length()(method). Collections use.size()(method). This is a well-known Java wart.
Task — Refactor with Better Collections
The WordCounter.java file has a working implementation, but it uses the wrong collection types. It uses ArrayList for everything — which is inefficient and misses the strengths of Java’s collections framework.
Your job: Identify which collection type is best for each use case and refactor:
- Word counting (word → frequency): Which collection maps keys to values? Replace the parallel
ArrayLists with a singleHashMap<String, Integer> - Unique words: Which collection automatically prevents duplicates? Replace
ArrayList<String>with aHashSet<String> - Fix
getCountto use HashMap’s lookup instead of linear search - Fix
getMostFrequentto iterate the HashMap instead of parallel arrays - Add
getCounts()returning yourHashMap<String, Integer>— used by the test suite
Think first: Before changing any code, decide: should each field be a
List,Set, orMap? Why?
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
public class WordCounter {
// BAD CHOICE: ArrayList is wrong for both of these.
// What collection type should counts be? (hint: key → value)
// What collection type should uniqueWords be? (hint: no duplicates)
private ArrayList<String> countKeys;
private ArrayList<Integer> countValues;
private ArrayList<String> uniqueWords;
public WordCounter(String[] words) {
countKeys = new ArrayList<>();
countValues = new ArrayList<>();
uniqueWords = new ArrayList<>();
for (String word : words) {
// Duplicate tracking is manual and verbose with ArrayList
if (!uniqueWords.contains(word)) {
uniqueWords.add(word);
}
// Parallel arrays for counting — fragile and slow
int idx = countKeys.indexOf(word);
if (idx >= 0) {
countValues.set(idx, countValues.get(idx) + 1);
} else {
countKeys.add(word);
countValues.add(1);
}
}
}
public int getCount(String word) {
// Linear search — O(n) instead of O(1)
int idx = countKeys.indexOf(word);
if (idx >= 0) {
return countValues.get(idx);
}
return 0;
}
public int getUniqueCount() {
return uniqueWords.size();
}
public String getMostFrequent() {
String maxWord = "";
int maxCount = 0;
for (int i = 0; i < countKeys.size(); i++) {
if (countValues.get(i) > maxCount) {
maxCount = countValues.get(i);
maxWord = countKeys.get(i);
}
}
return maxWord;
}
public static void main(String[] args) {
String[] words = {"java", "python", "java", "cpp", "java", "python", "go"};
WordCounter wc = new WordCounter(words);
System.out.println("java: " + wc.getCount("java"));
System.out.println("python: " + wc.getCount("python"));
System.out.println("rust: " + wc.getCount("rust"));
System.out.println("Unique: " + wc.getUniqueCount());
System.out.println("Most frequent: " + wc.getMostFrequent());
}
}
Step 9 — Knowledge Check
Min. score: 80%1. You need to store student IDs and quickly check if a given ID exists. Which collection is best?
When you only need to check membership (not retrieve associated values), a HashSet is ideal: O(1) contains() vs ArrayList’s O(n). HashMap would work but is overkill when you don’t need key-value mapping.
2. What’s wrong with: Map<String, Integer> m = new HashMap<>(); int val = m.get("missing");?
HashMap.get() returns null when a key is missing. Auto-unboxing null to int throws NullPointerException. Use containsKey() first, or getOrDefault(key, 0).
3. [Technique Selection] Match each scenario to the best collection: (A) Counting word frequencies. (B) Removing duplicate email addresses. (C) Maintaining an ordered list of recent commands.
HashMap maps keys to values (word→count). HashSet stores unique elements (emails without duplicates). ArrayList maintains insertion order with index access (command history).
Exception Handling: Checked vs Unchecked
Why this matters
Java’s checked exception model is unique among mainstream languages: the compiler refuses to let you ignore certain failures. Used well, it makes failure paths impossible to forget; used badly, it produces the “catch-and-swallow” anti-pattern that hides bugs forever. To write Java that other engineers will trust, you need a deliberate strategy for which exceptions to throw, which to catch, and which to declare.
🎯 You will learn to
- Apply
try-catch-finally(and try-with-resources) to handle checked exceptions correctly - Evaluate when to use a checked exception, an unchecked exception, or no exception at all
⚠ New concept — no analog in Python or C++: Java uniquely divides exceptions into checked (compiler-enforced handling) and unchecked (runtime errors). Neither Python nor C++ has this distinction.
The Three Philosophies
| Language | Philosophy | Approach |
|---|---|---|
| Python | EAFP (“Easier to Ask Forgiveness than Permission”) | Catch exceptions freely; use try/except as control flow |
| C++ | Exceptions are expensive | Prefer error codes; use exceptions sparingly |
| Java | The Bureaucratic Contract | Checked exceptions force you to handle or declare every possible failure |
Exception Hierarchy
The Rules
Checked exceptions (Exception but not RuntimeException):
- You must either
catchthem or declare them withthrowsin the method signature - Used for recoverable external failures (file not found, network error)
Unchecked exceptions (RuntimeException and subclasses):
- No compiler enforcement — same as Python/C++
- Used for programming errors (null pointer, bad index, bad argument)
// Checked: compiler FORCES you to handle this
public String readFile(String path) throws IOException {
// ...might throw IOException
}
// Calling code MUST handle it:
try {
String content = readFile("data.txt");
} catch (IOException e) {
System.err.println("File error: " + e.getMessage());
}
// Unchecked: no compiler enforcement
public int divide(int a, int b) {
return a / b; // might throw ArithmeticException — but compiler won't complain
}
Custom Exceptions
// Checked custom exception
public class InsufficientFundsException extends Exception {
private double deficit;
public InsufficientFundsException(double deficit) {
super("Insufficient funds: need " + deficit + " more");
this.deficit = deficit;
}
public double getDeficit() { return deficit; }
}
Task — Add Exception Safety
The SafeCalculator.java has working divide and sqrt methods, but they crash on bad input instead of handling errors gracefully. Your job:
- Define a
CalculatorExceptionclass (checked — extendsException) with a constructor taking a message - Modify
divideto throwCalculatorExceptionwhenbis 0 (instead of crashing withArithmeticException) - Modify
sqrtto throwCalculatorExceptionwhenxis negative (instead of returningNaN) - Update
mainto wrap each call in try-catch and print errors gracefully
The compiler will force you to handle the checked exceptions — try removing a catch block and see what happens.
// Step 1: Define CalculatorException extending Exception
// with a constructor that takes a String message
class CalculatorException extends Exception {
}
public class SafeCalculator {
// Step 2: Add "throws CalculatorException" and validation
public double divide(int a, int b) {
// Currently crashes with ArithmeticException on b=0
// Add: if b is 0, throw CalculatorException("Division by zero")
return (double) a / b;
}
// Step 3: Add "throws CalculatorException" and validation
public double sqrt(double x) {
// Currently returns NaN for negative input
// Add: if x < 0, throw CalculatorException("Cannot take square root of negative number")
return Math.sqrt(x);
}
public static void main(String[] args) {
SafeCalculator calc = new SafeCalculator();
// Step 4: Wrap each call in try-catch
// The compiler will tell you these need handling once you add "throws"
System.out.println("10 / 3 = " + calc.divide(10, 3));
System.out.println("10 / 0 = " + calc.divide(10, 0));
System.out.println("sqrt(16) = " + calc.sqrt(16));
System.out.println("sqrt(-4) = " + calc.sqrt(-4));
}
}
Step 10 — Knowledge Check
Min. score: 80%1. Which type of exception does the Java compiler force you to handle?
Checked exceptions (subclasses of Exception but NOT RuntimeException) must be caught or declared with throws. Unchecked exceptions (RuntimeException and its subclasses) have no compiler enforcement.
2. [Spaced Practice: Step 5] What keyword does a Java class use to declare that it fulfills an interface contract?
implements declares that a class fulfills an interface contract. extends is for class inheritance. A class can implement multiple interfaces but can only extend one class.
3. [Spaced Practice: Step 7] Why can’t you write ArrayList<int> in Java?
Java generics are erased at compile time — they become raw types operating on Object. Primitives like int are not objects, so they can’t participate in generics. The wrapper class Integer is used instead.
4. [Technique Selection] Match each scenario to the correct Java construct: (A) Multiple unrelated classes need a serialize() method. (B) Dog and Cat share name, age fields and eat() behavior. (C) You need a type-safe list that works for any element type.
Interfaces define contracts for unrelated classes (A). Abstract classes share state and behavior among related classes (B). Generics provide type-safe parameterization (C).
Design Challenge: Course Enrollment
Why this matters
Real Java systems are never about a single feature in isolation — they require interfaces, inheritance, generics, collections, and exceptions all working together. This step is your integration challenge: you’ll implement a small course enrollment system from a UML specification, exercising every concept from the prior steps. Read the UML diagram carefully before writing any code.
🎯 You will learn to
- Apply interfaces, inheritance, generics, collections, and exceptions in a single coherent design
- Create a Java implementation that conforms exactly to a UML specification
- Evaluate which abstractions belong in which class as the system grows
Full UML Class Diagram
Requirements
-
Student: Simple data class with name and id.toString()returns"Student(name, id)". -
EnrollmentException: A checked exception (extendsException). -
Enrollable: Interface defining the enrollment contract. -
Course: ImplementsEnrollable.enroll()throwsEnrollmentExceptionif the course is full OR if the student is already enrolleddrop()removes a student by name, returnstrueif foundisEnrolled()checks if a student with that name is enrolledgetRoster()returns anArrayList<String>of enrolled student names
Before You Code — Plan Your Approach
Before writing any code, answer these questions mentally:
- Which concepts from earlier steps does each class use? (interfaces, encapsulation, exceptions, collections,
.equals()) - What is the “secret” each class hides? (What could change without affecting other classes?)
- Where will you use
ArrayList<Student>vsArrayList<String>, and why?
Investigate
Look at the EnrollmentDemo.java main method (provided, read-only). It exercises every feature of your system. Your implementations must make it run correctly.
public class Student {
// TODO: Private fields: name (String), id (int)
// TODO: Constructor
// TODO: getName(), getId()
// TODO: toString() returning "Student(name, id)"
}
// TODO: Define EnrollmentException extending Exception
// with a constructor that takes a String message
public class EnrollmentException extends Exception {
}
import java.util.ArrayList;
// TODO: Define the Enrollable interface with methods:
// void enroll(Student student) throws EnrollmentException
// boolean drop(String name)
// boolean isEnrolled(String name)
// ArrayList<String> getRoster()
public interface Enrollable {
}
import java.util.ArrayList;
public class Course implements Enrollable {
// TODO: Private fields: name (String), capacity (int),
// students (ArrayList<Student>)
// TODO: Constructor taking name and capacity
// Initialize students as empty ArrayList
// TODO: getName(), getCapacity(), getEnrollmentCount()
// TODO: enroll(Student student) throws EnrollmentException
// - Throw if course is full: "Course COURSENAME is full"
// - Throw if already enrolled: "STUDENTNAME is already enrolled"
// - Otherwise add to students list
// TODO: drop(String name)
// - Remove student with matching name, return true
// - Return false if not found
// TODO: isEnrolled(String name)
// - Check if any student has the given name
// TODO: getRoster()
// - Return ArrayList<String> of student names
// TODO: toString() returning "Course(name, enrolled/capacity)"
}
public class EnrollmentDemo {
public static void main(String[] args) {
Course cs101 = new Course("CS101", 3);
Student alice = new Student("Alice", 1001);
Student bob = new Student("Bob", 1002);
Student carol = new Student("Carol", 1003);
Student dave = new Student("Dave", 1004);
// Enroll three students
try {
cs101.enroll(alice);
cs101.enroll(bob);
cs101.enroll(carol);
System.out.println("Enrolled 3 students: " + cs101);
} catch (EnrollmentException e) {
System.out.println("Unexpected: " + e.getMessage());
}
// Try to enroll a 4th — course is full
try {
cs101.enroll(dave);
System.out.println("ERROR: Should not reach here");
} catch (EnrollmentException e) {
System.out.println("Expected: " + e.getMessage());
}
// Try to enroll duplicate
try {
cs101.enroll(alice);
System.out.println("ERROR: Should not reach here");
} catch (EnrollmentException e) {
System.out.println("Expected: " + e.getMessage());
}
// Check roster
System.out.println("Roster: " + cs101.getRoster());
System.out.println("Alice enrolled: " + cs101.isEnrolled("Alice"));
// Drop Bob
boolean dropped = cs101.drop("Bob");
System.out.println("Dropped Bob: " + dropped);
System.out.println("After drop: " + cs101);
// Now Dave can enroll
try {
cs101.enroll(dave);
System.out.println("Dave enrolled: " + cs101);
} catch (EnrollmentException e) {
System.out.println("Unexpected: " + e.getMessage());
}
}
}