☕
Core Java Roadmap Phase 02
0 / 40 (0%)
☰ Syllabus Index Next: P03 →

Object-Oriented Programming (OOP) & Modern Class Design

📂 Repository Module: Module 1_ Class Relationship & Binding Demos \ Object-Oriented Programming
📖 Textbook Alignment: Volume I — Chapter 4: Objects and Classes • Chapter 5: Inheritance • Chapter 6: Interfaces and Inner Classes
Phase 2

Object-Oriented Programming (OOP) & Modern Class Design

📂 Module 1_ Problem Solving & Java programming \\ Module 2_ Java LTS Features
📖 Core Java Textbook Alignment: Volume I — Chapter 4: Objects and Classes • Chapter 5: Inheritance • Chapter 6: Interfaces, Lambda Expressions, and Inner Classes

2.1 Classes, Objects, Constructors, this Keyword & Chaining

📖 Cay S. Horstmann • Core Java Volume I — Chapter 5, Section 5.1 ("Inheritance & Dynamic Binding")
“Dynamic binding has a very important property: it makes programs extensible without needing to modify existing code. When the virtual machine calls a method, it looks up the method table for the actual class of the implicit parameter.”
  • vtable LawHotSpot computes a flat method table per loaded class for instant O(1) dynamic method dispatch.
⚡ Pro-Tip: Constructor Chaining Rule
this() or super() MUST be the very first statement in a constructor body. Base state must be initialized before subclass field initialization!
💡 Real-World Analogy: Blueprint vs Physical Building
• Class (The Architectural Blueprint): Drawn on paper. It defines that a house has 3 bedrooms, 2 bathrooms, and a front door. You cannot live inside a blueprint! It consumes almost no physical space.
• Object (The Physical House on 42 Elm Street): A tangible, physical reality built from that blueprint. It takes up actual space on land (Heap memory) and has its own distinct address (reference pointer). You can build 500 distinct houses from one blueprint.
📖 Why Constructor Chaining via this() Exists
Instead of duplicating field initialization logic across 5 overloaded constructors, Java allows one constructor to call another using this(args).
CRITICAL COMPILER RULE: The call to this() (or super()) MUST be the very first statement in the constructor body. You cannot execute custom logic before the parent or primary constructor establishes the object's fundamental state!
📂 Module 1.../Account.java — Constructor Chaining & Initialization Sequence
package com.accenture.ltt.oop;

public class Account {
    private final String accountId;
    private double balance;
    private static double defaultInterestRate;

    // 1. Static Initialization Block: Runs ONCE when class is first loaded by ClassLoader
    static {
        defaultInterestRate = 0.045; // 4.5% interest
        System.out.println("Step 1: Static block executed (Only once per class!)");
    }

    // 2. Instance Initialization Block: Runs before EVERY constructor
    {
        System.out.println("Step 2: Instance initialization block executed");
    }

    // 3. Overloaded Constructor 1: Takes only accountId, defaults balance to 0.0
    public Account(String accountId) {
        this(accountId, 0.0); // Chaining to Constructor 2! MUST BE LINE 1
        System.out.println("Step 4: Finished single-arg constructor for: " + accountId);
    }

    // 4. Overloaded Constructor 2: Master constructor initializing all fields
    public Account(String accountId, double initialBalance) {
        this.accountId = accountId; // 'this.accountId' resolves ambiguity with parameter 'accountId'
        this.balance = initialBalance;
        System.out.println("Step 3: Master constructor executed. ID: " + accountId + ", Balance: $" + balance);
    }
}
👁 Object Initialization Execution Order (Very Common Interview Question!)
  • 1st Static variables and static blocks: Execute once when the class is loaded by the ClassLoader into Metaspace.
  • 2nd Parent constructor: super() executes to construct parent fields.
  • 3rd Instance variables & instance blocks: Execute in textual order top-to-bottom.
  • 4th Constructor body: The remaining code in the invoked constructor completes.

2.2 Encapsulation, Inheritance & Why Java Bans Multiple Class Inheritance

💡 Real-World Analogy: The Diamond Problem
Imagine you have a biological Mom and a biological Dad, both of whom have an eye color gene.
• If Mom's gene says Blue and Dad's gene says Brown, your body has to follow complex genetic dominance rules.
• In C++, if Class D inherits from both Class B and Class C, and both override a method doWork() from Class A, which doWork() does Class D inherit?
• In Java, James Gosling and the designers said: "No ambiguity, no confusion." A class can extend EXACTLY ONE parent class. To inherit multiple behaviors, you use Interfaces because interfaces define contracts, not conflicting state!
🖊 Architectural Comparison: The Diamond Problem in Classes vs Clean Interface Implementation Mermaid Diagram
graph TD subgraph ForbiddenInJava ["BANNED in Java: Multiple Class Inheritance"] ClassA["Class A
+execute()"] ClassB["Class B extends A
Overrides: +execute() -> 'Option B'"] ClassC["Class C extends A
Overrides: +execute() -> 'Option C'"] ClassD["Class D extends B, C
COMPILE ERROR: Diamond Problem Ambiguity!"] ClassA --> ClassB ClassA --> ClassC ClassB -.->|ILLEGAL| ClassD ClassC -.->|ILLEGAL| ClassD end subgraph SolutionInJava ["LEGAL in Java: Multiple Interface Implementation"] Flyable["interface Flyable
void fly();"] Swimmable["interface Swimmable
void swim();"] Duck["class Duck implements Flyable, Swimmable
Provides concrete fly() and swim() methods"] Flyable --> Duck Swimmable --> Duck end

2.2b Class Relationships: Composition (Strong Has-A) vs Aggregation (Weak Has-A)

💡 Real-World Analogy: Human Heart vs Car Passenger
• Composition (The Heart): A human body has a heart. If the human body dies, the heart dies with it (death relationship / lifetime dependency). In code, the parent class instantiates the child object inside its own constructor: account = new Account();.
• Aggregation (The Passenger): A car has a passenger. If the car is driven to the junkyard and scrapped, the passenger simply steps out and continues living. The child object is created independently and passed into the parent via setter or constructor: dept.setFaculty(faculty);.
📂 Module 1.../Demo100_Composition/src/com/accenture/ltt/CompositionTester.java
package com.accenture.ltt;

// Customer owns Account's entire lifecycle (Composition)
class Customer {
    private String customerName;
    private Account account;

    public Customer() {
        // Composition: Account object is instantiated INSIDE Customer constructor!
        this.account = new Account();
        this.account.setAccountNo(9001);
        this.account.setAmount(5000.0);
    }

    public String getCustomerName() { return customerName; }
    public void setCustomerName(String name) { this.customerName = name; }
    public Account getAccount() { return account; }
}

public class CompositionTester {
    public static void main(String[] args) {
        Customer customer = new Customer();
        customer.setCustomerName("John");

        System.out.println("Customer Name: " + customer.getCustomerName());
        System.out.println("Account Number: " + customer.getAccount().getAccountNo());
        System.out.println("Balance Amount: $" + customer.getAccount().getAmount());
    }
}
👁 Composition (Strong Has-A) Architecture Explained
  • Internal Instantiation Line 9: this.account = new Account(); — The Customer directly instantiates the Account. External code cannot inject an arbitrary pre-existing account.
  • Shared Lifecycle (Death Dependency) When customer goes out of scope and is collected by JVM Garbage Collection, its internal Account instance has no other references and is reclaimed from the Heap simultaneously.
📂 Module 1.../Demo101_Aggregation/src/com/accenture/ltt/AggregationTester.java
package com.accenture.ltt;

class Faculty {
    private int facultyId;
    private String facultyName;

    public int getFacultyId() { return facultyId; }
    public void setFacultyId(int id) { this.facultyId = id; }
    public String getFacultyName() { return facultyName; }
    public void setFacultyName(String name) { this.facultyName = name; }
}

class Department {
    private String departmentName;
    private Faculty faculty; // Weak reference: injected from outside!

    public void setDepartmentName(String name) { this.departmentName = name; }
    public String getDepartmentName() { return departmentName; }
    public void setFaculty(Faculty f) { this.faculty = f; }
    public Faculty getFaculty() { return faculty; }
}

public class AggregationTester {
    public static void main(String[] args) {
        Department department = new Department();
        department.setDepartmentName("Computer Science");

        // Faculty exists independently on the Heap BEFORE Department assigns it:
        Faculty faculty = new Faculty();
        faculty.setFacultyId(1001);
        faculty.setFacultyName("Dr. Smith");

        department.setFaculty(faculty); // Loose aggregation association
        System.out.println("Dept: " + department.getDepartmentName() + " | Faculty: " + department.getFaculty().getFacultyName());
    }
}
👁 Aggregation (Weak Has-A) Architecture Explained
  • Independent Existence Lines 27-31: Faculty is instantiated as a standalone object on the Heap. It does not rely on Department to exist.
  • Loose Association If department = null is executed, the Department object is GC'd, but the Faculty object remains safely alive and referenced on the Heap.

2.3 Polymorphism, Dynamic Method Dispatch & The Virtual Table (vtable)

💡 Real-World Analogy: The Universal Remote Control
Imagine you have a universal remote control labeled "EntertainmentDevice" with a button labeled "powerOn()".
• If your remote points at a Television and you press powerOn(), a bright screen turns on.
• If your remote points at a Surround Sound Receiver and you press powerOn(), music starts playing through the speakers.
Your hand holds the exact same remote reference (EntertainmentDevice dev), but the physical device on the shelf (the Heap Object) dictates the actual behavior. That is Runtime Polymorphism!
📂 PolymorphicDispatchDemo.java
public class PolymorphicDispatchDemo {

    abstract static class PaymentService {
        abstract void process(double amount);
    }

    static class CreditCardPayment extends PaymentService {
        @Override
        void process(double amount) {
            System.out.println("Charging $" + amount + " to Visa Card via Payment Gateway");
        }
    }

    static class UpiPayment extends PaymentService {
        @Override
        void process(double amount) {
            System.out.println("Transferring $" + amount + " instantly via UPI Virtual ID");
        }
    }

    public static void main(String[] args) {
        // Reference type is parent PaymentService, but heap object is CreditCardPayment
        PaymentService service = new CreditCardPayment();
        
        // Under the hood: JVM uses 'invokevirtual' opcode and consults the vtable of CreditCardPayment!
        service.process(150.0);

        // Swapping the heap instance to UpiPayment
        service = new UpiPayment();
        service.process(75.0);
    }
}
👁 How Dynamic Method Dispatch Works Under the Hood (vtable)
  • Compile Time The compiler checks: Does the class PaymentService have a method called process(double)? Yes. Compilation succeeds. The bytecode emitted is invokevirtual.
  • Runtime The JVM inspects the actual object on the Heap that service points to. It finds that the object is a CreditCardPayment instance.
  • vtable Lookup Every class in memory has a Virtual Method Table (vtable) containing pointers to its method implementations. The JVM looks up the process entry in CreditCardPayment's vtable and jumps directly to its machine code.
📂 Module 1.../Demo103_DynamicBinding/src/com/accenture/ltt/DynamicBindingTester.java
package com.accenture.ltt;

class Employee {
    private String employeeName;
    private int employeeId;

    public Employee(String name, int id) {
        this.employeeName = name;
        this.employeeId = id;
    }

    public void displayDetails() {
        System.out.println("Employee: " + employeeName + " | ID: " + employeeId);
    }
}

class Manager extends Employee {
    private int numberOfReportees;

    public Manager(String name, int id, int reportees) {
        super(name, id);
        this.numberOfReportees = reportees;
    }

    @Override
    public void displayDetails() {
        System.out.println("Manager with " + numberOfReportees + " reportees!");
    }

    public int getNumberOfReportees() { return numberOfReportees; }
}

public class DynamicBindingTester {
    public static void main(String[] args) {
        // Reference is parent Employee, object on heap is Manager
        Employee emp = new Manager("Cooper", 3001, 4);

        // Dynamic Binding: At runtime, JVM calls Manager's displayDetails()!
        emp.displayDetails();

        // COMPILE ERROR if uncommented: Reference type 'Employee' does not declare getNumberOfReportees()!
        // System.out.println(emp.getNumberOfReportees());
    }
}
👁 Reference Type vs Heap Object Type Mechanics
  • Compiler Check (Left Hand Side) The compiler checks the reference type Employee. If a method like getNumberOfReportees() is not defined on Employee, the compiler rejects the code even though the underlying object is a Manager!
  • Runtime Execution (Right Hand Side) When emp.displayDetails() is invoked, the JVM looks at the heap object (Manager) and dynamically executes the overridden method.

2.4 Modern Interfaces: Why Java 8 Added Default Methods & Java 9 Added Private Methods

💡 Why Default Methods Were Added in Java 8 (The Backward Compatibility Crisis)
In Java 8, Oracle wanted to add the awesome new stream() method to the existing java.util.Collection interface.
The massive problem: In Java 7 and earlier, every class that implements an interface MUST provide an implementation for EVERY abstract method. Millions of Java enterprise projects had custom collections (like Hibernate, Apache Commons, Guava).
If Oracle added an abstract stream() method to Collection, every Java application in the world would break upon updating to Java 8!
The Solution: Default Methods (default). It allows an interface to provide a default fallback implementation. Existing classes automatically inherit the default implementation without breaking!
📂 Module 2_ Java LTS Features/ModernOrderService.java
package com.accenture.ltt.oop;

@FunctionalInterface
public interface ModernOrderService {

    // 1. Single Abstract Method (SAM): Makes it eligible for Lambdas!
    boolean processOrder(String orderId, double amount);

    // 2. Java 8 Default Method: Backward-compatible business logic
    default void logAuditTrail(String orderId, String status) {
        formatAndPrintAudit("AUDIT", orderId, status);
    }

    // 3. Java 8 Static Method: Utility helper bound to the interface
    static boolean isValidOrderId(String orderId) {
        return orderId != null && orderId.startsWith("ORD-");
    }

    // 4. Java 9 Private Method: Encapsulates shared logic between default methods
    private void formatAndPrintAudit(String prefix, String orderId, String status) {
        System.out.println("[" + prefix + "] Order " + orderId + " status updated to: " + status);
    }
}
👁 Modern Interface Evolution Breakdown (Java 8 & 9)
  • SAM & @FunctionalInterface Line 3: @FunctionalInterface tells the compiler this interface has exactly one abstract method (SAM: processOrder). This allows developers to instantiate it using clean Lambda expressions: (id, amt) -> amt > 0.
  • Default Methods (Java 8) Line 9: default void logAuditTrail(...) — Provides a default body. Implementing classes inherit this method automatically without being forced to implement it. Critical for API library maintainers adding new features without breaking existing client code.
  • Static Interface Methods (Java 8) Line 14: static boolean isValidOrderId(...) — Utility method directly attached to the interface namespace. Called as ModernOrderService.isValidOrderId("ORD-1") without needing external utility helper classes like OrderUtils.
  • Private Interface Methods (Java 9) Line 19: private void formatAndPrintAudit(...) — Solves code duplication between multiple default methods inside the interface while keeping internal helper logic hidden from external consumers.

2.5 Modern Java: Records (Java 16) & Sealed Classes (Java 17)

Modern Construct Traditional Pain Point How Modern Java Solves It
Records (Java 16+) Writing 60 lines of boilerplate (private final fields, constructor, getters, equals(), hashCode(), toString()) just to carry data. A 1-line immutable data carrier: record UserDTO(Long id, String email) {}. The compiler writes all boilerplate automatically!
Sealed Classes (Java 17+) You want a class hierarchy, but you want to tightly restrict WHO can subclass it, preventing third parties from creating rogue subclasses. sealed class Shape permits Circle, Square {}. The compiler guarantees that only Circle and Square can ever inherit from Shape.
📂 Module 2_ Java LTS Features/01_Java17_Demos/.../Product.java
package com.accenture.ltt.sampleclasses;

/**
 * Java 17 Sealed Class: Restricts hierarchy to strictly permitted subclasses.
 * Enables exhaustive pattern matching switch in modern backend systems.
 */
public sealed abstract class Product 
    permits ElectronicsProducts, KidsProducts, BeautyProducts {

    abstract public String getProductDescription();
}

// Subclasses MUST explicitly declare their inheritance posture:
// - 'final': Cannot be extended any further
// - 'sealed': Extends the sealed hierarchy to another set of permitted subclasses
// - 'non-sealed': Opens the class up for general inheritance
public final class ElectronicsProducts extends Product {
    @Override
    public String getProductDescription() {
        return "Certified Enterprise Electronic Device";
    }
}
👁 Sealed Classes Compiler Rules
  • permits Declares which specific subclasses can extend this class. All permitted subclasses must reside in the same package (or module).
  • Exhaustiveness Enables compiler-enforced exhaustiveness in pattern matching switch statements. You don't need a default clause if every permitted subclass is handled!
📂 UserRecordDemo.java — Java 16 Immutable Records
public class UserRecordDemo {

    // Modern Java 16 Record: A 1-line immutable data carrier!
    // Automatically generates: private final fields, constructor, getters (id(), email()), equals(), hashCode(), toString()
    public record UserDTO(Long id, String username, String email) {
        
        // Compact Constructor: Used for validation without repeating 'this.x = x' boilerplate
        public UserDTO {
            if (email == null || !email.contains("@")) {
                throw new IllegalArgumentException("Invalid email format: " + email);
            }
        }
    }

    public static void main(String[] args) {
        UserDTO user = new UserDTO(101L, "john_doe", "john@accenture.com");

        // Accessor methods use field name (id(), not getId())
        System.out.println("User Email: " + user.email());
        System.out.println("Auto toString(): " + user);
    }
}
👁 Java 16 Records & Compact Constructor Breakdown
  • What Java Generates Line 3: public record UserDTO(Long id, String username, String email) — In just ONE line, the Java compiler automatically synthesizes: (1) private final instance fields, (2) Canonical constructor, (3) Getter methods named after fields (user.email() instead of getEmail()), (4) Field-based equals() and hashCode(), and (5) Formatted toString()!
  • Immutability by Design Records are shallowly immutable and implicitly final (cannot be extended). Their state cannot be modified after construction, making them thread-safe DTOs for REST APIs and messaging queues.
  • Compact Constructor Lines 6-10: public UserDTO { ... } — Special record constructor without a parameter list. Used strictly for input validation and normalization. Notice you do NOT write this.email = email; — Java assigns fields automatically after the compact block finishes!
📂 OrderStatusEnumDemo.java — Enterprise Enums with Fields & Logic
public class OrderStatusEnumDemo {

    public enum OrderStatus {
        PENDING(100, "Awaiting Payment"),
        PROCESSING(200, "Packing in Warehouse"),
        SHIPPED(300, "In Transit"),
        DELIVERED(400, "Delivered to Customer");

        private final int statusCode;
        private final String description;

        // Enum constructor is always private
        OrderStatus(int statusCode, String description) {
            this.statusCode = statusCode;
            this.description = description;
        }

        public int getStatusCode() { return statusCode; }
        public String getDescription() { return description; }

        public boolean isTerminal() {
            return this == DELIVERED;
        }
    }

    public static void main(String[] args) {
        OrderStatus status = OrderStatus.PROCESSING;
        System.out.println("Status Code: " + status.getStatusCode() + " -> " + status.getDescription());
        System.out.println("Is complete? " + status.isTerminal());
    }
}
👁 Enterprise Java Enums Architecture Explained
  • Under the Hood In Java, an enum is actually a full class that implicitly extends java.lang.Enum<E>. Each declared constant (PENDING, PROCESSING, etc.) is a public static final instance created once during class loading.
  • Custom Fields & Constructor Lines 7-13: Enums can possess state and behaviors! The constructor is strictly private (invoked only by the JVM when initializing constants). You can never call new OrderStatus(...).
  • Thread-Safe Singletons Because enum constants are created by the ClassLoader during initialization, they are guaranteed by the JVM specification to be strictly thread-safe singletons and immune to reflection/serialization attacks.
  • Business Methods Line 18: isTerminal() — Enums can embed domain business rules, eliminating sprawling if-else checks across your service layer.
📂 CloningDeepCopyDemo.java — Shallow Copy vs Deep Copy Proof
public class CloningDeepCopyDemo {

    static class Address {
        String city;
        Address(String city) { this.city = city; }
    }

    static class Customer implements Cloneable {
        String name;
        Address address;

        Customer(String name, Address address) {
            this.name = name;
            this.address = address;
        }

        // Shallow Copy: Default Object.clone() only copies primitive values & address references!
        public Customer shallowCopy() throws CloneNotSupportedException {
            return (Customer) super.clone();
        }

        // Deep Copy: Explicitly duplicates nested objects so modifications don't corrupt original!
        public Customer deepCopy() {
            return new Customer(this.name, new Address(this.address.city));
        }
    }

    public static void main(String[] args) throws Exception {
        Customer original = new Customer("Alice", new Address("New York"));

        // 1. Shallow Copy Test
        Customer shallow = original.shallowCopy();
        shallow.address.city = "London";
        System.out.println("Shallow copy changed original city to: " + original.address.city); // London! Shared object corrupted!

        // 2. Deep Copy Test
        Customer deep = original.deepCopy();
        deep.address.city = "Tokyo";
        System.out.println("Deep copy changed its city to Tokyo, original stays: " + original.address.city); // London! Safe!
    }
}
👁 Shallow Copy vs Deep Copy Memory Trap Breakdown
  • The Cloneable Flaw In Java, calling super.clone() performs a shallow bitwise copy. It copies primitive fields (int, boolean), but for reference fields (Address address), it copies ONLY the 64-bit memory pointer!
  • The Shallow Copy Trap In Step 1: Both original and shallow have distinct Customer objects on the Heap, BUT both point to the exact same Address object in memory. Mutating shallow.address.city = "London" quietly corrupts the original customer's data!
  • The Deep Copy Solution Line 20: new Customer(this.name, new Address(this.address.city)) creates a brand new Address object on the Heap with duplicated values. Modifying the copy has zero effect on the original.
  • Modern Best Practice Joshua Bloch (Effective Java) explicitly advises: Avoid Cloneable! Prefer Copy Constructors (public Customer(Customer other)) or factory methods instead.