1

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 main entry 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’t new your class)
  • void — returns nothing (exit codes go through System.exit())
  • main — the name the JVM looks for (by convention, like C’s main)
  • String[] args — command-line arguments (like C++’s argv)

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:

  1. calculateAverage(int[] grades) — returns the average as a double
    • Hint: sum / (double) grades.length — the cast prevents integer division
  2. gradesAbove(int[] grades, double threshold) — returns an int[] 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
Starter files
Welcome.java
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);
        }
    }
}
2

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 == btrue — but only because Java interns string literals (puts them in a pool). Don’t rely on this!
  • c == dfalsenew creates 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.

Starter files
IdentityTrap.java
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));
    }
}
3

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 Integer values

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 unbox null to int, which is impossible.
  • Snippet 2: Every iteration unboxes sum to int, adds i, then boxes the result back to Integer — creating a new object each time. Use int sum = 0 instead.

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:

  1. Read the provided countAbove — understand how it uses ArrayList<Integer> with an int threshold
  2. Implement sumScores(ArrayList<Integer> list) — return the sum using a primitive int accumulator (avoid the autoboxing trap!)
  3. Complete main: create the ArrayList, add scores, and call both methods
Starter files
TypeSystem.java
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)
    }
}
4

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 class is private. 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:

  • withdraw returns boolean: true if successful, false if insufficient funds (balance cannot go negative)
  • deposit should ignore non-positive amounts
  • toString should 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:

  1. Make fields private
  2. Add getter methods getBalance() and getOwner()
  3. Add validation: deposit should ignore non-positive amounts; withdraw should return false if insufficient funds (balance cannot go negative)
  4. Add toString() returning "BankAccount[owner=Alice, balance=1000.0]"
  5. Update main to use getters instead of direct field access
Starter files
BankAccount.java
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);
    }
}
5

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 private and 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:

  1. The grading scale (A/B/C/D/F thresholds) is hardcoded in main, not in GradeReport. If the professor changes the scale, main must change.
  2. The report format (how grades are printed) is built in main by manually iterating the internal structure. If the format changes, main must change.
  3. The data representation is leaked through getScores() — callers depend on ArrayList<Integer>.

Your job — refactor so that GradeReport hides all three decisions:

  1. Move the grading logic into GradeReport by implementing getLetterGrade(int score) — the grading policy is the module’s secret
  2. Move the formatting into GradeReport by implementing formatReport() — the output format is the module’s secret
  3. Remove getScores() — the data representation is the module’s secret
  4. Simplify main so 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.

Starter files
GradeReport.java
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);
    }
}
6

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 interface and implements keywords 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 implement multiple interfaces, and Java 8+ interfaces can have default methods 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:

  1. Read Circle.java to see how a class implements the Shape interface
  2. Implement Rectangle.java following the same pattern, using width and height
  3. 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.

Starter files
Shape.java
public interface Shape {
    double getArea();
    double getPerimeter();
    String describe();
}
Circle.java
// 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 + ")";
    }
}
Rectangle.java
public class Rectangle implements Shape {
    // Follow the same pattern as Circle:
    // private fields, constructor, then implement all three interface methods

}
ShapeDemo.java
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);
    }
}
7

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 @Override to 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 extends exactly 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() { ... }
}

super vs C++: Java uses super(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:

  1. Read Vehicle and Car to understand the abstract/extends/super pattern
  2. Implement Motorcycle following the same pattern as Car
  3. Motorcycle.describe() returns "2023 Harley Motorcycle (with sidecar)" or "2023 Harley Motorcycle" depending on the flag
  4. Motorcycle.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).

Starter files
Vehicles.java
// 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());
        }
    }
}
8

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(), no T[], no primitive type parameters)

⚠ False Friend from C++: Java’s List<String> looks exactly like C++’s vector<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. Use ArrayList<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> → use List<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:

  1. Replace the concrete types (String, int) with type parameters A and B
  2. Rename the class from StringIntPair to Pair<A, B>
  3. Add a static generic method swap that returns a new Pair<B, A> with elements reversed
  4. toString() should return "(first, second)", e.g., "(Alice, 95)"
Starter files
Pair.java
// 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);
    }
}
9

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, or Map, then choose the right concrete implementation
  • Evaluate trade-offs between ArrayList/LinkedList, HashSet/TreeSet, and HashMap/TreeMap for a given task

Transfer from Python: listArrayList, dictHashMap, setHashSet. Similar semantics, different API.

Transfer from C++: vectorArrayList, unordered_mapHashMap, mapTreeMap, unordered_setHashSet.

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:

  1. Word counting (word → frequency): Which collection maps keys to values? Replace the parallel ArrayLists with a single HashMap<String, Integer>
  2. Unique words: Which collection automatically prevents duplicates? Replace ArrayList<String> with a HashSet<String>
  3. Fix getCount to use HashMap’s lookup instead of linear search
  4. Fix getMostFrequent to iterate the HashMap instead of parallel arrays
  5. Add getCounts() returning your HashMap<String, Integer> — used by the test suite

Think first: Before changing any code, decide: should each field be a List, Set, or Map? Why?

Starter files
WordCounter.java
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());
    }
}
10

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 catch them or declare them with throws in 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:

  1. Define a CalculatorException class (checked — extends Exception) with a constructor taking a message
  2. Modify divide to throw CalculatorException when b is 0 (instead of crashing with ArithmeticException)
  3. Modify sqrt to throw CalculatorException when x is negative (instead of returning NaN)
  4. Update main to 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.

Starter files
SafeCalculator.java
// 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));
    }
}
11

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

  1. Student: Simple data class with name and id. toString() returns "Student(name, id)".

  2. EnrollmentException: A checked exception (extends Exception).

  3. Enrollable: Interface defining the enrollment contract.

  4. Course: Implements Enrollable.

    • enroll() throws EnrollmentException if the course is full OR if the student is already enrolled
    • drop() removes a student by name, returns true if found
    • isEnrolled() checks if a student with that name is enrolled
    • getRoster() returns an ArrayList<String> of enrolled student names

Before You Code — Plan Your Approach

Before writing any code, answer these questions mentally:

  1. Which concepts from earlier steps does each class use? (interfaces, encapsulation, exceptions, collections, .equals())
  2. What is the “secret” each class hides? (What could change without affecting other classes?)
  3. Where will you use ArrayList<Student> vs ArrayList<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.

Starter files
Student.java
public class Student {
    // TODO: Private fields: name (String), id (int)

    // TODO: Constructor

    // TODO: getName(), getId()

    // TODO: toString() returning "Student(name, id)"

}
EnrollmentException.java
// TODO: Define EnrollmentException extending Exception
// with a constructor that takes a String message
public class EnrollmentException extends Exception {

}
Enrollable.java
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 {

}
Course.java
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)"

}
EnrollmentDemo.java
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());
        }
    }
}