JAVA 2

Phase 2 — OOP + Core Java

This phase is where Java starts to feel like real Java rather than basic programming.

We will move from simple classes and objects into inheritance, polymorphism, interfaces, composition, immutability, exceptions, collections, and generics.

Chapters in this Phase

Part A — Object-Oriented Java

  1. Classes, Objects & Instance State

  2. Constructors, this & Constructor Overloading

  3. Encapsulation & Access Modifiers

  4. static, final & Instance vs Class Members

  5. Inheritance & super

  6. Method Overriding & Polymorphism

  7. Abstraction & Abstract Classes

  8. Interfaces

  9. Composition vs Inheritance

  10. Enums

  11. Records & Immutable Data

  12. The Object Class — toString(), equals() & hashCode()

  13. Nested & Inner Classes

Part B — Exception Handling

  1. Exceptions & try/catch

  2. throw, throws, Custom Exceptions & Propagation

  3. Try-with-Resources & Exception Design

Part C — Collections

  1. Collections Fundamentals & ArrayList

  2. Sets — HashSet, LinkedHashSet & TreeSet

  3. Maps — HashMap, LinkedHashMap & TreeMap

  4. Queues, Deques & Priority Queues

  5. Iterators, Sorting & Comparators

Part D — Generics

  1. Generic Classes & Methods

  2. Bounded Types & Wildcards

  3. extends, super, PECS & Type Erasure


Part A — Object-Oriented Java

Chapter 1 — Classes, Objects & Instance State

Question

Given below is a code snippet that:

  • Defines a BankAccount class.

  • Creates multiple objects from the same class.

  • Gives each object its own state.

  • Uses methods to change and display object state.

What should be the output of the following code?

// Define a class that describes what a BankAccount object contains and can do.
class BankAccount {

    // Each object gets its own copy of this field.
    String owner;

    // Each object gets its own balance.
    double balance;

    // This method adds money to this particular account.
    void deposit(double amount) {
        balance = balance + amount;
    }

    // This method displays this particular account's information.
    void showBalance() {
        System.out.println(owner + ": " + balance);
    }
}

public class Main {

    public static void main(String[] args) {

        // Create the first BankAccount object.
        BankAccount account1 = new BankAccount();

        // Give the first object its own state.
        account1.owner = "Alice";
        account1.balance = 1000;

        // Create a second BankAccount object.
        BankAccount account2 = new BankAccount();

        // Give the second object different state.
        account2.owner = "Bob";
        account2.balance = 500;

        // Change only Alice's account.
        account1.deposit(250);

        // Display both accounts.
        account1.showBalance();
        account2.showBalance();
    }
}

Answer

Alice: 1250.0
Bob: 500.0

Step-by-step explanation

  1. BankAccount is a class. Think of it as a blueprint describing what a bank account object should contain and do.

  2. account1 refers to one BankAccount object.

  3. account2 refers to another BankAccount object.

  4. Each object has its own owner and balance.

  5. account1.deposit(250) changes only Alice's balance.

  6. Alice's balance becomes 1250.

  7. Bob's balance remains 500.

  8. Therefore two different objects can be created from the same class while holding different data.

How to read the important code

BankAccount account1 = new BankAccount();

Read it naturally:

"Create a BankAccount variable called account1 and assign it a new BankAccount object."

account1 is a reference variable. It refers to the object.

Beginner trap

The class itself is not the individual account.

Class = blueprint. Object = actual thing created from that blueprint.

Key takeaway

A class describes objects; each object can have its own instance state.


Chapter 2 — Constructors, this & Constructor Overloading

Question

Given below is a code snippet that:

  • Creates objects using constructors.

  • Uses this to refer to the current object.

  • Uses multiple constructors.

  • Initializes object state when objects are created.

What should be the output of the following code?

class User {

    // Store the user's name.
    String name;

    // Store the user's age.
    int age;

    // Constructor that accepts both values.
    User(String name, int age) {

        // 'this.name' means the field belonging to the current object.
        this.name = name;

        // Store the supplied age in the object's field.
        this.age = age;
    }

    // Another constructor with a different parameter list.
    User(String name) {

        // Reuse the main constructor with a default age.
        this(name, 18);
    }

    // Display the object's state.
    void show() {
        System.out.println(name + " - " + age);
    }
}

public class Main {

    public static void main(String[] args) {

        // Call the constructor that accepts name and age.
        User user1 = new User("Alice", 25);

        // Call the constructor that accepts only name.
        User user2 = new User("Bob");

        // Display both objects.
        user1.show();
        user2.show();
    }
}

Answer

Alice - 25
Bob - 18

Step-by-step explanation

  1. new User("Alice", 25) matches the constructor with two parameters.

  2. this.name = name stores "Alice" in the object's name field.

  3. this.age = age stores 25.

  4. new User("Bob") matches the second constructor.

  5. That constructor calls this(name, 18).

  6. This means "call another constructor in the same class."

  7. Bob therefore gets the default age 18.

How to read the important code

this.name = name;

Read:

"Assign the parameter named name to this object's name field."

this means the current object.

Beginner trap

These are different:

this.name
name

this.name → object's field.

name → parameter.

Key takeaway

A constructor initializes an object when it is created, and this refers to the current object.


Chapter 3 — Encapsulation & Access Modifiers

Question

Given below is a code snippet that:

  • Hides internal object data using private.

  • Controls access through methods.

  • Validates data before changing it.

  • Demonstrates encapsulation.

What should be the output of the following code?

class BankAccount {

    // Keep the balance private so outside code cannot directly change it.
    private double balance;

    // Allow money to be deposited through controlled logic.
    public void deposit(double amount) {

        // Accept only positive deposits.
        if (amount > 0) {
            balance += amount;
        }
    }

    // Provide controlled access to the balance.
    public double getBalance() {
        return balance;
    }
}

public class Main {

    public static void main(String[] args) {

        // Create an account.
        BankAccount account = new BankAccount();

        // Deposit valid money.
        account.deposit(1000);

        // This negative amount is rejected.
        account.deposit(-500);

        // Read the balance through the public method.
        System.out.println(account.getBalance());
    }
}

Answer

1000.0

Step-by-step explanation

  1. balance is private.

  2. Code outside BankAccount cannot directly manipulate it.

  3. deposit() is public, so outside code can request a deposit.

  4. The method checks whether the amount is positive.

  5. 1000 is accepted.

  6. -500 is rejected.

  7. The balance therefore remains 1000.

How to read the important code

"Keep balance private and expose controlled public methods."

This is encapsulation: keeping an object's internal data protected and controlling how other code interacts with it.

Beginner trap

Don't think encapsulation means simply "use getters and setters everywhere."

The important idea is:

The object controls its own valid state.

Key takeaway

Encapsulation protects object state and allows the class to control how that state changes.


Chapter 4 — static, final & Instance vs Class Members

Question

Given below is a code snippet that:

  • Demonstrates instance fields.

  • Demonstrates a shared static field.

  • Demonstrates a static method.

  • Demonstrates a final constant.

  • Shows the difference between object state and class-level state.

What should be the output of the following code?

class Employee {

    // This value belongs separately to each Employee object.
    String name;

    // This value is shared by all Employee objects.
    static int employeeCount = 0;

    // A final value cannot be reassigned after initialization.
    static final String COMPANY = "TechCorp";

    // Constructor runs whenever an Employee object is created.
    Employee(String name) {

        // Store the name in this particular object.
        this.name = name;

        // Increase the shared employee count.
        employeeCount++;
    }

    // Instance method works with a particular Employee object.
    void show() {
        System.out.println(name + " works at " + COMPANY);
    }

    // Static method belongs to the class rather than one employee.
    static void showCount() {
        System.out.println("Employees: " + employeeCount);
    }
}

public class Main {

    public static void main(String[] args) {

        // Create the first employee.
        Employee e1 = new Employee("Alice");

        // Create the second employee.
        Employee e2 = new Employee("Bob");

        // Show information for each object.
        e1.show();
        e2.show();

        // Call a class-level method.
        Employee.showCount();
    }
}

Answer

Alice works at TechCorp
Bob works at TechCorp
Employees: 2

Step-by-step explanation

  1. name belongs separately to each object.

  2. employeeCount is static, so there is one shared copy for the class.

  3. Creating Alice increases it to 1.

  4. Creating Bob increases it to 2.

  5. COMPANY is static final, so it represents a class-level constant.

  6. show() is an instance method and is called through an object.

  7. showCount() is static, so it can be called using Employee.showCount().

How to read the important code

static int employeeCount;

Read:

"A static integer called employeeCount that belongs to the class."

Beginner trap

A static field is not copied separately into every object.

Key takeaway

Instance members belong to objects; static members belong to the class.


Chapter 5 — Inheritance & super

Question

Given below is a code snippet that:

  • Creates a parent class.

  • Creates a child class using extends.

  • Reuses parent functionality.

  • Uses super to call the parent constructor.

  • Adds child-specific behavior.

What should be the output of the following code?

// Parent class containing common employee information.
class Employee {

    String name;

    // Parent constructor.
    Employee(String name) {
        this.name = name;
    }

    // Common behavior.
    void work() {
        System.out.println(name + " is working");
    }
}

// Manager inherits from Employee.
class Manager extends Employee {

    // Manager constructor.
    Manager(String name) {

        // Call the parent constructor.
        super(name);
    }

    // Add behavior specific to Manager.
    void manage() {
        System.out.println(name + " is managing");
    }
}

public class Main {

    public static void main(String[] args) {

        // Create a Manager object.
        Manager manager = new Manager("Alice");

        // Use inherited behavior.
        manager.work();

        // Use Manager-specific behavior.
        manager.manage();
    }
}

Answer

Alice is working
Alice is managing

Step-by-step explanation

  1. Manager extends Employee means Manager inherits from Employee.

  2. new Manager("Alice") calls the Manager constructor.

  3. super(name) calls the parent Employee constructor.

  4. The parent constructor stores "Alice" in name.

  5. Manager therefore has access to the inherited work() method.

  6. Manager also has its own manage() method.

How to read the important code

class Manager extends Employee

Read:

"Define Manager as a subclass of Employee."

Beginner trap

Inheritance does not mean the child object contains a completely separate parent object.

The child object is also an Employee in the type hierarchy.

Key takeaway

Inheritance allows a class to reuse and specialize behavior from another class.


Chapter 6 — Method Overriding & Polymorphism

Question

Given below is a code snippet that:

  • Uses inheritance.

  • Overrides a parent method.

  • Stores child objects in a parent-type variable.

  • Demonstrates runtime polymorphism.

What should be the output of the following code?

class Animal {

    // Define behavior common to animals.
    void speak() {
        System.out.println("Animal sound");
    }
}

class Dog extends Animal {

    // Replace the inherited behavior with Dog-specific behavior.
    @Override
    void speak() {
        System.out.println("Woof");
    }
}

class Cat extends Animal {

    // Replace the inherited behavior with Cat-specific behavior.
    @Override
    void speak() {
        System.out.println("Meow");
    }
}

public class Main {

    public static void main(String[] args) {

        // The variable type is Animal, but the actual object is Dog.
        Animal first = new Dog();

        // The variable type is Animal, but the actual object is Cat.
        Animal second = new Cat();

        // Java chooses the overridden method based on the actual object.
        first.speak();
        second.speak();
    }
}

Answer

Woof
Meow

Step-by-step explanation

  1. first is declared as Animal.

  2. But the actual object created is a Dog.

  3. Dog overrides speak().

  4. Therefore first.speak() runs Dog's version.

  5. second refers to a Cat.

  6. Therefore second.speak() runs Cat's version.

This is polymorphism.

The simple idea is:

One parent type can refer to different child objects, and the appropriate overridden behavior runs.

How to read the important code

Animal first = new Dog();

Read:

"Create a Dog object and store its reference in an Animal variable."

Beginner trap

Don't decide which overridden method runs only by looking at the variable type.

For overridden instance methods, Java uses the actual object.

Key takeaway

Polymorphism lets the same method call behave differently depending on the actual object.


Chapter 7 — Abstraction & Abstract Classes

Question

Given below is a code snippet that:

  • Defines an abstract class.

  • Defines an abstract method.

  • Provides shared concrete behavior.

  • Forces child classes to implement specific behavior.

What should be the output of the following code?

// Abstract class provides a common structure for payments.
abstract class Payment {

    // Every payment must implement this operation.
    abstract void pay(double amount);

    // All payment types can use this common method.
    void receipt(double amount) {
        System.out.println("Receipt generated: " + amount);
    }
}

class CardPayment extends Payment {

    // Provide the payment behavior required by Payment.
    @Override
    void pay(double amount) {
        System.out.println("Paid by card: " + amount);
    }
}

public class Main {

    public static void main(String[] args) {

        // Create a concrete payment object.
        Payment payment = new CardPayment();

        // Run the child implementation.
        payment.pay(500);

        // Use behavior supplied by the abstract parent.
        payment.receipt(500);
    }
}

Answer

Paid by card: 500.0
Receipt generated: 500.0

Step-by-step explanation

  1. Payment is abstract.

  2. An abstract class is a class intended to provide a common foundation rather than necessarily being created directly.

  3. pay() is abstract, meaning it has no implementation here.

  4. CardPayment must provide pay().

  5. receipt() already has an implementation in the parent.

  6. The Payment variable can refer to the CardPayment object.

  7. Calling pay() therefore runs the CardPayment implementation.

How to read the important code

abstract void pay(double amount);

Read:

"An abstract method called pay that subclasses must implement."

Beginner trap

An abstract class can contain normal methods too. It isn't required to contain only abstract methods.

Key takeaway

Abstraction defines what something must do while allowing subclasses to decide how it does it.


Chapter 8 — Interfaces

Question

Given below is a code snippet that:

  • Defines an interface.

  • Implements an interface.

  • Uses multiple implementations of the same contract.

  • Demonstrates polymorphism through an interface.

What should be the output of the following code?

// Define a capability that payment methods must provide.
interface PaymentMethod {

    // Any implementing class must provide this method.
    void pay(double amount);
}

class Card implements PaymentMethod {

    // Implement the interface method.
    @Override
    public void pay(double amount) {
        System.out.println("Card payment: " + amount);
    }
}

class UPI implements PaymentMethod {

    // Implement the same contract differently.
    @Override
    public void pay(double amount) {
        System.out.println("UPI payment: " + amount);
    }
}

public class Main {

    public static void main(String[] args) {

        // Store a Card object through the interface type.
        PaymentMethod payment1 = new Card();

        // Store a UPI object through the same interface type.
        PaymentMethod payment2 = new UPI();

        // The same method call produces different behavior.
        payment1.pay(1000);
        payment2.pay(750);
    }
}

Answer

Card payment: 1000.0
UPI payment: 750.0

Step-by-step explanation

  1. PaymentMethod defines a contract.

  2. Card promises to follow that contract by using implements.

  3. UPI also follows the same contract.

  4. Both classes implement pay().

  5. payment1 refers to a Card.

  6. payment2 refers to a UPI.

  7. The same interface method call produces different implementations.

How to read the important code

PaymentMethod payment1 = new Card();

Read:

"Create a Card and treat it as a PaymentMethod."

Beginner trap

An interface is not the same thing as an object.

The interface defines the contract; Card and UPI provide implementations.

Key takeaway

Interfaces allow unrelated classes to follow the same contract and be used polymorphically.


Chapter 9 — Composition vs Inheritance

Question

Given below is a code snippet that:

  • Creates independent classes.

  • Uses composition.

  • Gives one object another object as a field.

  • Demonstrates "has-a" rather than "is-a".

What should be the output of the following code?

class Engine {

    // Define behavior belonging to the engine.
    void start() {
        System.out.println("Engine started");
    }
}

class Car {

    // Car has an Engine.
    private Engine engine;

    // Receive the Engine when creating the Car.
    Car(Engine engine) {
        this.engine = engine;
    }

    // Car delegates engine work to its Engine object.
    void start() {
        engine.start();
        System.out.println("Car started");
    }
}

public class Main {

    public static void main(String[] args) {

        // Create an independent Engine object.
        Engine engine = new Engine();

        // Put that Engine inside a Car.
        Car car = new Car(engine);

        // Start the Car.
        car.start();
    }
}

Answer

Engine started
Car started

Step-by-step explanation

  1. Engine is a separate class.

  2. Car contains an Engine reference.

  3. Therefore a Car has an Engine.

  4. Car.start() asks its Engine to start.

  5. The Engine prints its message.

  6. The Car then prints its own message.

This is composition.

How to read the important code

private Engine engine;

Read:

"The Car has an Engine field."

Beginner trap

Don't use inheritance simply because one object is related to another.

Ask:

Is-a? → inheritance may fit.

Has-a? → composition may fit.

A Car has an Engine. A Car is not an Engine.

Key takeaway

Composition builds objects from other objects and is often more flexible than inheritance.


Chapter 10 — Enums

Question

Given below is a code snippet that:

  • Defines an enum.

  • Stores an enum value in an object.

  • Uses an enum in a switch.

  • Demonstrates why enums are safer than arbitrary strings.

What should be the output of the following code?

// Define a fixed set of valid order statuses.
enum OrderStatus {
    NEW,
    PAID,
    SHIPPED
}

class Order {

    // Store the current status.
    OrderStatus status;

    // Initialize the order status.
    Order(OrderStatus status) {
        this.status = status;
    }

    // Describe the current status.
    void describe() {

        // Select behavior based on the enum value.
        switch (status) {
            case NEW -> System.out.println("Order received");
            case PAID -> System.out.println("Payment completed");
            case SHIPPED -> System.out.println("Order shipped");
        }
    }
}

public class Main {

    public static void main(String[] args) {

        // Create an order with a specific enum value.
        Order order = new Order(OrderStatus.PAID);

        // Describe the order.
        order.describe();
    }
}

Answer

Payment completed

Step-by-step explanation

  1. OrderStatus defines three allowed values.

  2. The order receives OrderStatus.PAID.

  3. switch checks that value.

  4. The PAID case matches.

  5. Java prints "Payment completed".

How to read the important code

OrderStatus.PAID

Read:

"The PAID value from the OrderStatus enum."

Beginner trap

An enum is not just a collection of strings.

PAID is a specific enum constant of type OrderStatus.

Key takeaway

Use enums when a value should come from a fixed, known set of choices.


Chapter 11 — Records & Immutable Data

Question

Given below is a code snippet that:

  • Defines a Java record.

  • Creates a record object.

  • Reads record components.

  • Demonstrates the concise syntax used for immutable data carriers.

What should be the output of the following code?

// A record is a concise way to create a data-carrying class.
record Product(String name, double price) {
}

public class Main {

    public static void main(String[] args) {

        // Create a Product record.
        Product product = new Product("Laptop", 75000);

        // Access the record components using automatically created methods.
        System.out.println(product.name());
        System.out.println(product.price());
    }
}

Answer

Laptop
75000.0

Step-by-step explanation

  1. Product is a record with two components: name and price.

  2. new Product(...) creates a record object.

  3. Java automatically provides accessor methods named name() and price().

  4. The record is designed primarily for carrying data.

  5. Record components cannot simply be reassigned after construction.

How to read the important code

record Product(String name, double price)

Read:

"Define a Product record with a String name and a double price."

Beginner trap

Record accessors are:

product.name()

not:

product.getName()

Key takeaway

Records provide concise Java classes for immutable-style data carriers.


Chapter 12 — Object, toString(), equals() & hashCode()

Question

Given below is a code snippet that:

  • Overrides toString().

  • Overrides equals().

  • Overrides hashCode().

  • Compares objects by their data rather than identity.

  • Demonstrates the contract between equals() and hashCode().

What should be the output of the following code?

class User {

    // Store user information.
    String name;
    int age;

    // Initialize the object.
    User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Provide a readable representation of the object.
    @Override
    public String toString() {
        return name + " (" + age + ")";
    }

    // Compare Users based on their data.
    @Override
    public boolean equals(Object other) {

        // Check whether both references point to the same object.
        if (this == other) {
            return true;
        }

        // Check whether the other object is a User.
        if (!(other instanceof User user)) {
            return false;
        }

        // Compare the meaningful fields.
        return age == user.age && name.equals(user.name);
    }

    // Generate a hash code using the same fields used by equals().
    @Override
    public int hashCode() {
        return java.util.Objects.hash(name, age);
    }
}

public class Main {

    public static void main(String[] args) {

        // Create two separate User objects containing the same data.
        User user1 = new User("Alice", 25);
        User user2 = new User("Alice", 25);

        // Print the readable representation.
        System.out.println(user1);

        // Compare the two objects using equals().
        System.out.println(user1.equals(user2));

        // Check whether equal objects have equal hash codes.
        System.out.println(user1.hashCode() == user2.hashCode());
    }
}

Answer

Alice (25)
true
true

Step-by-step explanation

  1. user1 and user2 are two separate objects.

  2. toString() determines how user1 is represented when printed.

  3. equals() compares the name and age.

  4. Both objects contain "Alice" and 25.

  5. Therefore equals() returns true.

  6. hashCode() uses the same meaningful fields.

  7. Therefore equal objects produce the same hash code.

How to read the important code

@Override
public boolean equals(Object other)

Read:

"Override the equals method so Users can define what equality means."

Beginner trap

These are different concepts:

user1 == user2

checks whether two references point to the same object.

user1.equals(user2)

can check whether two objects should be considered equal in value.

Key takeaway

If you override equals(), you normally need a matching hashCode() implementation.


Chapter 13 — Nested & Inner Classes

Question

Given below is a code snippet that:

  • Defines a class inside another class.

  • Creates an inner-class object.

  • Allows the inner class to access the outer object's state.

  • Demonstrates the relationship between outer and inner objects.

What should be the output of the following code?

class Computer {

    // Store information belonging to the outer Computer object.
    String brand = "Dell";

    // Define a non-static inner class.
    class Processor {

        // Access the outer object's field.
        void show() {
            System.out.println("Processor belongs to " + brand);
        }
    }
}

public class Main {

    public static void main(String[] args) {

        // Create the outer object.
        Computer computer = new Computer();

        // Create the inner object through the outer object.
        Computer.Processor processor = computer.new Processor();

        // Run the inner object's method.
        processor.show();
    }
}

Answer

Processor belongs to Dell

Step-by-step explanation

  1. Computer is the outer class.

  2. Processor is an inner class.

  3. The inner class is associated with a particular Computer object.

  4. processor therefore belongs to the computer instance.

  5. It can access the outer object's brand field.

  6. brand contains "Dell".

How to read the important code

Computer.Processor processor = computer.new Processor();

Read:

"Create a Processor inner object associated with the computer object."

Beginner trap

Inner classes are useful, but don't use them simply because Java allows them. Modern Java code often prefers simpler class structures unless the nesting has a clear purpose.

Key takeaway

An inner class can be closely associated with an instance of its outer class.


Part B — Exception Handling

Chapter 14 — Exceptions & try/catch

Question

Given below is a code snippet that:

  • Demonstrates an exception.

  • Uses try.

  • Uses catch.

  • Shows that the program can recover from an exceptional situation.

  • Continues execution after handling the exception.

What should be the output of the following code?

public class Main {

    public static void main(String[] args) {

        // Start code that might produce an exception.
        try {

            // Convert valid text into an integer.
            int number = Integer.parseInt("100");

            // Print the valid number.
            System.out.println(number);

            // This text cannot be converted into an integer.
            int invalid = Integer.parseInt("hello");

            // This line is never reached.
            System.out.println(invalid);

        } catch (NumberFormatException e) {

            // Handle the invalid number situation.
            System.out.println("Invalid number");
        }

        // Execution continues after the catch block.
        System.out.println("Program continues");
    }
}

Answer

100
Invalid number
Program continues

Step-by-step explanation

  1. "100" can be converted into an integer.

  2. Java prints 100.

  3. "hello" cannot be converted into an integer.

  4. Java throws a NumberFormatException.

  5. Java immediately leaves the remaining code inside the try block.

  6. The matching catch block runs.

  7. "Invalid number" is printed.

  8. Execution continues after the exception handling.

How to read the important code

catch (NumberFormatException e)

Read:

"Catch a NumberFormatException and store the exception object in e."

Beginner trap

Once an exception occurs, Java does not continue with the next statement inside that try block.

Key takeaway

Exceptions allow Java programs to handle unexpected situations without necessarily terminating the entire program.


Chapter 15 — throw, throws, Custom Exceptions & Propagation

Question

Given below is a code snippet that:

  • Defines a custom exception.

  • Uses throw.

  • Uses throws.

  • Lets an exception travel from one method to its caller.

  • Handles the exception at a higher level.

What should be the output of the following code?

// Define a custom exception for invalid ages.
class InvalidAgeException extends Exception {

    // Pass the error message to the parent Exception class.
    InvalidAgeException(String message) {
        super(message);
    }
}

class RegistrationService {

    // This method may throw InvalidAgeException.
    void register(int age) throws InvalidAgeException {

        // Reject users younger than 18.
        if (age < 18) {

            // Create and throw the custom exception.
            throw new InvalidAgeException("User must be 18 or older");
        }

        // Run when the age is valid.
        System.out.println("Registration successful");
    }
}

public class Main {

    public static void main(String[] args) {

        // Create the service.
        RegistrationService service = new RegistrationService();

        try {

            // Ask the service to register a user.
            service.register(16);

        } catch (InvalidAgeException e) {

            // Handle the exception here.
            System.out.println(e.getMessage());
        }
    }
}

Answer

User must be 18 or older

Step-by-step explanation

  1. InvalidAgeException is a custom exception.

  2. register() declares throws InvalidAgeException.

  3. The supplied age is 16.

  4. 16 < 18 is true.

  5. The method executes throw new InvalidAgeException(...).

  6. The exception leaves register().

  7. It travels back to the caller.

  8. The catch block receives it.

  9. getMessage() returns the message supplied to the exception.

How to read the important code

throw new InvalidAgeException(...)

Read:

"Create an InvalidAgeException and throw it."

throws InvalidAgeException

Read:

"This method may throw an InvalidAgeException."

Beginner trap

throw and throws are different.

  • throw → actually throws an exception.

  • throws → declares that a method may throw an exception.

Key takeaway

Exceptions can travel up through method calls until suitable code handles them.


Chapter 16 — Try-with-Resources & Exception Design

Question

Given below is a code snippet that:

  • Opens a resource.

  • Uses try-with-resources.

  • Automatically closes the resource.

  • Demonstrates AutoCloseable.

  • Handles an exception safely.

What should be the output of the following code?

class DatabaseConnection implements AutoCloseable {

    // Simulate opening a database connection.
    DatabaseConnection() {
        System.out.println("Connection opened");
    }

    // Simulate performing database work.
    void query() {
        System.out.println("Query executed");
    }

    // Java automatically calls close() when leaving try-with-resources.
    @Override
    public void close() {
        System.out.println("Connection closed");
    }
}

public class Main {

    public static void main(String[] args) {

        try (

            // Create a resource that must eventually be closed.
            DatabaseConnection connection = new DatabaseConnection()
        ) {

            // Use the resource.
            connection.query();

        } catch (Exception e) {

            // Handle an exception if one occurs.
            System.out.println("Something went wrong");
        }
    }
}

Answer

Connection opened
Query executed
Connection closed

Step-by-step explanation

  1. DatabaseConnection implements AutoCloseable.

  2. Java therefore knows it has a close() method.

  3. The connection is created inside the try-with-resources parentheses.

  4. "Connection opened" is printed.

  5. query() executes.

  6. When the try block finishes, Java automatically calls close().

  7. "Connection closed" is printed.

How to read the important code

try (DatabaseConnection connection = new DatabaseConnection())

Read:

"Open a DatabaseConnection resource and automatically close it when the try block finishes."

Beginner trap

Don't manually close resources that are already managed by try-with-resources unless there is a specific reason.

Key takeaway

Try-with-resources automatically closes resources that implement AutoCloseable.


Part C — Collections

Chapter 17 — Collections Fundamentals & ArrayList

Question

Given below is a code snippet that:

  • Creates an ArrayList.

  • Adds and removes elements.

  • Reads elements by index.

  • Checks whether an element exists.

  • Iterates over the list.

What should be the output of the following code?

import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) {

        // Create a mutable list of String values.
        List<String> users = new ArrayList<>();

        // Add users to the list.
        users.add("Alice");
        users.add("Bob");
        users.add("Charlie");

        // Remove Bob from the list.
        users.remove("Bob");

        // Check whether Alice exists.
        System.out.println(users.contains("Alice"));

        // Read the first element using index 0.
        System.out.println(users.get(0));

        // Visit every remaining user.
        for (String user : users) {
            System.out.println(user);
        }
    }
}

Answer

true
Alice
Alice
Charlie

Step-by-step explanation

  1. The list starts empty.

  2. Alice, Bob and Charlie are added.

  3. Bob is removed.

  4. The list now contains Alice and Charlie.

  5. contains("Alice") returns true.

  6. Index 0 contains Alice.

  7. The enhanced for loop visits Alice and then Charlie.

How to read the important code

List<String> users = new ArrayList<>();

Read:

"Create a list of strings called users using an ArrayList."

List describes the general type of collection.

ArrayList is the concrete implementation being created.

Beginner trap

Java collection indexes start at 0.

Key takeaway

ArrayList is the common general-purpose choice when you need an ordered, resizable list.


Chapter 18 — Sets: HashSet, LinkedHashSet & TreeSet

Question

Given below is a code snippet that:

  • Uses a Set.

  • Demonstrates duplicate removal.

  • Demonstrates insertion ordering with LinkedHashSet.

  • Demonstrates sorted ordering with TreeSet.

What should be the output of the following code?

import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;

public class Main {

    public static void main(String[] args) {

        // LinkedHashSet removes duplicates while preserving insertion order.
        Set<String> cities = new LinkedHashSet<>();

        // Add city names.
        cities.add("Delhi");
        cities.add("Mumbai");
        cities.add("Delhi");

        // Print the set.
        System.out.println(cities);

        // TreeSet removes duplicates and sorts the values.
        Set<Integer> numbers = new TreeSet<>();

        // Add numbers in an unsorted order.
        numbers.add(50);
        numbers.add(10);
        numbers.add(30);

        // Print the sorted set.
        System.out.println(numbers);
    }
}

Answer

[Delhi, Mumbai]
[10, 30, 50]

Step-by-step explanation

  1. A Set does not allow duplicate elements.

  2. Delhi is added twice.

  3. The second Delhi does not create another element.

  4. LinkedHashSet preserves insertion order.

  5. Therefore the first set prints Delhi followed by Mumbai.

  6. TreeSet automatically sorts its elements.

  7. Therefore the numbers appear as 10, 30, 50.

How to read the important code

"Create a set of strings using LinkedHashSet."

Beginner trap

Don't assume every Set preserves insertion order.

  • HashSet → no ordering guarantee.

  • LinkedHashSet → insertion order.

  • TreeSet → sorted order.

Key takeaway

Use Set when uniqueness matters; choose the implementation according to your ordering needs.


Chapter 19 — Maps: HashMap, LinkedHashMap & TreeMap

Question

Given below is a code snippet that:

  • Stores key-value pairs.

  • Retrieves values using keys.

  • Updates a value.

  • Checks whether a key exists.

  • Iterates through a map.

What should be the output of the following code?

import java.util.HashMap;
import java.util.Map;

public class Main {

    public static void main(String[] args) {

        // Create a map where String keys point to Integer values.
        Map<String, Integer> scores = new HashMap<>();

        // Add key-value pairs.
        scores.put("Alice", 90);
        scores.put("Bob", 75);

        // Update Alice's existing value.
        scores.put("Alice", 95);

        // Read Alice's score.
        System.out.println(scores.get("Alice"));

        // Check whether Bob exists as a key.
        System.out.println(scores.containsKey("Bob"));

        // Visit every key-value pair.
        for (Map.Entry<String, Integer> entry : scores.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

Answer

The first two lines are deterministic:

95
true

The final two map-entry lines are not guaranteed to have a particular order, because HashMap does not guarantee iteration order.

A possible output is:

95
true
Bob: 75
Alice: 95

Step-by-step explanation

  1. The map stores a key and a value together.

  2. "Alice" initially receives 90.

  3. Putting "Alice" again replaces the old value with 95.

  4. get("Alice") therefore returns 95.

  5. containsKey("Bob") returns true.

  6. entrySet() gives access to each key-value pair.

  7. Because this is a HashMap, you should not rely on the order in which entries are printed.

How to read the important code

Map<String, Integer> scores

Read:

"A map from String keys to Integer values."

Beginner trap

Never write logic that depends on the iteration order of a normal HashMap.

Key takeaway

A Map associates keys with values; HashMap is optimized for general-purpose key-value lookup without an ordering guarantee.


Chapter 20 — Queues, Deques & Priority Queues

Question

Given below is a code snippet that:

  • Uses a queue.

  • Demonstrates FIFO behavior.

  • Uses Deque.

  • Uses a PriorityQueue.

  • Shows different ways elements can be retrieved.

What should be the output of the following code?

import java.util.ArrayDeque;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Deque;

public class Main {

    public static void main(String[] args) {

        // Create a normal queue.
        Queue<String> queue = new ArrayDeque<>();

        // Add elements in order.
        queue.offer("Alice");
        queue.offer("Bob");
        queue.offer("Charlie");

        // Remove the element that entered first.
        System.out.println(queue.poll());

        // Create a deque that can work from both ends.
        Deque<String> deque = new ArrayDeque<>();

        // Add elements to both ends.
        deque.addFirst("Middle");
        deque.addFirst("First");
        deque.addLast("Last");

        // Remove from the front.
        System.out.println(deque.pollFirst());

        // Create a priority queue.
        Queue<Integer> priorities = new PriorityQueue<>();

        // Add numbers in arbitrary order.
        priorities.offer(30);
        priorities.offer(10);
        priorities.offer(20);

        // The smallest value has the highest priority by natural ordering.
        System.out.println(priorities.poll());
    }
}

Answer

Alice
First
10

Step-by-step explanation

  1. A normal queue generally follows FIFO: first in, first out.

  2. Alice enters first, so poll() removes Alice.

  3. A Deque allows insertion/removal from both ends.

  4. addFirst("First") places First at the front.

  5. pollFirst() therefore returns First.

  6. A PriorityQueue does not simply behave like FIFO.

  7. With natural integer ordering, the smallest value has the highest priority.

  8. Therefore 10 is removed first.

How to read the important code

queue.offer("Alice");

Read:

"Add Alice to the queue."

queue.poll();

Read:

"Remove and return the front element."

Beginner trap

A PriorityQueue is not sorted like a normal list when you iterate over it. Its important guarantee concerns which element is returned by operations such as poll().

Key takeaway

Queue = process in order; Deque = both ends; PriorityQueue = process according to priority.


Chapter 21 — Iterators, Sorting & Comparators

Question

Given below is a code snippet that:

  • Uses an Iterator.

  • Removes an element safely during iteration.

  • Sorts objects.

  • Uses a Comparator.

  • Demonstrates custom sorting logic.

What should be the output of the following code?

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

class Product {

    // Store product information.
    String name;
    int price;

    // Initialize the product.
    Product(String name, int price) {
        this.name = name;
        this.price = price;
    }

    // Provide a readable representation.
    @Override
    public String toString() {
        return name + ":" + price;
    }
}

public class Main {

    public static void main(String[] args) {

        // Create a list of products.
        List<Product> products = new ArrayList<>();

        // Add products.
        products.add(new Product("Mouse", 500));
        products.add(new Product("Laptop", 70000));
        products.add(new Product("Keyboard", 1500));

        // Create an iterator for the list.
        Iterator<Product> iterator = products.iterator();

        // Visit each product.
        while (iterator.hasNext()) {

            // Get the next product.
            Product product = iterator.next();

            // Remove the Mouse through the iterator.
            if (product.name.equals("Mouse")) {
                iterator.remove();
            }
        }

        // Sort products from cheapest to most expensive.
        products.sort(Comparator.comparingInt(product -> product.price));

        // Print the sorted products.
        System.out.println(products);
    }
}

Answer

[Keyboard:1500, Laptop:70000]

Step-by-step explanation

  1. Three products are added.

  2. The iterator visits each product.

  3. When it reaches Mouse, iterator.remove() removes it safely.

  4. The remaining products are Laptop and Keyboard.

  5. Comparator.comparingInt(...) tells Java to compare products using their prices.

  6. Keyboard costs 1500.

  7. Laptop costs 70000.

  8. Therefore Keyboard comes first.

How to read the important code

products.sort(Comparator.comparingInt(product -> product.price));

Read:

"Sort the products by their price."

Beginner trap

Don't casually remove elements from a collection inside an enhanced for loop. For situations where removal during iteration is required, an Iterator provides the appropriate mechanism.

Key takeaway

Iterators control traversal, while comparators define how objects should be ordered.


Part D — Generics

Chapter 22 — Generic Classes & Methods

Question

Given below is a code snippet that:

  • Defines a generic class.

  • Creates objects with different types.

  • Defines a generic method.

  • Lets Java enforce type safety at compile time.

What should be the output of the following code?

// Define a generic container whose type is decided by the user.
class Box<T> {

    // Store a value of the generic type.
    private T value;

    // Store a value in the box.
    void set(T value) {
        this.value = value;
    }

    // Return the stored value.
    T get() {
        return value;
    }
}

public class Main {

    // Define a generic method that can work with any type.
    static <T> void printValue(T value) {
        System.out.println(value);
    }

    public static void main(String[] args) {

        // Create a Box that stores Strings.
        Box<String> nameBox = new Box<>();

        // Store a String.
        nameBox.set("Alice");

        // Create a Box that stores Integers.
        Box<Integer> ageBox = new Box<>();

        // Store an Integer.
        ageBox.set(25);

        // Print values from both boxes.
        printValue(nameBox.get());
        printValue(ageBox.get());
    }
}

Answer

Alice
25

Step-by-step explanation

  1. T is a type parameter.

  2. Box<T> means the class can work with different types.

  3. Box<String> creates a box specifically for Strings.

  4. Box<Integer> creates a box specifically for Integers.

  5. nameBox.get() therefore returns a String.

  6. ageBox.get() returns an Integer.

  7. printValue() is also generic and can accept both types.

How to read the important code

Box<String> nameBox

Read:

"A Box of Strings called nameBox."

Box<Integer> ageBox

Read:

"A Box of Integers called ageBox."

Beginner trap

T isn't a variable containing a value.

It represents a type.

Key takeaway

Generics allow classes and methods to work with different types while maintaining compile-time type safety.


Chapter 23 — Bounded Types & Wildcards

Question

Given below is a code snippet that:

  • Uses a bounded generic type.

  • Uses ? extends.

  • Accepts lists of different numeric types.

  • Demonstrates that wildcard types control what can safely be read.

What should be the output of the following code?

import java.util.Arrays;
import java.util.List;

public class Main {

    // T must be a Number or a subclass of Number.
    static <T extends Number> double doubleValue(T value) {
        return value.doubleValue();
    }

    // '? extends Number' means the list contains some unknown Number subtype.
    static double sum(List<? extends Number> numbers) {

        // Start the total at zero.
        double total = 0;

        // Safely read each value as a Number.
        for (Number number : numbers) {
            total += number.doubleValue();
        }

        // Return the total.
        return total;
    }

    public static void main(String[] args) {

        // Create a list of Integers.
        List<Integer> integers = Arrays.asList(10, 20, 30);

        // Create a list of Doubles.
        List<Double> doubles = Arrays.asList(1.5, 2.5);

        // Use the bounded generic method.
        System.out.println(doubleValue(10));

        // Sum the integer list.
        System.out.println(sum(integers));

        // Sum the double list.
        System.out.println(sum(doubles));
    }
}

Answer

10.0
60.0
4.0

Step-by-step explanation

  1. Number is a parent type of types such as Integer and Double.

  2. <T extends Number> says that T must be Number or a subtype.

  3. Therefore an Integer can be passed.

  4. ? extends Number means the method can receive a list containing some unknown subtype of Number.

  5. That could be List<Integer>.

  6. It could also be List<Double>.

  7. Java safely lets us read the elements as Number.

  8. The integer list sums to 60.

  9. The double list sums to 4.0.

How to read the important code

List<? extends Number>

Read:

"A list of some type that extends Number."

Beginner trap

? extends Number does not mean "a list where I can freely add any Number."

It is primarily useful here for safely reading values as Number.

Key takeaway

Bounded generics restrict allowed types and let generic code work safely with related types.


Chapter 24 — extends, super, PECS & Type Erasure

Question

Given below is a code snippet that:

  • Uses ? extends for reading.

  • Uses ? super for adding.

  • Demonstrates the PECS rule.

  • Shows why generic types provide compile-time safety.

What should be the output of the following code?

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Main {

    // Producer: this list provides Number values to us.
    static double total(List<? extends Number> numbers) {

        // Start the running total.
        double sum = 0;

        // Read values safely as Number.
        for (Number number : numbers) {
            sum += number.doubleValue();
        }

        // Return the total.
        return sum;
    }

    // Consumer: this list accepts Integer values from us.
    static void addScores(List<? super Integer> scores) {

        // Add Integer values safely.
        scores.add(100);
        scores.add(200);
    }

    public static void main(String[] args) {

        // Create a list of Integers.
        List<Integer> numbers = Arrays.asList(10, 20, 30);

        // Read values through an extends wildcard.
        System.out.println(total(numbers));

        // Create a list that can accept Integers.
        List<Number> scores = new ArrayList<>();

        // Add Integer values through a super wildcard.
        addScores(scores);

        // Print the resulting list.
        System.out.println(scores);
    }
}

Answer

60.0
[100, 200]

Step-by-step explanation

  1. List<? extends Number> is used when the collection is a producer of values.

  2. The total() method only needs to read numbers.

  3. It can therefore safely read each element as Number.

  4. List<? super Integer> is used when the collection is a consumer of Integer values.

  5. addScores() can safely add Integers.

  6. The scores list is actually a List<Number>.

  7. An Integer is a valid Number, so adding the Integers is safe.

  8. The final list contains 100 and 200.

This leads to the famous rule:

PECS = Producer Extends, Consumer Super.

How to read the important code

List<? super Integer>

Read:

"A list of some type that is Integer or a superclass of Integer."

Beginner trap

Generic inheritance doesn't work like normal class inheritance.

For example:

List<Integer>

is not a subtype of:

List<Number>

even though Integer is a subtype of Number.

Key takeaway

Use extends when you mainly read from a generic source, and super when you mainly put values into a generic destination.


Phase 2 Complete — What You Should Now Understand

You have now covered the major OOP + Core Java foundations that we need before moving into modern/advanced Java.

Object-Oriented Java

You should now understand:

  • Classes

  • Objects

  • Fields

  • Methods

  • Constructors

  • Constructor overloading

  • this

  • Encapsulation

  • Access modifiers

  • private

  • public

  • static

  • final

  • Instance vs class members

  • Inheritance

  • extends

  • super

  • Method overriding

  • Polymorphism

  • Abstract classes

  • Abstract methods

  • Interfaces

  • implements

  • Composition

  • Enums

  • Records

  • Object identity

  • equals()

  • hashCode()

  • toString()

  • Nested/inner classes

Exception Handling

  • Exceptions

  • try

  • catch

  • Exception objects

  • Exception propagation

  • throw

  • throws

  • Custom exceptions

  • finally concepts

  • Try-with-resources

  • AutoCloseable

  • Resource management

Collections

  • List

  • ArrayList

  • Set

  • HashSet

  • LinkedHashSet

  • TreeSet

  • Map

  • HashMap

  • LinkedHashMap

  • TreeMap

  • Queue

  • Deque

  • PriorityQueue

  • Iterator

  • Sorting

  • Comparator

Generics

  • Generic classes

  • Generic methods

  • Type parameters

  • Bounded types

  • Wildcards

  • ? extends

  • ? super

  • PECS

  • Generic type safety

  • Generic inheritance rules

The most important mental model from Phase 2

You should now start reading Java code like this:

List<User> users = new ArrayList<>();

"Create a list of User objects called users using an ArrayList."

PaymentMethod payment = new Card();

"Create a Card object and treat it as a PaymentMethod."

User user = new User("Alice", 25);

"Create a User object initialized with Alice and 25."

List<? extends Number> numbers

"A list containing some unknown Number subtype, which I can safely read as Number."

That style of thinking and reading code is more important than memorizing individual syntax rules.

Phase 3 can now build directly on this foundation: Modern & Advanced Core Java — functional interfaces, lambdas, Optional, Streams, Collectors, advanced collections, I/O/NIO, Date-Time, regex, annotations, reflection, modules, JVM memory and garbage collection.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.