[go: up one dir, main page]

0% found this document useful (0 votes)
72 views100 pages

Complete Java OOP Mastery

The document outlines a comprehensive curriculum for mastering Object-Oriented Programming (OOP) in Java, structured into ten modules ranging from prerequisites to advanced techniques. Key topics include core OOP principles, modern Java features, design patterns, system design, and anti-patterns, with practical projects for mastery validation. Each module emphasizes critical concepts, best practices, and common pitfalls to ensure a solid understanding of OOP in Java.

Uploaded by

untiliwinb48
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
72 views100 pages

Complete Java OOP Mastery

The document outlines a comprehensive curriculum for mastering Object-Oriented Programming (OOP) in Java, structured into ten modules ranging from prerequisites to advanced techniques. Key topics include core OOP principles, modern Java features, design patterns, system design, and anti-patterns, with practical projects for mastery validation. Each module emphasizes critical concepts, best practices, and common pitfalls to ensure a solid understanding of OOP in Java.

Uploaded by

untiliwinb48
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Complete Java OOP Mastery

Basics → Advanced
A structured, progressive curriculum to master Object-Oriented Programming in Java. Follow modules sequentially for
solid foundations.

📌 Module 0: Prerequisites
(Must know before OOP)

Java syntax basics ( main() , variables, data types)


Control structures ( if/else , loops)
Basic I/O ( [Link] , Scanner )
Arrays and String manipulation
JVM/JDK setup & build tools (Maven/Gradle basics)

🔰 Module 1: Core OOP Fundamentals


Topic Key Concepts
Classes & Class definition, new keyword, object lifecycle, this reference, constructors
Objects (default/parameterized), constructor chaining
Encapsulation Access modifiers ( private / protected / public /package-private), getters/setters, JavaBeans
convention, immutability ( final fields/classes)
Methods Instance vs. static methods, method overloading, varargs, pass-by-value semantics
static Static fields/methods/blocks, class vs. instance members, singleton pattern basics
Keyword
final Keyword Final classes/methods/variables, effectively final (for lambdas)

🔷 Module 2: Inheritance & Polymorphism


Topic Key Concepts
Inheritance extends , super() , method overriding ( @Override ), super keyword, IS-A relationship

Polymorphism Runtime binding, upcasting/downcasting, instanceof , dynamic method dispatch


Abstract Classes abstract keyword, partial implementation, when to use vs. interfaces

Interfaces (Pre-Java 8) Contract definition, multiple inheritance of type, marker interfaces


Method Resolution Compile-time vs. runtime binding, field hiding vs. method overriding

🔸 Module 3: Modern Java OOP Features


Topic Key Concepts
Interfaces (Java 8+) default methods, static methods in interfaces, solving diamond problem

Interfaces (Java 9+) private methods in interfaces

Records (Java 14+) Concise data carriers, equals / hashCode / toString auto-generation
Sealed Classes (Java sealed / permits / non-sealed , restricted inheritance hierarchies
17+)
Pattern Matching instanceof pattern matching (Java 16+), switch expressions with patterns (Java
21+)

⚖️ Module 4: Design Principles (SOLID + Beyond)


Principle Core Idea Java Application
Single One reason to change Separate data/validation/persistence logic
Responsibility
Open/Closed Open for extension, closed for Strategy pattern, interfaces
modification
Liskov Substitution Subtypes must be substitutable Avoid breaking parent contracts in overrides
Interface Many client-specific interfaces Split Worker into Eatable , Workable
Segregation
Dependency Depend on abstractions Constructor injection, avoid new inside
Inversion business logic
DRY / KISS / YAGNI Avoid duplication, keep simple, don't Refactor duplication, resist premature
over-engineer abstraction

🧩 Module 5: Essential Design Patterns


Category Patterns to Master When to Use
Creational Factory Method, Abstract Factory, Builder, Singleton Object creation complexity, configuration
(thread-safe), Prototype flexibility
Structural Adapter, Decorator, Composite, Facade Interface translation, dynamic behavior
addition, tree structures
Behavioral Strategy, Observer, Command, Template Method, Algorithm variation, event systems,
State undo/redo, state machines

💡 Critical: Implement each pattern by hand before using frameworks that abstract them away.

🧱 Module 6: Advanced OOP Techniques


Topic Key Concepts
Composition over Favor HAS-A over IS-A, delegation, behavior injection
Inheritance
Object Equality == vs .equals() , hashCode() contract, [Link]() , immutability impact
Topic Key Concepts
Cloning Cloneable interface pitfalls, copy constructors, defensive copying

Inner Classes Static nested, non-static inner, local, anonymous classes, lambda equivalence
Generics & OOP Bounded type parameters ( <T extends Animal> ), PECS ( ? extends / ? super ),
generic methods
Enums as Classes Enum constructors, methods, values() , state machines with enums
Exception Design Checked vs. unchecked, custom exceptions, exception translation

⚙️ Module 7: System Design & Architecture


Concept OOP Application
Domain-Driven Design (DDD) Entities/value objects/aggregates, rich domain models vs. anemic models
Layered Architecture Separation: Controller → Service → Repository (avoid leaking layers)
Dependency Injection Manual DI → frameworks (Spring/Guice), inversion of control containers
API Design Fluent interfaces, builder patterns, immutability in public APIs
Testing OOP Code Mocking dependencies (Mockito), testing polymorphic behavior, contract tests

🚫 Module 8: Anti-Patterns & Code Smells


Smell Why It's Bad Fix
Anemic Domain Model Classes = data bags with external logic Move behavior into classes
( [Link]() )
God Class 1000+ LOC, multiple responsibilities Extract cohesive classes/services
Switch Statements on Violates OCP/polymorphism Replace with strategy/state pattern
Type
Setter Overuse Breaks encapsulation, mutable state Prefer immutable objects or behavior
methods
Inheritance Abuse Deep hierarchies (>3 levels), fragile base Flatten hierarchy, use composition
class
Feature Envy Method uses another class' data more Move method to appropriate class
than its own

🔬 Module 9: JVM Internals (OOP-Relevant)


Topic Why It Matters for OOP
Object Memory Layout Header + instance data + padding, impact of synchronized
vtable (Method Tables) How polymorphism works at bytecode level
Class Loading ClassLoader hierarchy, dynamic class generation

Escape Analysis Stack allocation of objects, scalar replacement


String Interning String pool, == vs .equals() pitfalls
🧪 Module 10: Mastery Validation Projects
Build these without frameworks first to internalize OOP:

1. Banking System
Accounts with polymorphic interest calculation
Transaction history with immutability
Transfer service using dependency injection
2. E-Commerce Cart
Product variants via composition (not inheritance)
Discount strategies (Strategy pattern)
Order pipeline with decorators (gift wrap, tax, shipping)
3. Game Entity System
Entities with component-based design (composition)
State pattern for NPC behavior
Observer pattern for event system
4. Plugin Architecture
Define service interfaces
Runtime plugin loading via ServiceLoader
Versioned API contracts

Module 1: Core OOP Fundamentals — Deep Dive


Master these foundational concepts thoroughly. Weak fundamentals create fragile designs later. Each topic includes
key insights, common mistakes, and production-grade examples.

1️⃣ Classes & Objects: The Blueprint and Instance


Core Concepts

// Class = blueprint (template)


public class Car {
// State (fields)
private String model;
private int year;

// Behavior (methods)
public void startEngine() {
[Link](model + " engine started");
}
}

// Object = concrete instance in memory


Car myCar = new Car(); // 'new' allocates memory on heap
[Link](); // Method call on instance

Critical Insights
Concept Explanation Why It Matters
Class JVM loads .class file only when first actively Avoids unnecessary memory usage
Loading used ( new , static access)
Object Each new creates unique object with distinct == compares references, not content
Identity memory address
Default Fields auto-initialized: 0 , false , null (NOT Prevents NullPointerException on fields—
Values local variables!) but not locals

Common Mistake

// ❌ Local variable NOT auto-initialized


public void test() {
int x;
[Link](x); // COMPILE ERROR!
}

// ✅ Field IS auto-initialized
public class Test {
int x; // defaults to 0
}

2️⃣ Encapsulation: Protecting Object Integrity


Access Modifiers Deep Dive

Modifier Class Package Subclass World Use Case


private ✅ ❌ ❌ ❌ Internal implementation details
(default) ✅ ✅ ❌ ❌ Package-private helpers
protected ✅ ✅ ✅ ❌ Template method pattern, testing
public ✅ ✅ ✅ ✅ Public API contracts

Production-Grade Encapsulation

public class BankAccount {


private String accountNumber; // Hidden internal state
private BigDecimal balance;

// ✅
Behavior-focused API (NOT data exposure)
public void deposit(BigDecimal amount) {
if ([Link]([Link]) <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
[Link] = [Link](amount);
}

public boolean withdraw(BigDecimal amount) {


if ([Link](balance) > 0) return false;
[Link] = [Link](amount);
return true;
}

// ❌ BAD: Exposes mutable internal state


// public BigDecimal getBalance() { return balance; }

// ✅ GOOD: Defensive copy for mutable objects


public BigDecimal getBalance() {
return balance; // BigDecimal is immutable → safe
}

// ✅ GOOD: Immutability by design


public String getAccountNumber() {
return accountNumber; // String is immutable
}
}

Key Principle

Encapsulation isn't about getters/setters—it's about exposing behavior, not data.


[Link](100) ✅ vs [Link]([Link]() + 100) ❌

3️⃣ Constructors & Object Initialization


Constructor Types & Chaining

public class User {


private final String id; // Must be initialized in constructor
private String name;
private int age = 18; // Field initializer (runs BEFORE constructor body)

// Default constructor (compiler generates if no constructors defined)


public User() {
[Link] = generateId(); // 'this' must be first statement if used
// this("Guest"); // ❌
Can't chain after field assignment
}

// Parameterized constructor
public User(String name) {
this(); // Constructor chaining MUST be first statement
[Link] = name;
}

// Full constructor with validation


public User(String id, String name, int age) {
if (id == null || [Link]())
throw new IllegalArgumentException("ID required");
[Link] = id;
[Link] = name;
setAge(age); // Delegate to setter for validation reuse
}

private void setAge(int age) {


if (age < 0 || age > 150)
throw new IllegalArgumentException("Invalid age");
[Link] = age;
}

private String generateId() {


return [Link]().toString();
}
}
Initialization Order (Critical!)

public class InitDemo {


static { [Link]("1. Static block"); } // 1st

{ [Link]("2. Instance initializer"); } // 3rd

private String field = initField(); // 2nd (field initializers)

public InitDemo() {
[Link]("4. Constructor body");
}

private String initField() {


[Link](" -> Running field initializer");
return "value";
}

public static void main(String[] args) {


new InitDemo();
// Output order:
// 1. Static block
// -> Running field initializer
// 2. Instance initializer
// 4. Constructor body
}
}

4️⃣ Methods: Behavior Definition


Instance vs Static Methods

Feature Instance Method Static Method


Requires object? ✅ ( [Link]() ) ❌ ( [Link]() )
Access to this ? ✅ ❌
Can access instance fields? ✅ ❌
Polymorphic? ✅ (overriding) ❌ (hiding only)
Typical use Object behavior Utility/helper functions

public class MathUtils {


// ✅Appropriate static method
public static int max(int a, int b) {
return a > b ? a : b;
}

// ❌Anti-pattern: static method depending on instance state


private int baseValue;
public static int addToBase(int x) {
return baseValue + x; // COMPILE ERROR! Can't access instance field
}
}

Method Overloading Rules


public class Calculator {
public int add(int a, int b) { return a + b; }
public double add(double a, double b) { return a + b; }
public int add(int... numbers) { // Varargs (must be last parameter)
return [Link](numbers).sum();
}

// ❌ Ambiguous overload - won't compile


// public void process(int x) {}
// public void process(Integer x) {} // Autoboxing creates ambiguity

// ✅ Resolved by explicit casting


process((int) 5); // Calls primitive version
process((Integer)5); // Calls wrapper version
}

Pass-by-Value Semantics (Critical!)

public class PassByValueDemo {


public static void main(String[] args) {
int x = 10;
changePrimitive(x);
[Link](x); // 10 (unchanged) ✅
StringBuilder sb = new StringBuilder("Hello");
changeObject(sb);
[Link](sb); // "Hello World" ✅
(object mutated)

reassignObject(sb);
[Link](sb); // "Hello World" ✅ (reference unchanged)
}

static void changePrimitive(int num) {


num = 100; // Modifies LOCAL COPY only
}

static void changeObject(StringBuilder sb) {


[Link](" World"); // Mutates the OBJECT (reference points to same heap object)
}

static void reassignObject(StringBuilder sb) {


sb = new StringBuilder("New"); // Reassigns LOCAL REFERENCE only
}
}

💡 Java is ALWAYS pass-by-value:


Primitives: copy of value
Objects: copy of reference (not the object itself)

5️⃣ The this Keyword: Self-Reference


Usage Example Purpose
Disambiguate fields [Link] = name; When parameter shadows field
Constructor chaining this(name, 0); Reuse constructor logic
Usage Example Purpose
Return current object return this; Enable method chaining (fluent API)
Pass self to method [Link](this); Explicit self-reference

public class Person {


private String name;

public Person(String name) {


[Link] = name; // 'this' resolves shadowing
}

// Fluent interface using 'this'


public Person setName(String name) {
[Link] = name;
return this; // Enables chaining
}

public static void main(String[] args) {


Person p = new Person("Alice")
.setName("Bob") // Chaining via 'this'
.setAge(30);
}
}

6️⃣ The static Keyword: Class-Level Members


What static Means

Belongs to the class, not any instance


Shared across all instances
Initialized when class loads (before any object created)

public class Counter {


private static int instanceCount = 0; // Shared across all instances
private final int id;

public Counter() {
[Link] = ++instanceCount; // Thread-unsafe! (See Module 6 for fixes)
}

public static int getTotalInstances() {


return instanceCount; // Static method accesses static field
}

// ❌ Cannot access instance members from static context


// public static void printId() {
// [Link](id); // COMPILE ERROR!
// }
}

Static Import (Use Sparingly!)

// Instead of [Link]([Link](x,2) + [Link](y,2))


import static [Link].*;
double distance = sqrt(pow(x, 2) + pow(y, 2)); // Cleaner but can reduce readability

⚠️ Static Anti-Patterns:
Static mutable state → hidden dependencies, testing nightmares
Overuse of static utility classes → procedural design in OOP clothing

7️⃣ The final Keyword: Immutability & Intent


Context Meaning Example
Variable Reference can't be final List<String> list = new ArrayList<>(); list = null;
reassigned // ❌
Parameter Parameter can't be void process(final String s) { s = "new"; // ❌ }
reassigned
Method Method can't be overridden public final void validate() { ... }

Class Class can't be subclassed public final class String { ... }

Immutability Pattern (Production Ready)

public final class ImmutablePoint { // Class final → no subclassing


private final int x; // Fields final → no reassignment
private final int y;

public ImmutablePoint(int x, int y) {


this.x = x;
this.y = y;
}

// No setters → state never changes after construction

public int getX() { return x; } // Safe to expose (primitive)


public int getY() { return y; }

// For mutable objects: defensive copying


public final class User {
private final List<String> roles;

public User(List<String> roles) {


[Link] = [Link](roles); // Java 10+ defensive copy
}

public List<String> getRoles() {


return [Link](roles); // Prevent external mutation
}
}
}

✅ Benefits: Thread-safe by default, cacheable, easier reasoning

8️⃣ Packages & Imports: Organization


Package Naming Convention
// ✅
Reverse domain notation (globally unique)
package [Link];

// ❌ Avoid default package (no package declaration)


// ❌ Avoid java.* or javax.* prefixes (reserved)

Import Types

import [Link]; // Single-type import (preferred)


import [Link].*; // On-demand import (discouraged - hides dependencies)
import static [Link].*; // Static import (use sparingly)

Access Control Reminder

Classes must be public to be accessible outside package


Non-public classes can only have one per .java file (with matching filename)

🎯 What Is Object-Oriented Programming (OOP)?


OOP is a way of organizing code by modeling real-world things as objects that:

Hold data (called state or fields)


Perform actions (called behavior or methods)

In Java, everything revolves around classes (blueprints) and objects (actual instances built from those blueprints).

🔑 Core Idea: Instead of writing long lists of instructions (procedural code), you design smart objects that know
how to manage themselves.

1. Classes and Objects


✅ What is a Class?
A class is a template or blueprint that defines:

What data an object can hold (fields)


What actions it can perform (methods)

// Example: A 'Dog' class blueprint


public class Dog {
// Fields (state): data each dog will have
private String name;
private int age;

// Methods (behavior): what a dog can do


public void bark() {
[Link](name + " says: Woof!");
}
}

✅ What is an Object?
An object is a real, usable instance created from a class using the new keyword.
// Create actual dog objects
Dog myDog = new Dog(); // Object #1
Dog neighborDog = new Dog(); // Object #2

// Each has its own memory space → independent state

💡 Think of a class like a cookie cutter, and objects like the cookies you make with it. Same shape, but each
cookie is separate.

⚠️ Important Notes:
You cannot use a class directly—you must create an object ( new Dog() ).
Every time you use new , Java allocates memory on the heap for that object.
Objects are accessed via references (like remote controls pointing to TVs).

2. Encapsulation: Hiding Internal Details


Encapsulation means bundling data and methods together and controlling access to internal state.

🔒 Access Modifiers
Java gives you 4 levels of visibility:

Modifier Visible To
private Only inside the same class
(no keyword) → package-private Same package only
protected Same package + subclasses
public Everywhere

✅ Best Practice: Make fields private . Expose behavior—not raw data.


❌ Bad: Exposing Data
public class BankAccount {
public double balance; // DANGER! Anyone can change it!
}

// Usage:
BankAccount acc = new BankAccount();
[Link] = -1000000; // Allowed! 😱
✅ Good: Encapsulated Behavior
public class BankAccount {
private double balance; // Hidden

public void deposit(double amount) {


if (amount > 0) balance += amount;
}

public boolean withdraw(double amount) {


if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
}

public double getBalance() {


return balance; // Safe read-only access
}
}

🔑 Encapsulation isn’t about getters/setters—it’s about protecting object integrity.


If your class has public fields or setters that allow invalid states, you’ve broken encapsulation.

3. Constructors: Initializing Objects


A constructor is a special method that runs when you create an object with new . It sets up initial state.

Rules:

Same name as the class


No return type (not even void )
Called automatically with new

public class Person {


private String name;
private int age;

// Constructor
public Person(String name, int age) {
[Link] = name;
[Link] = age;
}
}

// Usage:
Person alice = new Person("Alice", 30); // Constructor runs here

Default Constructor

If you write no constructors, Java gives you a free no-arg one:

public class Car {


// Implicit: public Car() {}
}
Car c = new Car(); // Works!

But if you define any constructor, Java won’t give you the default one.

Constructor Chaining

Use this(...) to call another constructor from the first line:

public class Rectangle {


private int width, height;

public Rectangle() {
this(1, 1); // Calls next constructor
}
public Rectangle(int side) {
this(side, side);
}

public Rectangle(int w, int h) {


[Link] = w;
[Link] = h;
}
}

4. The this Keyword


this refers to the current object. Use it to:

Resolve naming conflicts between parameters and fields


Call other constructors ( this(...) )
Return the current object (for method chaining)

public class Student {


private String name;

public Student(String name) {


[Link] = name; // '[Link]' = field, 'name' = parameter
}

public Student setName(String name) {


[Link] = name;
return this; // Enables: [Link]("Bob").setGrade(90)...
}
}

5. Methods: Defining Behavior


Methods are functions inside a class that define what an object can do.

Instance Methods vs Static Methods

Instance Method Static Method


Belongs to An object The class itself
Called via [Link]() [Link]()

Can access Instance fields ( this.x ) Only static fields


Polymorphic? ✅ Yes (can be overridden) ❌ No

public class MathUtils {


// Static: utility function, no object needed
public static int add(int a, int b) {
return a + b;
}
}
// Usage:
int sum = [Link](2, 3); // No object created!

❌ Never mix static and instance logic unnecessarily. Static methods can’t participate in OOP polymorphism.

6. Method Overloading
You can have multiple methods with the same name if they have different parameters (type, number, or order).

public class Printer {


public void print(String text) { ... }
public void print(int number) { ... }
public void print(String text, int copies) { ... }
}

⚠️ Return type does not count for overloading!


int process() and void process() → compile error (ambiguous).

7. Pass-by-Value in Java
Java always passes by value—but what does that mean?

For primitives ( int , double , etc.): a copy of the value is passed.


For objects: a copy of the reference (memory address) is passed—not the object itself.

Example:

public static void main(String[] args) {


int x = 10;
changeNumber(x);
[Link](x); // Still 10 → value copied

StringBuilder sb = new StringBuilder("Hi");


changeText(sb);
[Link](sb); // "Hi!!" → object was mutated

reassignText(sb);
[Link](sb); // Still "Hi!!" → reference copy reassigned, original unchanged
}

static void changeNumber(int num) {


num = 999; // Changes local copy only
}

static void changeText(StringBuilder s) {


[Link]("!!"); // Mutates the actual object (same memory)
}

static void reassignText(StringBuilder s) {


s = new StringBuilder("Bye"); // Only changes local reference
}

🔑 Key Insight: You can mutate an object passed to a method, but you cannot reassign the caller’s reference.
8. The static Keyword
static means “belongs to the class, not to any object.”

Common Uses:

Constants: public static final double PI = 3.14159;


Utility methods: [Link]() , [Link]()
Counters shared across all instances

public class Counter {


private static int totalCreated = 0; // Shared by all objects

public Counter() {
totalCreated++;
}

public static int getTotal() {


return totalCreated;
}
}

⚠️ Avoid static mutable state in real applications—it makes testing hard and causes bugs in multi-threaded
code.

9. The final Keyword


final means “this cannot change.” It applies to:

Context Meaning
Variable Reference can’t be reassigned
Method Cannot be overridden by subclasses
Class Cannot be extended (no inheritance)

Examples:

final int MAX_SIZE = 100; // Constant


final List<String> items = new ArrayList<>();
[Link]("OK"); // ✅ Mutating object is fine
items = null; // ❌ Reassigning reference → compile error

public final class String { ... } // Can't subclass String

public final void validate() { ... } // Subclasses can't override

✅ Use final on fields to enforce immutability.


✅ Use final on classes/methods to prevent unintended extension.

10. Packages and Imports


Packages = Folders for Classes
Organize related classes and avoid naming conflicts.

package [Link]; // Must match folder structure

public class Account { ... }

File path: src/com/mycompany/bank/[Link]

Imports = Using Other Classes

Instead of writing [Link] every time:

import [Link]; // Now just use 'ArrayList'

public class Bank {


private ArrayList<Account> accounts;
}

✅ Always prefer explicit imports ( import [Link] ) over wildcard ( import [Link].* ).

🧠 Summary: The Big Picture


Concept Purpose Best Practice
Class Blueprint for objects Keep focused (Single Responsibility)
Object Runtime instance Create via new , manage lifecycle
Encapsulation Protect internal state private fields, expose behavior

Constructor Initialize object Validate inputs, chain with this()


this Refer to current object Resolve naming, enable chaining
Methods Define behavior Prefer instance over static
Overloading Multiple method signatures Keep semantics consistent
Pass-by-value How arguments work Understand reference copying
static Class-level members Use for constants/utils only
final Prevent change Enforce immutability & design intent
Packages Organize code Use reverse domain naming

Module 2: Inheritance & Polymorphism — The Power of Subtyping


This module unlocks Java’s ability to model hierarchical relationships and write flexible, reusable code. Master
these concepts to avoid fragile designs and leverage true OOP power.

1️⃣ Inheritance: The "IS-A" Relationship


Core Concept

Inheritance allows a class (subclass) to reuse and extend another class (superclass).

✅ Use only when there’s a true "IS-A" relationship:


Dog IS-A Animal → ✅ Good
Car IS-A Engine → ❌ Bad (should be HAS-A → use composition)

Syntax & Mechanics

// Superclass (parent)
public class Animal {
protected String name; // 'protected' = accessible to subclasses

public Animal(String name) {


[Link] = name;
}

public void eat() {


[Link](name + " is eating");
}

// Method to be overridden
public void makeSound() {
[Link]("Some generic animal sound");
}
}

// Subclass (child)
public class Dog extends Animal { // 'extends' = inheritance keyword
public Dog(String name) {
super(name); // MUST call superclass constructor first
}

// Override superclass method


@Override
public void makeSound() {
[Link]("Woof! Woof!");
}

// Add subclass-specific behavior


public void fetch() {
[Link](name + " is fetching the ball");
}
}

Key Rules

Rule Explanation
Single Inheritance Java allows only one direct superclass ( class Dog extends Animal ✅ , class Dog
extends Animal, Pet ❌ )
Constructor First line of subclass constructor must call super(...) or this(...)
Chaining
super Keyword Access superclass members: [Link]() , [Link]
@Override Mandatory for clarity—catches signature errors at compile time
Annotation

When NOT to Use Inheritance

// ❌ BAD: Square IS-NOT-A Rectangle (violates Liskov Substitution)


class Rectangle {
protected int width, height;
public void setWidth(int w) { width = w; }
public void setHeight(int h) { height = h; }
public int getArea() { return width * height; }
}

class Square extends Rectangle {


@Override
public void setWidth(int w) {
[Link](w);
[Link](w); // Forced coupling!
}

@Override
public void setHeight(int h) {
[Link](h);
[Link](h);
}
}

// Problem: Code expecting Rectangle breaks with Square


void test(Rectangle r) {
[Link](5);
[Link](4);
assert [Link]() == 20; // Fails if r is Square!
}

💡 Golden Rule: If you can’t substitute a subclass anywhere a superclass is expected without breaking
behavior, don’t use inheritance.

2️⃣ Method Overriding vs. Overloading


Feature Overriding Overloading
Purpose Redefine inherited behavior Provide multiple method signatures
Class Scope Between superclass and subclass Within same class (or subclass)
Signature Must match exactly (name + params) Must differ in params (type/number/order)
Return Type Must be covariant (same or subclass) Can be different
Access Modifier Cannot be more restrictive Independent
Binding Runtime (dynamic) Compile-time (static)

Overriding Example

class Vehicle {
public Object start() { // Returns Object
return "Vehicle started";
}
}

class Car extends Vehicle {


@Override
public String start() { // ✅
Covariant return: String is subclass of Object
return "Car engine started";
}

// ❌ Compile error: more restrictive access


// protected void stop() { } // If superclass method is public
}

Overloading Example (in same class)

class Calculator {
public int add(int a, int b) { return a + b; }
public double add(double a, double b) { return a + b; } // Different param types
public int add(int a, int b, int c) { return a + b + c; } // Different param count
}

3️⃣ Polymorphism: One Interface, Many Implementations


Core Concept

Polymorphism lets you treat different objects uniformly through a common supertype.

“Program to an interface, not an implementation.” — Gang of Four

How It Works

// Common supertype
Animal myPet;

// Assign different subtypes


myPet = new Dog("Buddy");
[Link](); // Output: Woof! Woof!

myPet = new Cat("Whiskers");


[Link](); // Output: Meow!

// Works with collections


List<Animal> zoo = [Link](new Dog("Rex"), new Cat("Luna"));
[Link](Animal::makeSound); // Calls correct version for each

Runtime Binding (Dynamic Dispatch)

Method called is determined by actual object type at runtime, not reference type
Enabled by JVM’s virtual method table (vtable)

Animal a = new Dog("Max");


[Link](); // Calls [Link]() even though reference is Animal

Benefits

Extensibility: Add new subclasses without changing existing code


Decoupling: Client code depends only on supertype
Testability: Easy to mock subclasses

4️⃣ The Object Class: Root of All Classes


Every Java class implicitly extends Object . You inherit these methods:
Method Purpose Must Override?
toString() String representation ✅ Almost always
equals(Object) Compare content equality ✅ If using in collections
hashCode() Support hash-based collections ✅ If overriding equals()
clone() Create copy Rarely (use copy constructors instead)
finalize() Cleanup before GC ❌ Deprecated—never use
Essential Overrides

public class Person {


private final String name;
private final int age;

// ...

@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != [Link]()) return false;
Person person = (Person) o;
return age == [Link] && [Link](name, [Link]);
}

@Override
public int hashCode() {
return [Link](name, age); // Always use same fields as equals()
}
}

⚠️ equals() / hashCode() Contract:

If [Link](b) → [Link]() == [Link]()


If [Link]() != [Link]() → [Link](b) must be false

5️⃣ Abstract Classes: Partial Implementation


When to Use

When you have common code to share among subclasses


When you want to enforce a contract but can’t fully implement it
When you need non-public methods in your contract (interfaces can’t have private / protected methods until
Java 9+)

Syntax

// Abstract class = cannot be instantiated directly


public abstract class Shape {
protected String color;
public Shape(String color) {
[Link] = color;
}

// Concrete method (shared implementation)


public String getColor() {
return color;
}

// Abstract method = must be implemented by subclasses


public abstract double calculateArea();

// Template Method pattern


public final void draw() {
[Link]("Drawing a " + color + " shape");
[Link]("Area: " + calculateArea()); // Calls subclass implementation
}
}

// Concrete subclass
public class Circle extends Shape {
private double radius;

public Circle(String color, double radius) {


super(color);
[Link] = radius;
}

@Override
public double calculateArea() {
return [Link] * radius * radius;
}
}

Abstract Class vs Interface

Feature Abstract Class Interface


State Can have fields Only constants ( public static final )
Constructors Yes No
Method Access private / protected / public public only (until Java 9)

Inheritance Single Multiple


Use Case "IS-A" with shared code "CAN-DO" capability contract

6️⃣ Upcasting and Downcasting


Upcasting (Implicit - Safe)

Treating a subclass as its superclass → always safe

Dog dog = new Dog("Buddy");


Animal animal = dog; // ✅
Implicit upcast - no cast needed
[Link](); // Still calls [Link]() (polymorphism)

Downcasting (Explicit - Risky)


Treating a superclass reference as a subclass → requires cast and check

Animal animal = new Dog("Rex");

// ❌ Dangerous - ClassCastException if animal isn't actually a Dog


Dog dog = (Dog) animal;

// ✅Safe - check first


if (animal instanceof Dog) {
Dog dog = (Dog) animal;
[Link](); // Now safe to call Dog-specific methods
}

// Java 14+ Pattern Matching (cleaner)


if (animal instanceof Dog d) {
[Link](); // 'd' is already cast to Dog
}

Why Downcasting Is a Code Smell

Frequent instanceof checks often indicate poor design:

// ❌Anti-pattern: violates polymorphism


void processAnimal(Animal a) {
if (a instanceof Dog) {
((Dog) a).fetch();
} else if (a instanceof Cat) {
((Cat) a).scratch();
}
}

// ✅ Better: add common method to superclass/interface


abstract class Animal {
public abstract void performTrick(); // Each subclass implements differently
}

7️⃣ The instanceof Operator


Checks if an object is an instance of a class/interface.

Basic Usage

Animal a = new Cat("Fluffy");


[Link](a instanceof Cat); // true
[Link](a instanceof Animal); // true (inheritance chain)
[Link](a instanceof Dog); // false

With Null

Animal a = null;
[Link](a instanceof Cat); // false (never throws NPE)

Modern Pattern Matching (Java 14+)

// Before Java 14
if (obj instanceof String) {
String s = (String) obj;
[Link]([Link]());
}

// Java 14+
if (obj instanceof String s) {
[Link]([Link]()); // 's' is already cast
}

// Java 17+ with null check


if (obj instanceof String s && ![Link]()) {
// ...
}

🧪 Module 2 Exercises
1. Shape Hierarchy
Create abstract Shape with calculateArea() . Implement Circle , Rectangle , Triangle .
Store in List<Shape> and calculate total area
Override toString() to show shape type and area
2. Employee Payroll System
Build Employee abstract class with calculatePay() .
FullTimeEmployee : salary + bonus
Contractor : hourly rate × hours
Demonstrate polymorphism with mixed employee list
3. Fix the Square-Rectangle Problem
Refactor using composition instead of inheritance:

class Rectangle {
private final Dimensions dimensions;
// ...
}

class Square {
private final Rectangle rectangle;
// Delegate to rectangle but enforce width == height
}

4. Template Method Pattern


Create DataProcessor abstract class with template method:

public final void process() {


loadData();
validateData();
transformData(); // Abstract - implemented by subclasses
saveData();
}

Implement CSVDataProcessor and JSONDataProcessor .


5. Equals/HashCode Deep Dive
Create Book class with title , author , isbn .
Implement equals() / hashCode() correctly
Test with HashSet and HashMap
✅ Module 2 Mastery Checklist
Can explain why Java has single inheritance but multiple interface implementation
Understand when to use abstract class vs interface
Never use downcasting without instanceof check (or pattern matching)
Always use @Override annotation
Can implement equals() / hashCode() that satisfies the contract
Recognize when inheritance violates Liskov Substitution Principle
Design systems where new subclasses require zero changes to existing code
Understand that polymorphism happens at runtime, overloading at compile time

💡 Critical Insight: Inheritance is about behavioral substitution, not just code reuse. If you’re inheriting just to
reuse code, consider composition instead (Module 6).

Module 3: Modern Java OOP Features — Evolution Beyond Basics


Java has evolved significantly since its early days. This module covers post-Java 8 features that reshape how we
design object-oriented systems—making code safer, more expressive, and less verbose, while preserving OOP
principles.

1️⃣ Interfaces: From Contracts to Capabilities (Java 8+)


Pre-Java 8: Pure Contracts

// Java 7 and earlier


public interface Drawable {
void draw(); // Implicitly public abstract
// NO method implementations allowed!
}

Java 8: Default and Static Methods

Interfaces can now provide concrete behavior without breaking existing implementations.

✅ Default Methods ( default )


Provide backward-compatible API evolution
Allow adding methods to interfaces without forcing all implementers to change

public interface Vehicle {


void start();
void stop();

// New capability added in v2 - existing classes still compile!


default void honk() {
[Link]("Beep beep!");
}

// Multiple inheritance of behavior (with rules)


default void emergencyStop() {
honk();
stop();
}
}
public class Car implements Vehicle {
public void start() { [Link]("Car started"); }
public void stop() { [Link]("Car stopped"); }
// No need to implement honk() or emergencyStop()!
}

⚠️ Diamond Problem Resolution:


If a class inherits the same default method from multiple interfaces, you must override it:

interface A { default void hello() { [Link]("A"); } }


interface B { default void hello() { [Link]("B"); } }

class C implements A, B {
@Override
public void hello() {
[Link](); // Explicitly choose A's version
// or [Link]();
// or write new implementation
}
}

✅ Static Methods in Interfaces


Utility methods tied to the interface
Called via [Link]()

public interface MathOperations {


static int max(int a, int b) {
return a > b ? a : b;
}

static <T extends Comparable<T>> T max(T a, T b) {


return [Link](b) > 0 ? a : b;
}
}

// Usage:
int biggest = [Link](5, 10);

Java 9+: Private Methods in Interfaces

Share common code between default/static methods


Not part of the public contract

public interface DatabaseConnection {


default void connect() {
validateConfig();
establishConnection();
logConnection();
}

default void reconnect() {


closeConnection();
validateConfig(); // Reused logic
establishConnection();
logConnection(); // Reused logic
}
// Private helper - only visible within interface
private void validateConfig() {
// Validation logic
}

private void logConnection() {


[Link]("Connected to DB");
}

// Abstract methods (must be implemented by classes)


void establishConnection();
void closeConnection();
}

💡 Modern Interface Design Principle:


Interfaces are now capability contracts with optional behavior, not just pure abstractions.

2️⃣ Records: Concise Data Carriers (Java 14+)


The Problem: Boilerplate for Data Classes

Before records, simple data classes required massive boilerplate:

// Traditional approach (pre-Java 14)


public final class Point {
private final int x;
private final int y;

public Point(int x, int y) {


this.x = x;
this.y = y;
}

public int x() { return x; }


public int y() { return y; }

@Override
public boolean equals(Object o) { /* 10+ lines */ }

@Override
public int hashCode() { /* 5+ lines */ }

@Override
public String toString() { /* 3+ lines */ }
}

✅ Records: Automatic Boilerplate Elimination


// Java 14+ record
public record Point(int x, int y) {}

This single line automatically provides:

private final fields


Public accessor methods ( x() , y() )
Canonical constructor
equals() , hashCode() , toString() implementations
Immutability by default

Advanced Record Features

Custom Constructors

public record Person(String name, int age) {


// Compact custom constructor (validates inputs)
public Person {
if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
if (name == null || [Link]())
throw new IllegalArgumentException("Name required");
}

// Full custom constructor (rarely needed)


public Person(String fullName) {
this(fullName, 0); // Delegates to canonical constructor
}
}

Additional Methods

public record Rectangle(Point topLeft, Point bottomRight) {


// Derived property
public double area() {
return [Link](bottomRight.x() - topLeft.x()) *
[Link](topLeft.y() - bottomRight.y());
}

// Static factory method


public static Rectangle square(int side) {
return new Rectangle(new Point(0, 0), new Point(side, side));
}
}

When to Use Records

Use Case Good? Why


DTOs / Value Objects ✅ Perfect for immutable data transfer
Entities with identity ❌ Records use structural equality ( equals based on fields)
Classes needing inheritance ❌ Records are implicitly final
Classes with complex behavior ⚠️ Only add simple derived methods

🔑 Record Philosophy: "Records are nominal tuples"—they model pure data, not behavior-rich objects.

3️⃣ Sealed Classes: Controlled Inheritance (Java 17+)


The Problem: Uncontrolled Subclassing

Traditional inheritance allows anyone to extend your class, leading to:

Security risks
Broken assumptions in pattern matching
Unmaintainable hierarchies
// Before sealed classes
public abstract class Shape { } // Anyone can extend!

// Somewhere in another package...


class SecretShape extends Shape { } // Unexpected subclass!

✅ Sealed Classes: Enum-Like Hierarchies


Restrict which classes can extend or implement your type.

// Sealed superclass - only listed classes can extend


public sealed class Shape
permits Circle, Rectangle, Triangle { }

// Concrete subclasses must be final, sealed, or non-sealed


final class Circle extends Shape {
private final double radius;
public Circle(double radius) { [Link] = radius; }
}

final class Rectangle extends Shape {


private final double width, height;
public Rectangle(double w, double h) { width = w; height = h; }
}

// Non-sealed allows further extension (rare)


non-sealed class Triangle extends Shape { }
final class EquilateralTriangle extends Triangle { }

Sealed Interfaces

Same concept applies to interfaces:

public sealed interface Expr


permits ConstantExpr, AddExpr, MultiplyExpr { }

record ConstantExpr(int value) implements Expr {}


record AddExpr(Expr left, Expr right) implements Expr {}
record MultiplyExpr(Expr left, Expr right) implements Expr {}

Benefits for Pattern Matching

Sealed types enable exhaustive pattern matching:

// Java 21+ pattern matching with switch


double evaluate(Expr expr) {
return switch (expr) {
case ConstantExpr c -> [Link]();
case AddExpr add -> evaluate([Link]()) + evaluate([Link]());
case MultiplyExpr mul -> evaluate([Link]()) * evaluate([Link]());
// Compiler knows these are ALL possible cases → no default needed!
};
}

💡 Sealed Class Philosophy: "I know all possible subtypes of this hierarchy at compile time."
4️⃣ Pattern Matching: Safer Type Queries (Java 16+)
Traditional Type Checking (Verbose & Error-Prone)

// Pre-Java 16
if (obj instanceof String) {
String s = (String) obj; // Manual cast
[Link]([Link]());
}

// Nested checks become messy


if (obj instanceof Map) {
Map<?, ?> map = (Map<?, ?>) obj;
if ([Link]("name")) {
Object name = [Link]("name");
if (name instanceof String) {
String s = (String) name;
// ...
}
}
}

✅ Pattern Matching for instanceof (Java 16+)


// Clean, safe, no manual casting
if (obj instanceof String s) {
[Link]([Link]()); // 's' is already String
}

// Works with null


if (obj instanceof String s && [Link]() > 0) {
// Safe - short-circuit prevents NPE
}

✅ Switch Pattern Matching (Java 21+)


Exhaustive, type-safe switching over object types:

// With sealed Expr hierarchy from earlier


String describe(Expr expr) {
return switch (expr) {
case ConstantExpr(var value) -> "Constant: " + value;
case AddExpr(var left, var right) ->
"Add(" + describe(left) + ", " + describe(right) + ")";
case MultiplyExpr(var left, var right) ->
"Multiply(" + describe(left) + ", " + describe(right) + ")";
};
}

Guarded Patterns

Add conditions to patterns:

String process(Object obj) {


return switch (obj) {
case Integer i when i > 0 -> "Positive integer: " + i;
case Integer i -> "Non-positive integer: " + i;
case String s when [Link]() > 10 -> "Long string";
case String s -> "Short string";
case null -> "Null value";
default -> "Unknown type";
};
}

🔑 Pattern Matching Philosophy: "Deconstruct objects safely while checking their type."

5️⃣ Text Blocks: Multi-line Strings (Java 15+)


While not strictly OOP, text blocks improve readability of string-heavy classes:

// Before Java 15
String json = "{\n" +
" \"name\": \"John\",\n" +
" \"age\": 30\n" +
"}";

// Java 15+ text blocks


String json = """
{
"name": "John",
"age": 30
}
""";

// Useful in toString() methods


@Override
public String toString() {
return """
Person{
name='%s',
age=%d
}
""".formatted(name, age);
}

🧪 Module 3 Exercises
1. Modern Shape Hierarchy
Refactor Module 2’s shape system using:
Sealed Shape class
Records for concrete shapes ( Circle , Rectangle )
Pattern matching calculateArea() method
2. API Response DTOs
Create records for REST API responses:

record UserResponse(long id, String name, String email) {}


record ErrorResponse(int code, String message) {}

Add validation in compact constructors.


3. Event Processing System
Build sealed Event interface with implementations:
sealed interface Event permits UserCreated, OrderPlaced, PaymentProcessed
record UserCreated(String userId, String email) implements Event {}
// ... others

Process events using switch pattern matching.


4. Database Query Builder
Use records for query components:

record SelectClause(List<String> columns) {}


record WhereClause(String condition) {}
record Query(SelectClause select, WhereClause where) {
public String toSql() { /* build SQL */ }
}

5. Refactor Legacy Code


Take a traditional data class from old codebase and convert to record.
Identify what behaviors should remain vs. what becomes pure data.

✅ Module 3 Mastery Checklist


Can explain why default methods were added to interfaces (API evolution)
Understand when to use records vs traditional classes
Design sealed hierarchies that enable exhaustive pattern matching
Replace instanceof + cast chains with pattern matching
Know that records are not replacements for all classes—only pure data carriers
Resolve diamond problem when inheriting conflicting default methods
Use private interface methods to reduce code duplication in default methods
Recognize that sealed classes + pattern matching = algebraic data types (like in functional languages)

🔮 The Big Picture: Modern Java OOP Philosophy


Era Philosophy Key Features
Java 1-7 "Inheritance + Encapsulation" Classes, interfaces (pure contracts)
Java 8-11 "Functional + OOP Hybrid" Default methods, lambdas, streams
Java 14-21 "Data-Oriented + Exhaustive Types" Records, sealed classes, pattern matching

💡 Modern Best Practice:


Use records for immutable data
Use sealed classes for restricted hierarchies
Use interfaces with default methods for evolving APIs
Use pattern matching instead of instanceof chains

Module 5: Essential Design Patterns — Proven Solutions to Common OOP


Problems
Design patterns are reusable, battle-tested solutions to recurring design problems in object-oriented programming.
They’re not code snippets—you can’t copy-paste them—but templates for solving specific problems in a flexible,
maintainable way.

This module covers the most essential patterns you’ll use daily as a professional Java developer, with deep
explanations, real-world examples, and clear guidance on when to use them (and when NOT to).

🧠 Pattern Categories Overview


Category Purpose Key Patterns
Creational Object creation logic Factory Method, Abstract Factory, Builder, Singleton
Structural Object composition & structure Adapter, Decorator, Composite, Facade
Behavioral Object interaction & responsibility Strategy, Observer, Command, Template Method

⚠️ Critical Warning: Don’t use patterns just because you can! Over-engineering is worse than no patterns.
Use them only when they solve a real problem.

🔨 CREATATIONAL PATTERNS
Solve problems related to object creation, making systems independent of how objects are created, composed,
and represented.

1. Factory Method Pattern


🎯 Problem
You need to create objects, but the exact type isn’t known at compile time, or you want to centralize object
creation logic.

❌ Without Factory Method


// Tight coupling to concrete classes
public class PaymentProcessor {
public void processPayment(String paymentType, double amount) {
if ("CREDIT_CARD".equals(paymentType)) {
CreditCardPayment payment = new CreditCardPayment(); // Hardcoded!
[Link](amount);
} else if ("PAYPAL".equals(paymentType)) {
PayPalPayment payment = new PayPalPayment(); // Hardcoded!
[Link](amount);
}
// Adding new payment method requires modifying this class!
}
}

✅ With Factory Method


// 1. Define product interface
public interface Payment {
void process(double amount);
}

// 2. Concrete products
public class CreditCardPayment implements Payment {
public void process(double amount) {
[Link]("Processing credit card payment: $" + amount);
}
}

public class PayPalPayment implements Payment {


public void process(double amount) {
[Link]("Processing PayPal payment: $" + amount);
}
}

// 3. Creator with factory method


public abstract class PaymentProcessor {
// Factory method - subclasses decide which product to create
protected abstract Payment createPayment();

// Common algorithm using the product


public void processPayment(double amount) {
Payment payment = createPayment();
[Link](amount);
}
}

// 4. Concrete creators
public class CreditCardProcessor extends PaymentProcessor {
@Override
protected Payment createPayment() {
return new CreditCardPayment();
}
}

public class PayPalProcessor extends PaymentProcessor {


@Override
protected Payment createPayment() {
return new PayPalPayment();
}
}

// Usage
PaymentProcessor processor = new CreditCardProcessor();
[Link](100.0); // Output: Processing credit card payment: $100.0

🔄 Alternative: Simple Factory (Not GoF, but useful)


public class PaymentFactory {
public static Payment createPayment(String type) {
switch ([Link]()) {
case "credit_card": return new CreditCardPayment();
case "paypal": return new PayPalPayment();
default: throw new IllegalArgumentException("Unknown payment type");
}
}
}

// Usage
Payment payment = [Link]("credit_card");
[Link](100.0);

📌 When to Use Factory Method


✅ You don’t know the exact types of objects your code should work with
✅ You want to provide users of your library/framework a way to extend internal components
✅ You want to centralize complex object creation logic
🚫 When NOT to Use
❌ You only have one concrete class
❌ Object creation is simple ( new MyClass() )

2. Builder Pattern
🎯 Problem
You need to construct complex objects with many optional parameters, avoiding telescoping constructors and
ensuring immutability.

❌ Without Builder: Telescoping Constructor Hell


public class User {
private final String firstName;
private final String lastName;
private final int age;
private final String email;
private final String phone;
private final String address;

// Constructor with all parameters


public User(String firstName, String lastName, int age, String email, String phone, String
address) {
[Link] = firstName;
[Link] = lastName;
[Link] = age;
[Link] = email;
[Link] = phone;
[Link] = address;
}

// Need multiple constructors for optional parameters


public User(String firstName, String lastName) {
this(firstName, lastName, 0, null, null, null);
}

public User(String firstName, String lastName, String email) {


this(firstName, lastName, 0, email, null, null);
}
// ... many more combinations needed!
}

✅ With Builder
public class User {
private final String firstName;
private final String lastName;
private final int age;
private final String email;
private final String phone;
private final String address;
// Private constructor - only Builder can create instances
private User(Builder builder) {
[Link] = [Link];
[Link] = [Link];
[Link] = [Link];
[Link] = [Link];
[Link] = [Link];
[Link] = [Link];
}

// Static nested Builder class


public static class Builder {
private final String firstName; // Required
private final String lastName; // Required
private int age; // Optional
private String email; // Optional
private String phone; // Optional
private String address; // Optional

public Builder(String firstName, String lastName) {


[Link] = firstName;
[Link] = lastName;
}

public Builder age(int age) {


[Link] = age;
return this; // Return this for method chaining
}

public Builder email(String email) {


[Link] = email;
return this;
}

public Builder phone(String phone) {


[Link] = phone;
return this;
}

public Builder address(String address) {


[Link] = address;
return this;
}

public User build() {


// Add validation here if needed
return new User(this);
}
}

// Getters...
}

// Usage - clean, readable, and flexible


User user = new [Link]("John", "Doe")
.age(30)
.email("john@[Link]")
.phone("123-456-7890")
.build();
📌 When to Use Builder
✅ Class has 4+ parameters (especially with optional ones)
✅ You want immutable objects with complex construction
✅ You need to validate object state during construction
✅ Parameters are mostly optional
🚫 When NOT to Use
❌ Simple objects with 1-2 parameters
❌ Objects that are naturally mutable

3. Singleton Pattern
🎯 Problem
You need exactly one instance of a class throughout your application (e.g., configuration manager, database
connection pool).

❌ Naive Singleton (Thread-Unsafe)


public class DatabaseConnection {
private static DatabaseConnection instance;

private DatabaseConnection() {} // Private constructor

public static DatabaseConnection getInstance() {


if (instance == null) {
instance = new DatabaseConnection(); // Race condition!
}
return instance;
}
}

✅ Thread-Safe Singleton Options


Option 1: Eager Initialization (Simple, thread-safe)

public class DatabaseConnection {


private static final DatabaseConnection INSTANCE = new DatabaseConnection();

private DatabaseConnection() {}

public static DatabaseConnection getInstance() {


return INSTANCE;
}
}

Option 2: Double-Checked Locking (Lazy + Thread-safe)

public class DatabaseConnection {


private static volatile DatabaseConnection instance;

private DatabaseConnection() {}

public static DatabaseConnection getInstance() {


if (instance == null) {
synchronized ([Link]) {
if (instance == null) {
instance = new DatabaseConnection();
}
}
}
return instance;
}
}

Option 3: Enum Singleton (Most robust - recommended by Joshua Bloch)

public enum DatabaseConnection {


INSTANCE;

public void connect() {


// Connection logic
}
}

// Usage
[Link]();

📌 When to Use Singleton


✅ True single-instance requirement (logging, configuration, thread pools)
✅ Global access point is genuinely needed
🚫 When NOT to Use (Anti-Pattern Territory)
❌ To avoid passing objects as parameters (dependency injection is better)
❌ For "utility" classes (use static methods instead)
❌ In testable code (singletons make unit testing difficult)
❌ When you think you need it but actually don't (most common mistake!)
💡 Modern Alternative: Use dependency injection frameworks (Spring, Guice) with singleton scope instead of
manual singleton implementation.

🏗️ STRUCTURAL PATTERNS
Deal with object composition—how classes and objects are structured to form larger structures.

4. Adapter Pattern
🎯 Problem
You need to use an existing class, but its interface doesn’t match what you need. Convert the interface of a class
into another interface clients expect.

Real-World Analogy
Electrical plug adapter: Converts European plug to fit American outlet.

Example: Legacy System Integration


// Legacy system interface (can't modify)
public class LegacyPaymentProcessor {
public void processCreditCard(String cardNumber, double amount) {
[Link]("Legacy: Processing card " + cardNumber + " for $" + amount);
}

public void processPayPal(String email, double amount) {


[Link]("Legacy: Processing PayPal " + email + " for $" + amount);
}
}

// Modern interface we want to use


public interface PaymentProcessor {
void process(PaymentMethod method, double amount);
}

// Payment method abstraction


public interface PaymentMethod {
String getType();
String getIdentifier();
}

public class CreditCard implements PaymentMethod {


private String cardNumber;
public CreditCard(String cardNumber) { [Link] = cardNumber; }
public String getType() { return "CREDIT_CARD"; }
public String getIdentifier() { return cardNumber; }
}

// Adapter: Makes LegacyPaymentProcessor look like PaymentProcessor


public class LegacyPaymentAdapter implements PaymentProcessor {
private LegacyPaymentProcessor legacyProcessor;

public LegacyPaymentAdapter(LegacyPaymentProcessor legacyProcessor) {


[Link] = legacyProcessor;
}

@Override
public void process(PaymentMethod method, double amount) {
switch ([Link]()) {
case "CREDIT_CARD":
[Link]([Link](), amount);
break;
case "PAYPAL":
[Link]([Link](), amount);
break;
default:
throw new IllegalArgumentException("Unsupported payment method");
}
}
}

// Usage
LegacyPaymentProcessor legacy = new LegacyPaymentProcessor();
PaymentProcessor modern = new LegacyPaymentAdapter(legacy);

PaymentMethod card = new CreditCard("1234-5678-9012-3456");


[Link](card, 100.0); // Uses legacy system through modern interface

📌 When to Use Adapter


✅ Integrating third-party libraries with incompatible interfaces
✅ Working with legacy code you can’t modify
✅ Creating reusable components that need to work with various interfaces
🚫 When NOT to Use
❌ You control both interfaces (just make them compatible directly)
❌ Simple one-to-one method mapping (might be overkill)

5. Decorator Pattern
🎯 Problem
You need to add responsibilities to objects dynamically without subclassing. Attach additional responsibilities
to an object dynamically.

Real-World Analogy
Coffee with add-ons: Start with basic coffee, then add milk, sugar, whipped cream—each addition wraps the previous.

Example: Stream Processing Pipeline

// Component interface
public interface InputStream {
byte[] read();
void close();
}

// Concrete component
public class FileInputStream implements InputStream {
private String filename;

public FileInputStream(String filename) {


[Link] = filename;
}

@Override
public byte[] read() {
// Read file bytes
return "file content".getBytes();
}

@Override
public void close() {
[Link]("File closed");
}
}

// Base decorator
public abstract class InputStreamDecorator implements InputStream {
protected InputStream wrappedStream;

public InputStreamDecorator(InputStream stream) {


[Link] = stream;
}

@Override
public void close() {
[Link]();
}
}

// Concrete decorators
public class BufferedInputStream extends InputStreamDecorator {
public BufferedInputStream(InputStream stream) {
super(stream);
}

@Override
public byte[] read() {
[Link]("Adding buffering...");
return [Link]();
}
}

public class GZIPInputStream extends InputStreamDecorator {


public GZIPInputStream(InputStream stream) {
super(stream);
}

@Override
public byte[] read() {
byte[] data = [Link]();
[Link]("Compressing with GZIP...");
return compress(data); // Compression logic
}

private byte[] compress(byte[] data) {


return "compressed".getBytes();
}
}

// Usage - compose behaviors dynamically


InputStream stream = new GZIPInputStream(
new BufferedInputStream(
new FileInputStream("[Link]")
)
);

byte[] data = [Link]();


// Output:
// Adding buffering...
// Compressing with GZIP...

Java Built-in Example


Java’s I/O streams are the classic example:

BufferedReader reader = new BufferedReader(


new InputStreamReader(
new FileInputStream("[Link]")
)
);

📌 When to Use Decorator


✅ You need to add/remove responsibilities at runtime
✅ You want to avoid explosion of subclasses for every combination of features
✅ Responsibilities can be layered (like stream processing)
🚫 When NOT to Use
❌ The added responsibilities are static (use inheritance instead)
❌ You only have one or two combinations of features

6. Composite Pattern
🎯 Problem
You need to treat individual objects and compositions of objects uniformly. Compose objects into tree
structures to represent part-whole hierarchies.

Real-World Analogy
File system: Files and directories can both be treated as "filesystem items"—you can delete, move, or get size of both.

Example: Graphic Drawing System

// Component interface
public interface Graphic {
void draw();
void add(Graphic graphic); // Only meaningful for composites
void remove(Graphic graphic);
Graphic getChild(int index);
}

// Leaf: Individual object


public class Circle implements Graphic {
private int x, y, radius;

public Circle(int x, int y, int radius) {


this.x = x; this.y = y; [Link] = radius;
}

@Override
public void draw() {
[Link]("Drawing circle at (" + x + "," + y + ") with radius " + radius);
}

// Leaf implementations throw UnsupportedOperationException


@Override
public void add(Graphic graphic) {
throw new UnsupportedOperationException("Cannot add to leaf");
}

@Override
public void remove(Graphic graphic) {
throw new UnsupportedOperationException("Cannot remove from leaf");
}

@Override
public Graphic getChild(int index) {
throw new UnsupportedOperationException("Leaf has no children");
}
}

// Composite: Container of components


public class Group implements Graphic {
private List<Graphic> children = new ArrayList<>();

@Override
public void draw() {
[Link]("Drawing group with " + [Link]() + " children:");
for (Graphic child : children) {
[Link](); // Recursively draw all children
}
}

@Override
public void add(Graphic graphic) {
[Link](graphic);
}

@Override
public void remove(Graphic graphic) {
[Link](graphic);
}

@Override
public Graphic getChild(int index) {
return [Link](index);
}
}

// Usage
Graphic circle1 = new Circle(10, 20, 5);
Graphic circle2 = new Circle(30, 40, 8);
Graphic circle3 = new Circle(50, 60, 3);

Group group1 = new Group();


[Link](circle1);
[Link](circle2);

Group mainGroup = new Group();


[Link](group1);
[Link](circle3);

[Link](); // Draws entire hierarchy uniformly

📌 When to Use Composite


✅ You have tree-like hierarchical structures
✅ You want clients to treat individual and composite objects the same way
✅ Part-whole relationships are fundamental to your domain
🚫 When NOT to Use
❌ Your hierarchy is flat (no nesting)
❌ Individual and composite objects have very different interfaces

🎭 BEHAVIORAL PATTERNS
Focus on communication between objects—how objects interact and distribute responsibility.
7. Strategy Pattern
🎯 Problem
You have multiple algorithms for accomplishing the same task, and you want to select the algorithm at runtime.
Define a family of algorithms, encapsulate each one, and make them interchangeable.

Real-World Analogy
Navigation app: Different routing strategies (fastest route, shortest distance, avoid highways)—user selects strategy at
runtime.

Example: Sorting Algorithms

// Strategy interface
public interface SortStrategy {
<T extends Comparable<T>> void sort(List<T> list);
}

// Concrete strategies
public class BubbleSortStrategy implements SortStrategy {
@Override
public <T extends Comparable<T>> void sort(List<T> list) {
[Link]("Using bubble sort");
// Bubble sort implementation
[Link](list); // Simplified
}
}

public class QuickSortStrategy implements SortStrategy {


@Override
public <T extends Comparable<T>> void sort(List<T> list) {
[Link]("Using quick sort");
// Quick sort implementation
[Link](list); // Simplified
}
}

public class MergeSortStrategy implements SortStrategy {


@Override
public <T extends Comparable<T>> void sort(List<T> list) {
[Link]("Using merge sort");
// Merge sort implementation
[Link](list); // Simplified
}
}

// Context: Uses a strategy


public class Sorter {
private SortStrategy strategy;

public Sorter(SortStrategy strategy) {


[Link] = strategy;
}

public <T extends Comparable<T>> void sort(List<T> list) {


[Link](list);
}

// Allow changing strategy at runtime


public void setStrategy(SortStrategy strategy) {
[Link] = strategy;
}
}

// Usage
List<Integer> numbers = [Link](3, 1, 4, 1, 5, 9, 2, 6);
Sorter sorter = new Sorter(new QuickSortStrategy());
[Link](numbers); // Uses quick sort

[Link](new MergeSortStrategy());
[Link](numbers); // Now uses merge sort

Java Built-in Example


Comparator interface is a perfect example:

List<String> names = [Link]("Alice", "Bob", "Charlie");

// Different strategies for sorting


[Link]([Link]()); // Alphabetical
[Link]([Link]()); // Reverse alphabetical
[Link]([Link](String::length)); // By length

📌 When to Use Strategy


✅ Multiple variants of an algorithm exist
✅ You need to switch algorithms at runtime
✅ You want to isolate algorithm implementation from client code
✅ Different algorithms have different performance characteristics for different inputs
🚫 When NOT to Use
❌ Only one algorithm exists
❌ Algorithms are very simple (just use if/else)

8. Observer Pattern
🎯 Problem
You need to notify multiple objects when another object changes state. Define a one-to-many dependency
between objects so that when one object changes state, all its dependents are notified and updated
automatically.

Real-World Analogy
News subscription: When news happens, all subscribers get notified immediately.

Example: Event Notification System

// Observer interface
public interface Observer {
void update(String event);
}

// Subject interface
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers(String event);
}

// Concrete subject
public class NewsAgency implements Subject {
private List<Observer> observers = new ArrayList<>();
private String latestNews;

@Override
public void registerObserver(Observer observer) {
[Link](observer);
}

@Override
public void removeObserver(Observer observer) {
[Link](observer);
}

@Override
public void notifyObservers(String event) {
for (Observer observer : observers) {
[Link](event);
}
}

public void setBreakingNews(String news) {


[Link] = news;
notifyObservers("BREAKING: " + news);
}
}

// Concrete observers
public class NewsChannel implements Observer {
private String name;

public NewsChannel(String name) {


[Link] = name;
}

@Override
public void update(String event) {
[Link](name + " received: " + event);
}
}

// Usage
NewsAgency agency = new NewsAgency();
NewsChannel cnn = new NewsChannel("CNN");
NewsChannel bbc = new NewsChannel("BBC");

[Link](cnn);
[Link](bbc);

[Link]("Major earthquake reported!");


// Output:
// CNN received: BREAKING: Major earthquake reported!
// BBC received: BREAKING: Major earthquake reported!

Java Built-in Support


Java provides built-in observer support:
Option 1: [Link] (Legacy - deprecated in Java 9)

// Not recommended for new code

Option 2: Property Change Support (Better)

public class ObservableBean {


private PropertyChangeSupport support = new PropertyChangeSupport(this);
private String name;

public void addPropertyChangeListener(PropertyChangeListener listener) {


[Link](listener);
}

public void setName(String name) {


String oldName = [Link];
[Link] = name;
[Link]("name", oldName, name);
}
}

Option 3: Custom with Functional Interfaces (Modern)

public class EventSource {


private List<Consumer<String>> listeners = new ArrayList<>();

public void addListener(Consumer<String> listener) {


[Link](listener);
}

public void fireEvent(String event) {


[Link](listener -> [Link](event));
}
}

📌 When to Use Observer


✅ Implementing distributed event handling systems
✅ Decoupling event producers from consumers
✅ Real-time updates (UI, notifications, logging)
🚫 When NOT to Use
❌ Simple one-to-one notification (just call the method directly)
❌ Performance-critical scenarios with many observers (consider pull vs push)

9. Command Pattern
🎯 Problem
You need to parameterize objects with operations, queue requests, log operations, or support undoable
operations. Encapsulate a request as an object, thereby letting you parameterize clients with different
requests, queue or log requests, and support undoable operations.

Real-World Analogy
Restaurant: Customer creates order (command), waiter (invoker) takes order to chef (receiver), chef executes order.

Example: Undoable Text Editor

// Command interface
public interface Command {
void execute();
void undo(); // For undo functionality
}

// Receiver: Knows how to perform the actual work


public class TextEditor {
private StringBuilder content = new StringBuilder();
private Stack<String> history = new Stack<>();

public void insertText(String text) {


[Link]([Link]());
[Link](text);
}

public void deleteText(int length) {


if (length > [Link]()) length = [Link]();
[Link]([Link]());
[Link]([Link]() - length, [Link]());
}

public String getContent() {


return [Link]();
}

public void restorePreviousState() {


if (![Link]()) {
content = new StringBuilder([Link]());
}
}
}

// Concrete commands
public class InsertCommand implements Command {
private TextEditor editor;
private String text;

public InsertCommand(TextEditor editor, String text) {


[Link] = editor;
[Link] = text;
}

@Override
public void execute() {
[Link](text);
}

@Override
public void undo() {
[Link]();
}
}

public class DeleteCommand implements Command {


private TextEditor editor;
private int length;
public DeleteCommand(TextEditor editor, int length) {
[Link] = editor;
[Link] = length;
}

@Override
public void execute() {
[Link](length);
}

@Override
public void undo() {
[Link]();
}
}

// Invoker: Holds and executes commands


public class CommandHistory {
private Stack<Command> executedCommands = new Stack<>();

public void executeCommand(Command command) {


[Link]();
[Link](command);
}

public void undoLastCommand() {


if (![Link]()) {
Command lastCommand = [Link]();
[Link]();
}
}
}

// Usage
TextEditor editor = new TextEditor();
CommandHistory history = new CommandHistory();

[Link](new InsertCommand(editor, "Hello "));


[Link](new InsertCommand(editor, "World!"));
[Link]([Link]()); // "Hello World!"

[Link]();
[Link]([Link]()); // "Hello "

📌 When to Use Command


✅ Implementing undo/redo functionality
✅ Building macro recording systems
✅ Decoupling request sender from request receiver
✅ Queuing requests for asynchronous processing
✅ Logging/transactional operations
🚫 When NOT to Use
❌ Simple direct method calls suffice
❌ No need for queuing, logging, or undo functionality
10. Template Method Pattern
🎯 Problem
You have an algorithm with some fixed steps and some variable steps. Define the skeleton of an algorithm in
an operation, deferring some steps to subclasses.

Real-World Analogy
Recipe template: "Prepare ingredients → Cook → Serve" where "Cook" step varies by recipe.

Example: Data Processing Pipeline

// Abstract class with template method


public abstract class DataProcessor {

// Template method - final to prevent overriding


public final void process() {
loadData();
validateData();
transformData(); // Hook method - implemented by subclasses
saveData();
logCompletion();
}

// Concrete methods - shared implementation


private void loadData() {
[Link]("Loading data from source...");
}

private void validateData() {


[Link]("Validating data integrity...");
}

private void saveData() {


[Link]("Saving processed data...");
}

private void logCompletion() {


[Link]("Processing completed successfully!");
}

// Abstract hook method - must be implemented by subclasses


protected abstract void transformData();
}

// Concrete implementations
public class CsvDataProcessor extends DataProcessor {
@Override
protected void transformData() {
[Link]("Transforming CSV data: parsing columns, handling quotes...");
}
}

public class JsonDataProcessor extends DataProcessor {


@Override
protected void transformData() {
[Link]("Transforming JSON data: parsing objects, handling nested
structures...");
}
}
// Usage
DataProcessor csvProcessor = new CsvDataProcessor();
[Link]();

DataProcessor jsonProcessor = new JsonDataProcessor();


[Link]();

Java Built-in Examples


HttpServlet : service() method is template, doGet() , doPost() are hooks
AbstractList : get() is abstract hook, other methods use it

📌 When to Use Template Method


✅ Algorithm structure is fixed, but specific steps vary
✅ You want to enforce algorithm structure while allowing customization
✅ Code duplication exists in similar algorithms
🚫 When NOT to Use
❌ All steps vary significantly between implementations
❌ You need to change the algorithm structure itself

🧪 Module 5 Exercises
Exercise 1: Factory Method
Create a document processing system that supports PDF, Word, and Excel documents. Use Factory Method so
adding new document types doesn’t require modifying existing code.

Exercise 2: Builder Pattern


Implement a Pizza class with many optional toppings (cheese, pepperoni, mushrooms, olives, etc.). Use Builder
pattern to create pizzas fluently.

Exercise 3: Strategy Pattern


Build a navigation system with different routing strategies: fastest route, shortest distance, eco-friendly (avoid hills).
Allow switching strategies at runtime.

Exercise 4: Observer Pattern


Create a stock price monitoring system where multiple displays (mobile app, web dashboard, email alerts) get notified
when stock prices change.

Exercise 5: Decorator Pattern


Implement a coffee ordering system where customers can add condiments (milk, sugar, whipped cream) dynamically.
Each addition should wrap the previous beverage.

Exercise 6: Command Pattern


Extend a simple drawing application to support undo/redo functionality using the Command pattern.

✅ Module 5 Mastery Checklist


Can identify which pattern solves a given design problem
Understand the difference between Factory Method and Abstract Factory
Know when Builder is better than telescoping constructors
Recognize that Singleton is often an anti-pattern in testable code
Can implement Adapter to integrate incompatible interfaces
Understand how Decorator enables dynamic behavior composition
Know when to use Strategy vs Template Method
Can implement Observer for event-driven architectures
Understand how Command enables undo/redo and queuing
Avoid over-engineering—apply patterns only when they solve real problems

💡 Professional Insight: The best code often doesn’t look like it uses patterns—the patterns are there in the
design, but the implementation is clean and natural. Focus on solving problems, not implementing patterns.

Module 6: Advanced OOP Techniques — Professional-Grade Object Design


This module covers sophisticated techniques that separate competent developers from expert designers. These
concepts are essential for building robust, maintainable, and scalable systems in production environments.

1️⃣ Composition Over Inheritance


🎯 The Core Principle
"Favor object composition over class inheritance." — Gang of Four

Why Inheritance Is Problematic


Fragile Base Class Problem: Changes to superclass can break subclasses unexpectedly
Tight Coupling: Subclasses are tightly bound to superclass implementation details
Inflexible Hierarchies: Deep inheritance trees become rigid and hard to modify
Multiple Inheritance Limitation: Java only allows single inheritance

✅ Composition: The Better Approach


Instead of IS-A relationships, use HAS-A relationships with delegation.

Example: Game Character System

❌ Inheritance-Based Design (Problematic)


// Deep inheritance hierarchy
abstract class Character {
protected int health;
protected int attackPower;

public void attack(Character target) {


[Link](attackPower);
}

public abstract void takeDamage(int damage);


}

class Warrior extends Character {


private int defense;
@Override
public void takeDamage(int damage) {
int actualDamage = [Link](0, damage - defense);
health -= actualDamage;
}
}

class Mage extends Character {


private int mana;

@Override
public void takeDamage(int damage) {
health -= damage;
}

public void castSpell() {


if (mana >= 10) {
mana -= 10;
// Cast spell logic
}
}
}

// Problem: What about a Warrior who can also cast spells?


// Can't inherit from both Warrior and Mage!

✅ Composition-Based Design (Flexible)


// Behaviors as interfaces
interface Attackable {
void attack(Target target);
}

interface Damageable {
void takeDamage(int damage);
int getHealth();
}

interface SpellCaster {
void castSpell();
boolean hasMana();
}

// Components (reusable behavior units)


class BasicAttack implements Attackable {
private int attackPower;

public BasicAttack(int attackPower) {


[Link] = attackPower;
}

@Override
public void attack(Target target) {
[Link](attackPower);
}
}

class PhysicalDefense implements Damageable {


private int health;
private int defense;
public PhysicalDefense(int health, int defense) {
[Link] = health;
[Link] = defense;
}

@Override
public void takeDamage(int damage) {
int actualDamage = [Link](0, damage - defense);
health = [Link](0, health - actualDamage);
}

@Override
public int getHealth() {
return health;
}
}

class ManaSystem implements SpellCaster {


private int mana;
private int maxMana;

public ManaSystem(int maxMana) {


[Link] = maxMana;
[Link] = maxMana;
}

@Override
public void castSpell() {
if (hasMana()) {
mana -= 10;
[Link]("Casting spell!");
}
}

@Override
public boolean hasMana() {
return mana >= 10;
}
}

// Flexible character composition


class GameCharacter implements Attackable, Damageable, SpellCaster {
private final Attackable attackBehavior;
private final Damageable damageBehavior;
private final SpellCaster spellBehavior;

public GameCharacter(Attackable attack, Damageable damage, SpellCaster spell) {


[Link] = attack;
[Link] = damage;
[Link] = spell;
}

@Override
public void attack(Target target) {
[Link](target);
}

@Override
public void takeDamage(int damage) {
[Link](damage);
}
@Override
public int getHealth() {
return [Link]();
}

@Override
public void castSpell() {
if (spellBehavior != null) {
[Link]();
}
}

@Override
public boolean hasMana() {
return spellBehavior != null && [Link]();
}
}

// Usage: Mix and match behaviors


GameCharacter warrior = new GameCharacter(
new BasicAttack(15),
new PhysicalDefense(100, 5),
null // No spell casting
);

GameCharacter battleMage = new GameCharacter(


new BasicAttack(10),
new PhysicalDefense(80, 2),
new ManaSystem(50)
);

📌 When to Use Composition


✅ Multiple inheritance-like behavior needed
✅ Runtime behavior changes required
✅ Code reuse without tight coupling
✅ Flexible, testable designs
🚫 When Inheritance Might Be Acceptable
❌ True IS-A relationship with behavioral substitution (LSP compliant)
❌ Framework extension points (e.g., HttpServlet)
❌ Template Method pattern with stable algorithms

2️⃣ Object Equality: equals() and hashCode()


🎯 The Contract
Java’s equals() and hashCode() must follow strict contracts for proper collection behavior.

✅ Correct Implementation Guidelines


Step 1: Override Both Methods Together

If you override equals() , you must override hashCode() .

Step 2: Follow the equals() Contract


Reflexive: [Link](x) → true
Symmetric: [Link](y) ↔ [Link](x)
Transitive: If [Link](y) and [Link](z) , then [Link](z)
Consistent: Multiple calls return same result (unless modified)
Null-safe: [Link](null) → false

Step 3: Use [Link]() and [Link]()

These handle null safety automatically.

Example: Proper Implementation

public class Person {


private final String firstName;
private final String lastName;
private final LocalDate birthDate;

public Person(String firstName, String lastName, LocalDate birthDate) {


[Link] = firstName;
[Link] = lastName;
[Link] = birthDate;
}

@Override
public boolean equals(Object o) {
// 1. Reference equality check (optimization)
if (this == o) return true;

// 2. Null and type check


if (o == null || getClass() != [Link]()) return false;

// 3. Cast and compare fields


Person person = (Person) o;
return [Link](firstName, [Link]) &&
[Link](lastName, [Link]) &&
[Link](birthDate, [Link]);
}

@Override
public int hashCode() {
// 4. Use same fields as equals()
return [Link](firstName, lastName, birthDate);
}

@Override
public String toString() {
return "Person{" +
"firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", birthDate=" + birthDate +
'}';
}
}

⚠️ Common Pitfalls
Pitfall 1: Inconsistent Field Usage

// ❌ BAD: Different fields in equals() vs hashCode()


@Override
public boolean equals(Object o) {
// ... compares firstName, lastName
}

@Override
public int hashCode() {
return [Link](firstName); // Missing lastName!
}

Pitfall 2: Mutable Fields in hashCode()

// ❌ BAD: Mutable field affects hashCode()


public class BadPoint {
private int x, y; // Mutable

@Override
public int hashCode() {
return [Link](x, y); // Changes when x/y change!
}
}

// Problem:
Set<BadPoint> points = new HashSet<>();
BadPoint p = new BadPoint(1, 2);
[Link](p);
[Link](3); // hashCode() changes!
[Link](p); // May return false! Object is "lost" in HashSet

✅ Solution: Use Immutable Fields or Document Behavior


// ✅ GOOD: Immutable fields
public record Point(int x, int y) {} // Records handle this automatically

// ✅
GOOD: If mutable, document that object shouldn't be used as map key
public class MutablePoint {
private int x, y;

// Document: "Do not use as HashMap key or HashSet element"


@Override
public int hashCode() {
return [Link](x, y);
}
}

📌 Best Practices
✅ Use immutable fields for equality
✅ Use records for simple data classes (auto-generated correct methods)
✅ Always use getClass() instead of instanceof in equals() unless you specifically need LSP-compliant
equality
✅ Use IDE auto-generation or Lombok ( @EqualsAndHashCode ) for boilerplate

3️⃣ Cloning: Copy Constructors vs Cloneable


🎯 The Problem
Creating copies of objects while preserving encapsulation and handling deep vs shallow copying correctly.

❌ The Cloneable Interface (Broken by Design)


Joshua Bloch calls Cloneable "deeply flawed." Avoid it!

Problems with Cloneable :

No clone() method in interface (it's in Object )


Protected clone() method requires public wrapper
No compile-time safety for deep copying
Inheritance issues with final fields

// ❌ AVOID THIS APPROACH


public class BrokenClone implements Cloneable {
private List<String> items = new ArrayList<>();

@Override
public BrokenClone clone() {
try {
BrokenClone cloned = (BrokenClone) [Link]();
// Must manually deep copy mutable fields
[Link] = new ArrayList<>([Link]);
return cloned;
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // Should never happen
}
}
}

✅ Copy Constructors (Recommended)


Simple, type-safe, and handles inheritance properly.

public class Person {


private final String name;
private final List<String> hobbies;

public Person(String name, List<String> hobbies) {


[Link] = name;
[Link] = new ArrayList<>(hobbies); // Defensive copy
}

// Copy constructor
public Person(Person original) {
this([Link], [Link]);
}

// Getters with defensive copying


public List<String> getHobbies() {
return new ArrayList<>(hobbies);
}
}

// Usage
Person original = new Person("Alice", [Link]("reading", "hiking"));
Person copy = new Person(original); // Clean, obvious, safe

✅ Factory Methods for Copying


Alternative to copy constructors:

public class Person {


// ... fields and constructors

public static Person copyOf(Person original) {


return new Person([Link], [Link]);
}
}

// Usage
Person copy = [Link](original);

📌 When to Provide Copying


✅ Class is mutable and used in collections
✅ Class contains mutable fields that clients might modify
✅ You need to defend against external mutation
🚫 When Copying Isn't Needed
❌ Immutable classes (records, String, etc.)
❌ Classes with only primitive fields
❌ Value objects that are always created fresh

4️⃣ Inner Classes: When and How to Use Them


🎯 Four Types of Nested Classes
Type Syntax Access to Outer Instantiation Use Case
Static Nested static class Only static members [Link] inner = Helper classes,
Inner new [Link]() logical grouping
Non-Static class Inner All outer members [Link] inner = Adapters, event
Inner [Link] Inner() handlers
Local Inside method Final/effectively Only inside method One-time use
final locals helpers
Anonymous new Interface() Final/effectively Inline Event listeners,
{ ... } final locals simple callbacks

✅ Static Nested Classes (Most Common)


public class LinkedList<T> {
// Static nested class - doesn't need LinkedList instance
private static class Node<T> {
T data;
Node<T> next;

Node(T data) {
[Link] = data;
}
}

private Node<T> head;


// Methods using Node...
}

✅ Non-Static Inner Classes (Use Sparingly)


public class Calculator {
private double currentValue = 0;

// Inner class has access to Calculator's state


public class Operation {
public void add(double value) {
currentValue += value; // Can access outer class fields
}

public double getResult() {


return currentValue;
}
}

public Operation createOperation() {


return new Operation();
}
}

⚠️ Memory Leak Risk with Non-Static Inner Classes


Non-static inner classes hold implicit reference to outer class, which can prevent garbage collection.

// ❌ Memory leak risk in Android/long-lived contexts


public class MainActivity extends Activity {
private void startBackgroundTask() {
// Handler holds implicit reference to MainActivity
new Thread(new Runnable() {
@Override
public void run() {
// Do work...
}
}).start();
}
}

// ✅Fix: Use static nested class or weak references


public class MainActivity extends Activity {
private static class BackgroundTask implements Runnable {
private final WeakReference<MainActivity> activityRef;

BackgroundTask(MainActivity activity) {
[Link] = new WeakReference<>(activity);
}

@Override
public void run() {
MainActivity activity = [Link]();
if (activity != null) {
// Safe to use activity
}
}
}
}
📌 Best Practices for Nested Classes
✅ Prefer static nested classes unless you specifically need outer class access
✅ Use anonymous classes for simple, one-time implementations (event listeners)
✅ Avoid non-static inner classes in long-lived contexts (memory leaks)
✅ Use local classes sparingly—usually better as private methods

5️⃣ Generics and OOP: Type Safety with Flexibility


🎯 Generic Methods and Classes
Generics provide compile-time type safety while maintaining code reusability.

✅ Bounded Type Parameters


Restrict generic types to specific hierarchies.

// Upper bounded wildcard - "is-a" relationship


public class Box<T extends Number> {
private T value;

public void setValue(T value) {


[Link] = value;
}

public double getValueAsDouble() {


return [Link](); // Safe - all Numbers have doubleValue()
}
}

// Usage
Box<Integer> intBox = new Box<>();
Box<Double> doubleBox = new Box<>();
// Box<String> stringBox = new Box<>(); // Compile error!

✅ PECS Principle: Producer Extends, Consumer Super


Joshua Bloch's mnemonic for wildcard usage:

Producer ( ? extends T ): Use when you get values out (read-only)


Consumer ( ? super T ): Use when you put values in (write-only)

// Producer: Getting numbers out


public double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += [Link](); // Safe - all are Numbers
}
return total;
}

// Consumer: Putting integers in


public void addIntegers(List<? super Integer> destination) {
[Link](1); // Safe - Integer is subtype of ? super Integer
[Link](42); // Can add any Integer
}

// Usage
List<Integer> ints = [Link](1, 2, 3);
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();

double total = sum(ints); // ✅ Works - Integer extends Number


double total2 = sum(numbers); // ✅ Works
double total3 = sum(objects); // ❌ Compile error - Object doesn't extend Number

addIntegers(ints); // ✅ Works - Integer super Integer


addIntegers(numbers); // ✅ Works - Number super Integer
addIntegers(objects); // ✅ Works - Object super Integer

✅ Generic Methods
Methods that introduce their own type parameters.

public class GenericUtils {


// Generic method - type parameter <T>
public static <T> T getFirst(List<T> list) {
return [Link]() ? null : [Link](0);
}

// Bounded generic method


public static <T extends Comparable<T>> T max(T a, T b) {
return [Link](b) > 0 ? a : b;
}
}

// Usage
String first = [Link]([Link]("a", "b", "c"));
Integer bigger = [Link](5, 10);

📌 Best Practices for Generics


✅ Use bounded wildcards ( ? extends T , ? super T ) for flexible APIs
✅ Apply PECS principle consistently
✅ Avoid raw types ( List instead of List<String> )
✅ Use type inference ( var list = new ArrayList<String>(); ) to reduce verbosity

6️⃣ Enums as Classes: Beyond Simple Constants


🎯 Enums Are Full-Featured Classes
Java enums can have fields, methods, constructors, and implement interfaces.

✅ Rich Enum Example: State Machine


public enum OrderStatus {
PENDING {
@Override
public OrderStatus process() {
return PROCESSING;
}

@Override
public boolean canCancel() {
return true;
}
},
PROCESSING {
@Override
public OrderStatus process() {
return COMPLETED;
}

@Override
public boolean canCancel() {
return false;
}
},
COMPLETED {
@Override
public OrderStatus process() {
throw new IllegalStateException("Order already completed");
}

@Override
public boolean canCancel() {
return false;
}
};

// Abstract method - each constant must implement


public abstract OrderStatus process();
public abstract boolean canCancel();

// Static utility method


public static OrderStatus fromString(String status) {
return valueOf([Link]());
}
}

// Usage
OrderStatus status = [Link];
status = [Link](); // Now PROCESSING
[Link]([Link]()); // false

✅ Enums Implementing Interfaces


public interface Operation {
double apply(double x, double y);
}

public enum MathOperation implements Operation {


ADD {
public double apply(double x, double y) { return x + y; }
},
SUBTRACT {
public double apply(double x, double y) { return x - y; }
},
MULTIPLY {
public double apply(double x, double y) { return x * y; }
},
DIVIDE {
public double apply(double x, double y) {
if (y == 0) throw new ArithmeticException("Division by zero");
return x / y;
}
};

// Can also have common methods


public static MathOperation fromSymbol(String symbol) {
return switch (symbol) {
case "+" -> ADD;
case "-" -> SUBTRACT;
case "*" -> MULTIPLY;
case "/" -> DIVIDE;
default -> throw new IllegalArgumentException("Unknown symbol: " + symbol);
};
}
}

// Usage
double result = [Link](5, 3); // 8.0
Operation op = [Link]("*");
double product = [Link](4, 7); // 28.0

📌 Best Practices for Enums


✅ Use enums for fixed sets of constants with behavior
✅ Implement state machines with enum methods
✅ Add validation and utility methods to enums
✅ Use switch expressions with enums (exhaustive checking)

7️⃣ Exception Design in OOP


🎯 Checked vs Unchecked Exceptions
Type When to Use Examples
Checked Recoverable conditions that client code IOException , SQLException
should handle
Unchecked Programming errors or unrecoverable IllegalArgumentException ,
conditions NullPointerException

✅ Custom Exception Design


// Checked exception - forces client to handle
public class InsufficientFundsException extends Exception {
private final BigDecimal currentBalance;
private final BigDecimal requestedAmount;

public InsufficientFundsException(BigDecimal balance, BigDecimal amount) {


super("Insufficient funds: balance=" + balance + ", requested=" + amount);
[Link] = balance;
[Link] = amount;
}

public BigDecimal getCurrentBalance() { return currentBalance; }


public BigDecimal getRequestedAmount() { return requestedAmount; }
}

// Unchecked exception - programming error


public class InvalidAccountException extends RuntimeException {
public InvalidAccountException(String accountId) {
super("Invalid account ID: " + accountId);
}
}

// Usage in service layer


public class BankAccount {
private BigDecimal balance;

public void withdraw(BigDecimal amount) throws InsufficientFundsException {


if ([Link](balance) > 0) {
throw new InsufficientFundsException(balance, amount);
}
balance = [Link](amount);
}

public void transferTo(String targetAccountId, BigDecimal amount)


throws InsufficientFundsException {
if (!isValidAccountId(targetAccountId)) {
throw new InvalidAccountException(targetAccountId); // Unchecked
}
withdraw(amount);
// Transfer logic...
}
}

📌 Exception Design Best Practices


✅ Checked exceptions for recoverable, expected conditions
✅ Unchecked exceptions for programming errors and unrecoverable states
✅ Provide rich context in exception constructors (don't just pass strings)
✅ Create specific exception types rather than generic ones
✅ Don't catch and ignore exceptions
✅ Use try-with-resources for automatic resource cleanup

🧪 Module 6 Exercises
Exercise 1: Composition Over Inheritance
Refactor a vehicle hierarchy (Car, Truck, Motorcycle) that currently uses inheritance to use composition instead.
Support features like: GPS navigation, sunroof, trailer hitch, premium audio.

Exercise 2: Proper Equality Implementation


Create a BankAccount class with accountNumber , ownerName , and balance . Implement equals() and
hashCode() correctly. Test with HashSet and HashMap .

Exercise 3: Copy Constructor Implementation


Implement a Document class with title , content , and List<Tag> tags . Provide a copy constructor that creates
deep copies of mutable fields.

Exercise 4: Generic Utility Class


Create a CollectionUtils class with generic methods:

firstMatch(List<T>, Predicate<T>) - returns first element matching predicate


transform(List<T>, Function<T, R>) - transforms list elements
merge(Map<K, V>, Map<K, V>) - merges two maps with conflict resolution

Exercise 5: Rich Enum State Machine


Implement a TrafficLight enum with states RED, YELLOW, GREEN. Each state should know what the next state
is and how long it should stay in that state.

Exercise 6: Exception Hierarchy


Create a custom exception hierarchy for a file processing system:

FileProcessingException (checked base)


InvalidFileFormatException (checked)
FileTooLargeException (checked)
ConfigurationException (unchecked)

✅ Module 6 Mastery Checklist


Can explain why composition is generally better than inheritance
Implement equals() / hashCode() that satisfies all contract requirements
Avoid Cloneable interface in favor of copy constructors
Understand memory implications of different nested class types
Apply PECS principle correctly with generics
Design rich enums with behavior, not just constants
Choose appropriate exception types (checked vs unchecked) for different scenarios
Design exception hierarchies that provide useful context to callers

💡 Professional Insight: Advanced OOP isn't about using every feature—it's about choosing the right tool for
the job and understanding the tradeoffs of each decision. The best designs are often the simplest ones that solve
the actual problem.

Module 7: System Design & Architecture — Applying OOP at Scale


This module bridges the gap between individual class design and large-scale system architecture. You'll learn
how to apply OOP principles to build systems that are maintainable, testable, scalable, and aligned with business
requirements.

🏗️ 1. Layered Architecture: Separation of Concerns


🎯 The Core Principle
"Separate your system into distinct layers, each with a single responsibility."

Traditional Three-Layer Architecture

Layer Responsibility OOP Principles Applied


Presentation Layer Handle user interaction, display data Encapsulation, SRP
Business Logic Layer Core business rules, workflows SOLID, Domain-Driven Design
Data Access Layer Database interactions, persistence DIP, Abstraction
✅ Clean Implementation Example
Domain Model (Business Layer)

// Rich domain model - behavior-focused, not anemic


public class BankAccount {
private final String accountId;
private BigDecimal balance;
private final List<Transaction> transactions;

public BankAccount(String accountId) {


[Link] = accountId;
[Link] = [Link];
[Link] = new ArrayList<>();
}

// Business logic encapsulated in domain object


public void deposit(BigDecimal amount) {
if ([Link]([Link]) <= 0) {
throw new IllegalArgumentException("Deposit amount must be positive");
}
[Link] = [Link](amount);
[Link](new Transaction([Link], amount,
[Link]()));
}

public boolean withdraw(BigDecimal amount) {


if ([Link]([Link]) <= 0) {
throw new IllegalArgumentException("Withdrawal amount must be positive");
}
if ([Link](balance) > 0) {
return false; // Insufficient funds
}
[Link] = [Link](amount);
[Link](new Transaction([Link], amount,
[Link]()));
return true;
}

// Getters for read-only access


public String getAccountId() { return accountId; }
public BigDecimal getBalance() { return balance; }
public List<Transaction> getTransactions() {
return [Link](transactions);
}
}

Repository Interface (Abstraction)

// Data Access abstraction - owned by business layer


public interface AccountRepository {
BankAccount findById(String accountId);
void save(BankAccount account);
List<BankAccount> findByCustomerId(String customerId);
}

Service Layer (Orchestrator)


// Business service - coordinates domain objects and repositories
@Service
public class AccountService {
private final AccountRepository accountRepository;
private final NotificationService notificationService;

// Dependency Injection via constructor


public AccountService(AccountRepository accountRepository,
NotificationService notificationService) {
[Link] = accountRepository;
[Link] = notificationService;
}

@Transactional
public void transfer(String fromAccountId, String toAccountId, BigDecimal amount) {
BankAccount fromAccount = [Link](fromAccountId);
BankAccount toAccount = [Link](toAccountId);

if (![Link](amount)) {
throw new InsufficientFundsException([Link](), amount);
}

[Link](amount);

[Link](fromAccount);
[Link](toAccount);

[Link](fromAccountId, toAccountId, amount);


}
}

Controller (Presentation Layer)

// Web controller - handles HTTP requests/responses


@RestController
@RequestMapping("/api/accounts")
public class AccountController {
private final AccountService accountService;

public AccountController(AccountService accountService) {


[Link] = accountService;
}

@PostMapping("/{fromId}/transfer/{toId}")
public ResponseEntity<Void> transfer(
@PathVariable String fromId,
@PathVariable String toId,
@RequestBody TransferRequest request) {

try {
[Link](fromId, toId, [Link]());
return [Link]().build();
} catch (InsufficientFundsException e) {
return [Link]().build();
}
}
}

Repository Implementation (Infrastructure)


// Concrete implementation - depends on abstraction
@Repository
public class JpaAccountRepository implements AccountRepository {
@PersistenceContext
private EntityManager entityManager;

@Override
public BankAccount findById(String accountId) {
return [Link]([Link], accountId);
}

@Override
public void save(BankAccount account) {
[Link](account);
}

@Override
public List<BankAccount> findByCustomerId(String customerId) {
return [Link](
"SELECT a FROM BankAccount a WHERE [Link] = :customerId",
[Link])
.setParameter("customerId", customerId)
.getResultList();
}
}

📌 Key Benefits of Layered Architecture


Testability: Each layer can be tested independently
Maintainability: Changes in one layer don't affect others
Flexibility: Can swap implementations (e.g., database, notification service)
Team Collaboration: Different teams can work on different layers

⚠️ Common Anti-Patterns to Avoid


Anemic Domain Model

// ❌ BAD: Anemic model - just data bags with external logic


public class BankAccount {
private String accountId;
private BigDecimal balance;
// getters/setters only
}

// Business logic scattered in service layer


public class AccountService {
public void withdraw(BankAccount account, BigDecimal amount) {
// All business logic here - violates encapsulation
if ([Link]([Link]()) > 0) {
// ...
}
}
}

Leaky Layers

// ❌
BAD: Presentation layer knows about database entities
@RestController
public class AccountController {
@Autowired
private EntityManager entityManager; // Direct database access!

@GetMapping("/{id}")
public BankAccount getAccount(@PathVariable String id) {
return [Link]([Link], id); // Returns JPA entity directly
}
}

🔌 2. Dependency Injection (DI): Inversion of Control


🎯 The Core Principle
"Don't call us, we'll call you." — Hollywood Principle

Manual Dependency Injection vs Framework DI


Manual DI (Simple Applications)

// Application bootstrap
public class BankingApplication {
public static void main(String[] args) {
// Manually wire dependencies
AccountRepository repository = new JpaAccountRepository();
NotificationService notificationService = new EmailNotificationService();
AccountService accountService = new AccountService(repository, notificationService);
AccountController controller = new AccountController(accountService);

// Start web server with controller...


}
}

Framework DI (Spring Example)

// Spring automatically wires dependencies based on annotations


@Service
public class AccountService {
private final AccountRepository accountRepository;
private final NotificationService notificationService;

// Spring injects implementations automatically


public AccountService(AccountRepository accountRepository,
NotificationService notificationService) {
[Link] = accountRepository;
[Link] = notificationService;
}
}

// Configuration tells Spring which implementation to use


@Configuration
public class BankingConfig {
@Bean
public AccountRepository accountRepository() {
return new JpaAccountRepository();
}

@Bean
@Profile("production")
public NotificationService productionNotificationService() {
return new EmailNotificationService();
}

@Bean
@Profile("test")
public NotificationService testNotificationService() {
return new MockNotificationService();
}
}

📌 DI Best Practices
✅ Constructor injection over field injection (immutable dependencies, easier testing)
✅ Interface segregation - inject only what you need
✅ Avoid service locator pattern - it hides dependencies
✅ Use qualifiers when multiple implementations exist ( @Qualifier , @Primary )
Testing with DI

// Unit test with manual dependency injection


@Test
public void testTransferInsufficientFunds() {
// Create mock repository
AccountRepository mockRepository = new MockAccountRepository();
[Link](new BankAccount("123", new BigDecimal("50")));
[Link](new BankAccount("456", new BigDecimal("100")));

// Create real service with mock dependencies


AccountService service = new AccountService(mockRepository, new MockNotificationService());

// Test business logic


assertThrows([Link], () -> {
[Link]("123", "456", new BigDecimal("100"));
});
}

🧱 3. Domain-Driven Design (DDD): Modeling Business Reality


🎯 Core Concepts
Concept Description Java Implementation
Entity Object with identity and lifecycle Class with ID field, equals() based on ID
Value Immutable object defined by attributes Record or immutable class, equals() based on all
Object fields
Aggregate Cluster of related objects treated as unit Root entity controls access to children
Repository Collection-like interface for aggregates Interface in domain layer, implementation in
infrastructure
Service Stateless operations that don't belong to Stateless classes with business logic
entities

✅ DDD Implementation Example


Value Objects (Immutable)
// Money value object - represents monetary amount with currency
public record Money(BigDecimal amount, Currency currency) {
public Money {
if ([Link]([Link]) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
}

public Money add(Money other) {


if (![Link]([Link])) {
throw new IllegalArgumentException("Cannot add different currencies");
}
return new Money([Link]([Link]), currency);
}

public Money subtract(Money other) {


if (![Link]([Link])) {
throw new IllegalArgumentException("Cannot subtract different currencies");
}
return new Money([Link]([Link]), currency);
}
}

// Address value object


public record Address(String street, String city, String postalCode, String country) {
public Address {
requireNonBlank(street, "Street");
requireNonBlank(city, "City");
// ... validation
}

private static void requireNonBlank(String value, String fieldName) {


if (value == null || [Link]()) {
throw new IllegalArgumentException(fieldName + " cannot be blank");
}
}
}

Entities (With Identity)

// Customer entity - identified by ID, has lifecycle


@Entity
public class Customer {
@Id
private String customerId;
private String name;
private EmailAddress email;
private List<Address> addresses;

// Package-private constructor for framework use


Customer() {}

public Customer(String name, EmailAddress email) {


[Link] = generateId();
[Link] = name;
[Link] = email;
[Link] = new ArrayList<>();
}

public void addAddress(Address address) {


[Link](address);
}

public void changeEmail(EmailAddress newEmail) {


// Business rule: validate email format, check uniqueness, etc.
[Link] = newEmail;
}

// equals() and hashCode() based on customerId only


@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != [Link]()) return false;
Customer customer = (Customer) o;
return [Link](customerId, [Link]);
}

@Override
public int hashCode() {
return [Link](customerId);
}
}

Aggregate Root

// Order aggregate root - controls access to OrderLine items


@Entity
public class Order {
@Id
private String orderId;
private OrderStatus status;
private CustomerId customerId;
private Money totalAmount;

@OneToMany(cascade = [Link], orphanRemoval = true)


private List<OrderLine> lines;

private Order() {} // For JPA

public Order(CustomerId customerId) {


[Link] = generateId();
[Link] = customerId;
[Link] = [Link];
[Link] = new ArrayList<>();
[Link] = [Link](); // Assume USD zero
}

// Business method that maintains aggregate consistency


public void addProduct(ProductId productId, int quantity, Money unitPrice) {
if (status != [Link]) {
throw new IllegalStateException("Cannot modify completed order");
}

// Check if product already exists


Optional<OrderLine> existingLine = [Link]()
.filter(line -> [Link]().equals(productId))
.findFirst();

if ([Link]()) {
[Link]().increaseQuantity(quantity);
} else {
[Link](new OrderLine(productId, quantity, unitPrice));
}

recalculateTotal();
}

public void completeOrder() {


if ([Link]()) {
throw new IllegalStateException("Cannot complete empty order");
}
[Link] = [Link];
}

private void recalculateTotal() {


[Link] = [Link]()
.map(OrderLine::getLineTotal)
.reduce([Link](), Money::add);
}

// Getters...
}

Repository Interface

// Domain layer interface


public interface OrderRepository {
Order findById(OrderId id);
void save(Order order);
List<Order> findByCustomerId(CustomerId customerId);
List<Order> findCompletedOrders(LocalDate since);
}

📌 DDD Benefits
Ubiquitous Language: Code reflects business terminology
Bounded Contexts: Clear boundaries between different business areas
Rich Models: Business logic lives in domain objects, not services
Testability: Domain logic can be tested without infrastructure

🎨 4. API Design: Creating Maintainable Interfaces


🎯 Principles of Good API Design
Fluent Interfaces

// Builder pattern for fluent API


public class HttpRequest {
private String url;
private Map<String, String> headers = new HashMap<>();
private String body;

private HttpRequest() {}

public static HttpRequestBuilder builder() {


return new HttpRequestBuilder();
}
public static class HttpRequestBuilder {
private HttpRequest request = new HttpRequest();

public HttpRequestBuilder url(String url) {


[Link] = url;
return this;
}

public HttpRequestBuilder header(String name, String value) {


[Link](name, value);
return this;
}

public HttpRequestBuilder body(String body) {


[Link] = body;
return this;
}

public HttpRequest build() {


validate(request);
return request;
}

private void validate(HttpRequest request) {


if ([Link] == null) {
throw new IllegalStateException("URL is required");
}
}
}
}

// Usage
HttpRequest request = [Link]()
.url("[Link]
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token")
.body("{\"name\":\"John\"}")
.build();

Immutable APIs

// Return immutable collections and defensive copies


public class UserService {
private final List<User> users = new ArrayList<>();

// Return unmodifiable view - prevents external mutation


public List<User> getUsers() {
return [Link](users);
}

// For mutable objects, return defensive copies


public User getUserById(String id) {
User user = findUserById(id);
return user != null ? new User(user) : null; // Copy constructor
}
}

Fail-Fast Design
// Validate inputs early and fail fast
public class PaymentProcessor {
public void processPayment(PaymentRequest request) {
// Validate immediately - don't wait until later in processing
validateRequest(request);

// Process payment...
}

private void validateRequest(PaymentRequest request) {


if ([Link]().compareTo([Link]) <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
if ([Link]() == null || !isValidCardNumber([Link]())) {
throw new IllegalArgumentException("Invalid card number");
}
// ... other validations
}
}

📌 API Design Best Practices


✅ Program to interfaces, not implementations
✅ Prefer immutability in public APIs
✅ Validate inputs early (fail-fast principle)
✅ Use meaningful names that reflect business domain
✅ Minimize API surface area (ISP - Interface Segregation Principle)
✅ Document contracts clearly (preconditions, postconditions, exceptions)

🧪 5. Testing OOP Code: Strategies and Patterns


🎯 Testing Pyramid
Level Percentage Focus Tools
Unit Tests 70% Individual classes/methods JUnit, Mockito
Integration Tests 20% Layer interactions Testcontainers, DBUnit
End-to-End Tests 10% Full system flow Selenium, REST Assured

✅ Unit Testing OOP Code


Testing Polymorphic Behavior

// Test that all payment processors handle errors correctly


@Test
void allPaymentProcessorsHandleInvalidCardNumbers() {
List<PaymentProcessor> processors = [Link](
new CreditCardProcessor(),
new PayPalProcessor(),
new StripeProcessor()
);

for (PaymentProcessor processor : processors) {


PaymentRequest invalidRequest = new PaymentRequest(
"invalid-card",
new BigDecimal("100.00")
);

assertThrows([Link], () -> {
[Link](invalidRequest);
});
}
}

Testing with Mocks

@Test
void transferSendsNotificationOnSuccess() {
// Arrange
BankAccount fromAccount = createAccountWithBalance("123", new BigDecimal("200"));
BankAccount toAccount = createAccountWithBalance("456", new BigDecimal("100"));

AccountRepository mockRepository = mock([Link]);


when([Link]("123")).thenReturn(fromAccount);
when([Link]("456")).thenReturn(toAccount);

NotificationService mockNotification = mock([Link]);

AccountService service = new AccountService(mockRepository, mockNotification);

// Act
[Link]("123", "456", new BigDecimal("50"));

// Assert
verify(mockNotification).sendTransferNotification("123", "456", new BigDecimal("50"));
verify(mockRepository, times(2)).save(any([Link]));
}

Contract Tests for Polymorphism

// Abstract test class for all PaymentProcessor implementations


public abstract class PaymentProcessorContractTest {
protected abstract PaymentProcessor createProcessor();

@Test
void processesValidPaymentSuccessfully() {
PaymentProcessor processor = createProcessor();
PaymentRequest request = validPaymentRequest();

PaymentResult result = [Link](request);

assertTrue([Link]());
assertNotNull([Link]());
}

@Test
void throwsExceptionForInvalidAmount() {
PaymentProcessor processor = createProcessor();
PaymentRequest request = invalidAmountRequest();

assertThrows([Link], () -> {
[Link](request);
});
}
}
// Concrete test for each implementation
public class CreditCardProcessorTest extends PaymentProcessorContractTest {
@Override
protected PaymentProcessor createProcessor() {
return new CreditCardProcessor();
}
}

✅ Integration Testing
@SpringBootTest
@Testcontainers
class AccountServiceIntegrationTest {

@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13");

@Autowired
private AccountService accountService;

@Test
void transferBetweenAccountsWorksEndToEnd() {
// Given
String fromAccountId = [Link]("customer1");
String toAccountId = [Link]("customer2");
[Link](fromAccountId, new BigDecimal("1000"));

// When
[Link](fromAccountId, toAccountId, new BigDecimal("500"));

// Then
assertEquals(new BigDecimal("500"), [Link](fromAccountId));
assertEquals(new BigDecimal("500"), [Link](toAccountId));
}
}

📌 Testing Best Practices


✅ Test behavior, not implementation (don't test private methods)
✅ Use factory methods for test data setup ( createAccountWithBalance() )
✅ Write contract tests for polymorphic hierarchies
✅ Mock only external dependencies (don't mock value objects)
✅ Test edge cases and error conditions thoroughly
✅ Keep tests fast and independent (no shared state between tests)

🧪 Module 7 Exercises
Exercise 1: Layered Architecture
Build a library management system with:

Domain layer: Book, Member, Loan entities with business logic


Repository layer: Interfaces and JPA implementations
Service layer: LoanService with business rules (max loans, overdue handling)
Controller layer: REST API for borrowing/returning books
Exercise 2: DDD Implementation
Model an e-commerce checkout process using DDD concepts:

Value Objects: Money, Address, ProductId


Entities: Customer, Order, OrderLine
Aggregate: Order as aggregate root
Repository: OrderRepository interface and implementation

Exercise 3: Dependency Injection


Refactor a tightly coupled notification system to use proper DI:

Email, SMS, and Push notification implementations


Configuration-based selection of notification channels
Unit tests with mocked dependencies

Exercise 4: API Design


Create a fluent API for building SQL queries:

Query query = [Link]("name", "email")


.from("users")
.where("age").greaterThan(18)
.orderBy("name")
.limit(10);

Exercise 5: Testing Strategy


Write comprehensive tests for a banking transfer service:

Unit tests for business logic


Contract tests for different account types
Integration tests with real database
Edge case tests (insufficient funds, invalid accounts, etc.)

✅ Module 7 Mastery Checklist


Can design layered architectures that respect separation of concerns
Implement rich domain models instead of anemic ones
Apply DDD concepts (entities, value objects, aggregates) appropriately
Use dependency injection to decouple components
Design APIs that are intuitive, safe, and maintainable
Write effective unit tests that focus on behavior, not implementation
Create contract tests for polymorphic hierarchies
Understand when to use manual DI vs framework DI
Avoid leaky abstractions between layers
Design systems that are testable from the start

💡 Professional Insight: Great system design isn't about complex patterns—it's about clear boundaries, well-
defined responsibilities, and code that reads like business requirements. The best architectures emerge
from deep understanding of the problem domain, not from applying patterns blindly.
Module 8: Anti-Patterns & Code Smells — Recognizing and Fixing Design
Problems
This module teaches you to identify problematic code patterns before they become technical debt. You'll learn to
spot code smells early, understand their root causes, and apply proven refactoring techniques to fix them.

🚨 What Are Code Smells and Anti-Patterns?


Code Smell: A surface indication that something might be wrong with your design
Anti-Pattern: A commonly implemented but counterproductive solution to a recurring problem

💡 Key Insight: Code smells are symptoms, not problems themselves. Always look for the underlying cause.

🧪 1. Anemic Domain Model


🎯 The Problem
Domain objects contain only data (fields and getters/setters) with no business logic. All behavior is in service
classes.

❌ Classic Example
// Anemic domain model - just a data bag
public class BankAccount {
private String accountId;
private BigDecimal balance;

// Only getters and setters - no behavior!


public String getAccountId() { return accountId; }
public void setAccountId(String accountId) { [Link] = accountId; }
public BigDecimal getBalance() { return balance; }
public void setBalance(BigDecimal balance) { [Link] = balance; }
}

// All business logic scattered in service layer


public class AccountService {
public void withdraw(BankAccount account, BigDecimal amount) {
if ([Link]([Link]()) > 0) {
throw new InsufficientFundsException();
}
[Link]([Link]().subtract(amount));
}

public void transfer(BankAccount from, BankAccount to, BigDecimal amount) {


withdraw(from, amount);
[Link]([Link]().add(amount));
}
}

🔍 Why It's Bad


Violates encapsulation: External code can put objects in invalid states
Scattered logic: Business rules are spread across multiple service classes
Hard to maintain: Changes require modifying multiple places
Difficult to test: Logic isn't co-located with data
✅ Refactored Solution (Rich Domain Model)
// Rich domain model - behavior with data
public class BankAccount {
private final String accountId;
private BigDecimal balance;

public BankAccount(String accountId) {


[Link] = accountId;
[Link] = [Link];
}

// Business logic encapsulated in the domain object


public void deposit(BigDecimal amount) {
validatePositiveAmount(amount);
[Link] = [Link](amount);
}

public boolean withdraw(BigDecimal amount) {


validatePositiveAmount(amount);
if ([Link](balance) > 0) {
return false;
}
[Link] = [Link](amount);
return true;
}

public void transferTo(BankAccount target, BigDecimal amount) {


if (!withdraw(amount)) {
throw new InsufficientFundsException(balance, amount);
}
[Link](amount);
}

private void validatePositiveAmount(BigDecimal amount) {


if ([Link]([Link]) <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
}

// Read-only access
public String getAccountId() { return accountId; }
public BigDecimal getBalance() { return balance; }
}

// Service layer becomes thin orchestrator


public class AccountService {
public void transfer(String fromId, String toId, BigDecimal amount) {
BankAccount from = [Link](fromId);
BankAccount to = [Link](toId);
[Link](to, amount);
[Link](from);
[Link](to);
}
}

📌 Detection Checklist
Class has mostly getters/setters
No methods that modify internal state based on business rules
Service classes contain complex logic that should belong to domain objects
Objects can be easily put into invalid states

🏗️ 2. God Class (or God Object)


🎯 The Problem
A single class that knows too much or does too much. It violates Single Responsibility Principle spectacularly.

❌ Classic Example
// God class - handles everything!
public class OrderProcessor {
// Data fields
private List<Order> orders;
private Customer customer;
private PaymentInfo paymentInfo;

// Order management
public void createOrder(Order order) { /* ... */ }
public void cancelOrder(String orderId) { /* ... */ }
public Order findOrder(String orderId) { /* ... */ }

// Customer management
public void updateCustomer(Customer customer) { /* ... */ }
public Customer getCustomer() { /* ... */ }

// Payment processing
public void processPayment(PaymentInfo payment) { /* ... */ }
public boolean validateCreditCard(String cardNumber) { /* ... */ }

// Email notifications
public void sendOrderConfirmationEmail() { /* ... */ }
public void sendShippingNotification() { /* ... */ }

// Database operations
public void saveToDatabase() { /* ... */ }
public void loadFromDatabase() { /* ... */ }

// PDF generation
public byte[] generateInvoicePdf() { /* ... */ }

// Logging
public void logActivity(String activity) { /* ... */ }

// Validation
public boolean isValidOrder(Order order) { /* ... */ }

// And 50 more methods...


}

🔍 Why It's Bad


Impossible to test: Too many dependencies and responsibilities
Fragile: Changes in one area break unrelated functionality
Unmaintainable: Hard to understand and modify
Not reusable: Can't use individual pieces independently
✅ Refactored Solution (SRP-Compliant)
// Split into focused, single-responsibility classes

// Data model
public class Order {
private String orderId;
private List<OrderItem> items;
private OrderStatus status;
// ... basic behavior related to order state
}

// Order management service


public class OrderService {
private OrderRepository orderRepository;
private PaymentService paymentService;
private NotificationService notificationService;

public void createOrder(OrderRequest request) {


Order order = new Order([Link]());
if (![Link]()) {
throw new InvalidOrderException();
}
[Link](order);
[Link](order);
}
}

// Payment processing
public class PaymentService {
private PaymentGateway paymentGateway;

public PaymentResult processPayment(PaymentRequest request) {


return [Link](request);
}
}

// Notification service
public class NotificationService {
private EmailService emailService;
private SMSService smsService;

public void sendOrderConfirmation(Order order) {


[Link]([Link]().getEmail(),
"Order Confirmed",
buildOrderConfirmationMessage(order));
}
}

// Repository for data access


public class OrderRepository {
public void save(Order order) { /* database logic */ }
public Order findById(String id) { /* database logic */ }
}

📌 Detection Checklist
Class has more than 500 lines of code
Class has more than 10-15 methods
Class uses many different external libraries/frameworks
Multiple developers need to modify the same class for different features
Class name is vague (Manager, Processor, Handler, Service)

🔀 3. Switch Statements on Type


🎯 The Problem
Using if/else or switch statements to handle different types instead of using polymorphism.

❌ Classic Example
// Type switching - violates OCP and polymorphism
public class ShapeDrawer {
public void drawShape(Object shape) {
if (shape instanceof Circle) {
Circle c = (Circle) shape;
[Link]("Drawing circle with radius " + [Link]());
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
[Link]("Drawing rectangle " + [Link]() + "x" + [Link]());
} else if (shape instanceof Triangle) {
Triangle t = (Triangle) shape;
[Link]("Drawing triangle with base " + [Link]() + " and height " +
[Link]());
} else {
throw new IllegalArgumentException("Unknown shape type");
}
}

public double calculateArea(Object shape) {


if (shape instanceof Circle) {
Circle c = (Circle) shape;
return [Link] * [Link]() * [Link]();
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
return [Link]() * [Link]();
} else if (shape instanceof Triangle) {
Triangle t = (Triangle) shape;
return 0.5 * [Link]() * [Link]();
}
throw new IllegalArgumentException("Unknown shape type");
}
}

🔍 Why It's Bad


Violates Open/Closed Principle: Must modify code to add new shapes
Error-prone: Easy to forget to update all switch statements when adding new types
Not polymorphic: Can't treat different shapes uniformly
Hard to maintain: Logic scattered across multiple methods

✅ Refactored Solution (Polymorphism)


// Define common interface
public interface Shape {
void draw();
double calculateArea();
}

// Each shape implements its own behavior


public class Circle implements Shape {
private double radius;

public Circle(double radius) {


[Link] = radius;
}

@Override
public void draw() {
[Link]("Drawing circle with radius " + radius);
}

@Override
public double calculateArea() {
return [Link] * radius * radius;
}
}

public class Rectangle implements Shape {


private double width, height;

public Rectangle(double width, double height) {


[Link] = width;
[Link] = height;
}

@Override
public void draw() {
[Link]("Drawing rectangle " + width + "x" + height);
}

@Override
public double calculateArea() {
return width * height;
}
}

// Client code is simple and extensible


public class ShapeRenderer {
public void renderShapes(List<Shape> shapes) {
for (Shape shape : shapes) {
[Link](); // Polymorphic call
[Link]("Area: " + [Link]());
}
}
}

// Adding new shapes requires NO changes to existing code!


public class Triangle implements Shape {
private double base, height;

public Triangle(double base, double height) {


[Link] = base;
[Link] = height;
}

@Override
public void draw() {
[Link]("Drawing triangle with base " + base + " and height " + height);
}

@Override
public double calculateArea() {
return 0.5 * base * height;
}
}

📌 Detection Checklist
instanceof checks followed by casting
switch statements on enum values that represent types
Multiple methods with similar if/else chains for the same types
Adding new types requires modifying existing code

🔓 4. Setter Overuse (Breaking Encapsulation)


🎯 The Problem
Exposing public setters that allow external code to bypass validation and put objects in invalid states.

❌ Classic Example
// Broken encapsulation with public setters
public class User {
private String email;
private int age;
private String status;

// Public setters allow invalid states!


public void setEmail(String email) {
[Link] = email; // No validation!
}

public void setAge(int age) {


[Link] = age; // Can set negative age!
}

public void setStatus(String status) {


[Link] = status; // Can set invalid status!
}
}

// Usage - creates invalid objects


User user = new User();
[Link]("invalid-email"); // Should validate format
[Link](-5); // Should reject negative age
[Link]("INVALID_STATUS"); // Should validate against allowed values

🔍 Why It's Bad


Invalid states: Objects can exist in states that violate business rules
Scattered validation: Validation logic must be duplicated everywhere objects are used
Hard to reason about: You can't trust that any object is in a valid state
Security risks: Malicious input can bypass intended constraints

✅ Refactored Solution (Behavior-Focused API)


// Encapsulated validation with behavior-focused methods
public class User {
private String email;
private int age;
private UserStatus status;

// Constructor ensures valid initial state


public User(String email, int age) {
setEmail(email);
setAge(age);
[Link] = [Link];
}

// Behavior methods with validation


public void updateEmail(String newEmail) {
validateEmail(newEmail);
[Link] = newEmail;
}

public void celebrateBirthday() {


[Link]++;
if (age >= 65) {
[Link] = [Link];
}
}

public void deactivate() {


if (status != [Link]) {
[Link] = [Link];
}
}

// Private validation methods


private void validateEmail(String email) {
if (email == null || ![Link]("@")) {
throw new IllegalArgumentException("Invalid email format");
}
}

private void setAge(int age) {


if (age < 0 || age > 150) {
throw new IllegalArgumentException("Invalid age: " + age);
}
[Link] = age;
}

// Read-only access
public String getEmail() { return email; }
public int getAge() { return age; }
public UserStatus getStatus() { return status; }
}

// Usage - only valid operations allowed


User user = new User("john@[Link]", 30);
[Link]("new@[Link]"); // Validated
[Link](); // Age increases safely
// [Link](-5); // Not possible - no setter exposed!

📌 Detection Checklist
Public setters without validation
Objects can be created in invalid states
Validation logic duplicated in multiple places
Getters return mutable collections without defensive copying

🧬 5. Inheritance Abuse
🎯 The Problem
Using inheritance for code reuse rather than true IS-A relationships, leading to fragile hierarchies.

❌ Classic Example: Square-Rectangle Problem


// Violates Liskov Substitution Principle
class Rectangle {
protected int width, height;

public void setWidth(int w) { width = w; }


public void setHeight(int h) { height = h; }
public int getArea() { return width * height; }
}

class Square extends Rectangle {


@Override
public void setWidth(int w) {
[Link](w);
[Link](w); // Forces square constraint
}

@Override
public void setHeight(int h) {
[Link](h);
[Link](h);
}
}

// Client code breaks with Square


void testRectangle(Rectangle r) {
[Link](5);
[Link](4);
assert [Link]() == 20; // FAILS if r is Square! (area = 16)
}

🔍 Why It's Bad


Violates Liskov Substitution Principle: Subclasses can't substitute for parent
Fragile base class: Changes to parent break subclasses unexpectedly
Tight coupling: Subclasses depend on parent implementation details
Inflexible: Can't combine behaviors from multiple parents

✅ Refactored Solution (Composition)


// Composition over inheritance
class Rectangle {
private int width, height;

public Rectangle(int width, int height) {


[Link] = width;
[Link] = height;
}

public int getArea() { return width * height; }


public int getWidth() { return width; }
public int getHeight() { return height; }
}

class Square {
private Rectangle rectangle;

public Square(int side) {


[Link] = new Rectangle(side, side);
}

public void setSide(int side) {


rectangle = new Rectangle(side, side);
}

public int getArea() { return [Link](); }


public int getSide() { return [Link](); } // Width == Height
}

// Alternative: Independent implementations


interface Shape {
int getArea();
}

class Rectangle implements Shape {


private int width, height;
public Rectangle(int w, int h) { width = w; height = h; }
public int getArea() { return width * height; }
}

class Square implements Shape {


private int side;
public Square(int side) { [Link] = side; }
public int getArea() { return side * side; }
}

📌 Detection Checklist
Subclass overrides methods to throw exceptions ("can't do that!")
Subclass strengthens preconditions or weakens postconditions
Deep inheritance hierarchies (>3 levels)
Using inheritance just to reuse code (not for behavioral substitution)

🧩 6. Feature Envy
🎯 The Problem
A method uses more data from another class than from its own class, indicating it belongs elsewhere.

❌ Classic Example
// Feature envy - this method belongs in the Customer class!
public class OrderService {
public String getCustomerFullName(Order order) {
Customer customer = [Link]();
return [Link]() + " " + [Link]();
}

public boolean isCustomerVip(Order order) {


Customer customer = [Link]();
return [Link]() > 100 &&
[Link]().compareTo(new BigDecimal("1000")) > 0;
}
}

// Customer class lacks these obvious methods


public class Customer {
private String firstName;
private String lastName;
private int totalOrders;
private BigDecimal averageOrderValue;

// Missing: getFullName(), isVip() methods!


}

🔍 Why It's Bad


Violates encapsulation: Logic is separated from the data it operates on
Hard to find: Related functionality is scattered across classes
Duplication: Same logic might be implemented in multiple places
Maintenance nightmare: Changes require updating multiple locations

✅ Refactored Solution (Move Method)


// Move methods to the class that owns the data
public class Customer {
private String firstName;
private String lastName;
private int totalOrders;
private BigDecimal averageOrderValue;

public String getFullName() {


return firstName + " " + lastName;
}

public boolean isVip() {


return totalOrders > 100 &&
[Link](new BigDecimal("1000")) > 0;
}

// Getters...
}

// Service class becomes simpler


public class OrderService {
public String getCustomerFullName(Order order) {
return [Link]().getFullName(); // Delegate to Customer
}

public boolean isCustomerVip(Order order) {


return [Link]().isVip(); // Delegate to Customer
}
}
📌 Detection Checklist
Method accesses more fields from other classes than its own
Method name includes another class's name (e.g., getCustomerName() )
Multiple methods in one class delegate to the same other class
The method would make more sense as part of another class's interface

🧪 Module 8 Exercises
Exercise 1: Anemic Model Refactoring
Take this anemic e-commerce cart and convert it to a rich domain model:

public class ShoppingCart {


private List<Item> items;
private Customer customer;

// Only getters/setters
}

public class CartService {


public void addItem(ShoppingCart cart, Item item) { /* ... */ }
public void removeItem(ShoppingCart cart, String itemId) { /* ... */ }
public BigDecimal calculateTotal(ShoppingCart cart) { /* ... */ }
public boolean applyDiscount(ShoppingCart cart, Discount discount) { /* ... */ }
}

Exercise 2: God Class Decomposition


Break down this god class into SRP-compliant components:

public class UserManager {


// Handles user CRUD, authentication, email notifications,
// logging, validation, password hashing, session management,
// role management, audit logging, etc.
}

Exercise 3: Switch Statement Elimination


Replace these type-switching methods with polymorphism:

public class DocumentProcessor {


public void processDocument(Object doc) {
if (doc instanceof PdfDocument) { /* ... */ }
else if (doc instanceof WordDocument) { /* ... */ }
else if (doc instanceof ExcelDocument) { /* ... */ }
}

public String getMimeType(Object doc) {


if (doc instanceof PdfDocument) return "application/pdf";
else if (doc instanceof WordDocument) return "application/msword";
else if (doc instanceof ExcelDocument) return "application/[Link]-excel";
return "application/octet-stream";
}
}
Exercise 4: Encapsulation Restoration
Fix this poorly encapsulated bank account class:

public class BankAccount {


public String accountNumber;
public BigDecimal balance;
public String status;

// Public setters with no validation


public void setBalance(BigDecimal balance) { [Link] = balance; }
public void setStatus(String status) { [Link] = status; }
}

Exercise 5: Inheritance to Composition


Refactor this inheritance hierarchy to use composition:

class Vehicle {
protected String brand;
protected int year;
}

class Car extends Vehicle {


private int doors;
}

class Truck extends Vehicle {


private int cargoCapacity;
}

class PickupTruck extends Car { // Also needs truck functionality!


private int cargoCapacity;
}

✅ Module 8 Mastery Checklist


Can identify anemic domain models and refactor to rich models
Recognize god classes and decompose them using SRP
Replace type-switching code with polymorphic solutions
Restore proper encapsulation by replacing setters with behavior methods
Detect inheritance abuse and refactor to composition
Identify feature envy and move methods to appropriate classes
Understand that code smells are symptoms of deeper design problems
Apply refactoring techniques systematically, not randomly

💡 Professional Insight: The best developers aren't those who never write smelly code—they're those who
recognize smells quickly and refactor continuously. Technical debt compounds; address it early and often.

Module 9: JVM Internals (OOP-Relevant) — Understanding How Java Really


Works
This module reveals what happens under the hood when you write OOP code in Java. Understanding JVM internals
helps you write more efficient, predictable, and debuggable code by knowing how your high-level constructs
translate to low-level operations.

🧠 1. Object Memory Layout


🎯 How Objects Are Stored in Memory
Every Java object in memory has a specific layout:

[Object Header (8-16 bytes)] + [Instance Data] + [Padding]

Object Header Components

Component Size (64-bit JVM) Purpose


Mark Word 8 bytes Hash code, lock state, GC age, biased locking info
Class Pointer 4-8 bytes Reference to class metadata (compressed oops = 4 bytes)

Instance Data Layout


Fields are reordered for memory alignment (larger types first)
Inheritance hierarchy affects layout (superclass fields first)
final fields may be optimized differently

🔍 Practical Example
public class Point {
private int x; // 4 bytes
private int y; // 4 bytes
private boolean active; // 1 byte + 3 bytes padding
}

// Memory layout (approximate):


// [Mark Word: 8 bytes]
// [Class Pointer: 4 bytes]
// [x: 4 bytes]
// [y: 4 bytes]
// [active: 1 byte + padding: 3 bytes]
// Total: ~24 bytes (due to 8-byte alignment)

💡 Why This Matters


Memory footprint: Understanding object size helps optimize memory usage
Cache locality: Field ordering affects CPU cache performance
Padding waste: Small objects can have significant overhead

Tools to Inspect Memory Layout

// Using JOL (Java Object Layout) library


import [Link];

Point p = new Point();


[Link]([Link](p).toPrintable());
// Output shows exact memory layout and sizes

📊 2. Virtual Method Table (vtable)


🎯 How Polymorphism Works at Runtime
The JVM uses virtual method tables (vtables) to implement dynamic method dispatch.

vtable Structure
Each class has its own vtable
Contains pointers to actual method implementations
Method resolution happens at runtime based on actual object type

🔍 Step-by-Step Method Dispatch


class Animal {
public void makeSound() { [Link]("Animal sound"); }
}

class Dog extends Animal {


@Override
public void makeSound() { [Link]("Woof!"); }
}

Animal myPet = new Dog();


[Link](); // Prints "Woof!"

What happens at runtime:

1. JVM sees [Link]() call


2. Checks actual type of myPet → Dog
3. Looks up makeSound() in Dog 's vtable
4. Finds pointer to [Link]() implementation
5. Executes that method

vtable Visualization

Animal vtable:
[0] [Link]()
[1] [Link]()
[2] [Link]() → points to [Link]()

Dog vtable:
[0] [Link]()
[1] [Link]()
[2] [Link]() → points to [Link]() // Overridden!

💡 Performance Implications
Virtual calls are slightly slower than static calls (vtable lookup)
JIT compiler optimizes frequently called virtual methods
Final methods can be inlined (no vtable lookup needed)
Monomorphic calls (same type always) are heavily optimized
HotSpot Optimization Example

// If JIT detects this always calls [Link]():


for (int i = 0; i < 1000000; i++) {
animals[i].makeSound(); // All Dog instances
}

// JIT may inline the call directly to [Link]()


// Eliminating vtable lookup entirely!

📥 3. Class Loading and Linking


🎯 The Class Loading Process
Java classes are loaded lazily and go through three phases:

Loading → Linking → Initialization

Loading Phase
ClassLoader finds and reads .class file bytecode
Creates internal representation in Method Area
Loads superclass first (recursive)

Linking Phase
1. Verification: Ensures bytecode is valid and safe
2. Preparation: Allocates memory for static fields, sets defaults
3. Resolution: Converts symbolic references to direct references

Initialization Phase
Executes static initializers and static field assignments
Happens when class is first actively used

🔍 Class Loading Order Example


public class ClassLoadingDemo {
static { [Link]("1. ClassLoadingDemo static block"); }

{ [Link]("2. Instance initializer"); }

private static String staticField = initStatic();

private String instanceField = initInstance();

public ClassLoadingDemo() {
[Link]("4. Constructor");
}

private static String initStatic() {


[Link](" -> Initializing static field");
return "static";
}

private String initInstance() {


[Link](" -> Initializing instance field");
return "instance";
}

public static void main(String[] args) {


[Link]("5. Main method start");
new ClassLoadingDemo();
[Link]("6. Main method end");
}
}

Output:

1. ClassLoadingDemo static block


-> Initializing static field
2. Main method start
3. Instance initializer
-> Initializing instance field
4. Constructor
5. Main method end

ClassLoader Hierarchy

Bootstrap ClassLoader (native, loads [Link])



Extension ClassLoader (loads jre/lib/ext)

Application ClassLoader (loads application classes)

Custom ClassLoaders (if defined)

💡 Why This Matters


Lazy loading: Classes aren't loaded until needed (memory efficiency)
Static initialization order: Critical for understanding startup behavior
Custom ClassLoaders: Enable dynamic code loading (plugins, hot deployment)

🧪 4. Escape Analysis and Stack Allocation


🎯 Optimizing Object Allocation
The JVM uses escape analysis to determine if an object can be allocated on the stack instead of the heap.

What is "Escape"?
An object escapes if it:

Is stored in a field of another object


Is returned from a method
Is passed to another thread

Stack Allocation Benefits


Faster allocation: Stack allocation is cheaper than heap
No GC pressure: Stack objects are automatically cleaned up
Better cache locality: Stack memory is more cache-friendly
🔍 Escape Analysis Example
public class EscapeAnalysisDemo {

// Object doesn't escape - eligible for stack allocation


public void processLocally() {
Point p = new Point(10, 20); // May be stack-allocated
int distance = calculateDistance(p);
[Link]("Distance: " + distance);
// 'p' is not returned, stored, or shared → doesn't escape
}

// Object escapes - must be heap-allocated


public Point createPoint() {
Point p = new Point(10, 20); // Must be heap-allocated
return p; // Escapes by being returned
}

private int calculateDistance(Point p) {


return (int) [Link](p.x * p.x + p.y * p.y);
}
}

Scalar Replacement
When escape analysis determines an object doesn't escape, the JVM may replace the object with individual
primitive fields (scalar replacement):

// Original code
Point p = new Point(10, 20);
int sum = p.x + p.y;

// After scalar replacement (conceptually)


int x = 10;
int y = 20;
int sum = x + y;
// No Point object created at all!

Enabling Escape Analysis


Enabled by default in modern JVMs ( -XX:+DoEscapeAnalysis )
Requires JIT compilation (not in interpreted mode)
Most effective for short-lived, local objects

💡 Performance Impact
Significant GC reduction for local object-heavy code
Faster execution due to stack allocation and scalar replacement
Automatic optimization - no code changes needed

🔤 5. String Interning and Memory Management


🎯 How Strings Are Optimized
Java maintains a String pool (intern pool) to reuse immutable String objects.

String Creation Methods


// Method 1: String literal - goes to String pool
String s1 = "Hello"; // Pool: "Hello"
String s2 = "Hello"; // Reuses same pool object

// Method 2: new String() - creates new heap object


String s3 = new String("Hello"); // New heap object + pool object
String s4 = new String("Hello"); // Another new heap object

// Method 3: intern() - adds to pool or returns existing


String s5 = [Link](); // Returns pool object "Hello"

Memory Layout Comparison

String Pool (in Metaspace/Heap):


"Hello" → one object

Heap Objects:
s3 → new String("Hello") → separate object
s4 → new String("Hello") → another separate object
s5 → points to pool object "Hello"

Equality Behavior

[Link](s1 == s2); // true (same pool object)


[Link](s1 == s3); // false (different objects)
[Link](s1 == s5); // true (s5 points to pool)
[Link]([Link](s3)); // true (same content)

String Concatenation Internals

// Before Java 9: StringBuilder


String result = "Hello" + " " + name + "!";

// Compiles to:
StringBuilder sb = new StringBuilder();
[Link]("Hello");
[Link](" ");
[Link](name);
[Link]("!");
String result = [Link]();

// Java 9+: Invokedynamic for better optimization


// JVM can choose optimal strategy at runtime

💡 Best Practices
Use string literals when possible ( "text" vs new String("text") )
Avoid unnecessary interning (it's automatic for literals)
Be careful with == for strings - use .equals() for content comparison
Understand that string concatenation in loops creates many temporary objects

Memory Optimization Example

// Bad: Creates many intermediate strings


String result = "";
for (String item : items) {
result += item + ","; // New StringBuilder each iteration!
}

// Good: Explicit StringBuilder


StringBuilder sb = new StringBuilder();
for (String item : items) {
[Link](item).append(",");
}
String result = [Link]();

🧪 Module 9 Exercises
Exercise 1: Object Memory Analysis
Use JOL (Java Object Layout) to analyze the memory footprint of different object designs:

Compare inheritance vs composition memory usage


Analyze the impact of field ordering on object size
Measure memory overhead of small objects

Exercise 2: Polymorphism Performance


Write benchmarks to compare:

Virtual method calls vs static method calls


Final methods vs overridable methods
Interface calls vs abstract class calls

Exercise 3: Escape Analysis Testing


Create microbenchmarks to observe escape analysis in action:

Local object creation vs returned objects


Measure GC pressure with and without escape analysis
Test scalar replacement with simple data classes

Exercise 4: String Interning Investigation


Experiment with different string creation patterns:

Compare memory usage of literals vs new String()


Test string concatenation performance in loops
Analyze when intern() is beneficial vs harmful

Exercise 5: Class Loading Exploration


Create a custom ClassLoader and experiment with:

Dynamic class loading at runtime


Class reloading scenarios
ClassLoader isolation and delegation

✅ Module 9 Mastery Checklist


Understand object memory layout and overhead
Know how vtables enable polymorphism and their performance implications
Understand class loading phases and initialization order
Recognize when escape analysis can optimize object allocation
Understand String interning and its memory implications
Can use tools like JOL to analyze memory layout
Understand that many OOP optimizations happen automatically via JIT
Know when low-level knowledge should influence high-level design decisions

💡 Professional Insight: You rarely need to optimize based on JVM internals—but understanding them helps you
write naturally efficient code and debug performance issues when they arise. The JVM is incredibly smart;
trust it to optimize your well-designed OOP code.

You might also like