A complete guide to OOP — from first principles (classes, objects) through the four pillars, to advanced topics like generics, composition, descriptors, and metaclasses. Every concept shown in Java, Python, and JavaScript.
Object-Oriented Programming is a programming paradigm that organises software around objects — bundles of state (data) and behaviour (methods) — rather than around functions and logic alone. The core insight is that real-world problems are easier to model when code reflects the structure of the problem domain.
OOP rests on four traditional pillars — Encapsulation, Inheritance, Polymorphism, and Abstraction — plus a set of advanced techniques that build on top of them.
📦 Classes & Objects
The basic building blocks — blueprints and instances
🔒 Encapsulation
Bundle state + behaviour, hide implementation details
🌱 Inheritance
Reuse and specialise existing classes
🎭 Polymorphism
Same interface, different behaviour at runtime
🧩 Abstraction
Expose what, hide how — simplify interfaces
🔀 Composition
"Has-a" over "is-a" — flexible object assembly
⚡ Advanced OOP
Generics, descriptors, metaclasses, protocols
"A class is a blueprint. An object is a specific thing built from that blueprint."
Before OOP, programs were collections of procedures operating on raw data. OOP bundles the data (fields/attributes) and the operations on that data (methods) together into one unit — the object.
A class defines the structure: what fields every instance will have and what methods it can perform. An object (instance) is a specific realisation of that class with its own independent state. Think of a class as the mould and objects as the individual castings from that mould — all the same shape, but each filled with its own data.
Variables stored inside the object — name, balance, colour. Each instance has its own copy.
Functions defined inside the class — deposit(), withdraw(). They operate on the object's own fields.
Special method called when the object is created. Sets up initial state.
| Aspect | Class | Instance (Object) |
|---|---|---|
| What it is | Blueprint / Template | Real thing in memory |
| Count | One per definition | Unlimited instances |
| Fields | Describes the shape | Holds actual values |
| State | No runtime state | Has its own state |
| Created with | class keyword | new / () call |
// ── Classes & Objects ─────────────────────────────────────────
// A CLASS is a blueprint. An OBJECT is an instance built from that blueprint.
public class BankAccount {
// ── Fields (state) ────────────────────────────────────────
private String accountId;
private String holderName;
private double balance;
private final String currency;
// ── Constructor ───────────────────────────────────────────
// Called when you write: new BankAccount(...)
public BankAccount(String accountId, String holderName, String currency) {
this.accountId = accountId;
this.holderName = holderName;
this.currency = currency;
this.balance = 0.0; // default state
}
// ── Methods (behaviour) ───────────────────────────────────
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
this.balance += amount;
System.out.printf("[%s] Deposited %.2f. Balance: %.2f %s%n",
accountId, amount, balance, currency);
}
public void withdraw(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
if (amount > this.balance) throw new IllegalStateException("Insufficient funds");
this.balance -= amount;
System.out.printf("[%s] Withdrew %.2f. Balance: %.2f %s%n",
accountId, amount, balance, currency);
}
public double getBalance() { return balance; }
public String getAccountId() { return accountId; }
// ── toString — human-readable representation ──────────────
@Override
public String toString() {
return String.format("BankAccount{id='%s', holder='%s', balance=%.2f %s}",
accountId, holderName, balance, currency);
}
// ── equals & hashCode — value-based identity ──────────────
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BankAccount)) return false;
return accountId.equals(((BankAccount) o).accountId);
}
@Override
public int hashCode() { return accountId.hashCode(); }
}
// ── Creating objects (instances) ──────────────────────────────
public class Main {
public static void main(String[] args) {
// 'new' allocates memory and calls the constructor
BankAccount alice = new BankAccount("ACC-001", "Alice", "INR");
BankAccount bob = new BankAccount("ACC-002", "Bob", "INR");
alice.deposit(50_000);
alice.withdraw(12_000);
bob.deposit(20_000);
System.out.println(alice); // BankAccount{id='ACC-001', ...}
System.out.println(bob);
// Two variables, two independent objects in memory
System.out.println(alice.equals(bob)); // false — different accountIds
System.out.println(alice == alice); // true — same reference
}
}User.objects.create(email=...) creates an instance from the User class. The class defines the schema; each user record is an instance with its own data.
A React class component is an OOP class. Its state is instance-level; its render() and lifecycle methods are the behaviour. Each mounted component is a separate object in memory.
new Thread(runnable) creates an object from the Thread class. Each Thread object manages its own execution stack, priority, and state independently.
Mongoose models are classes. Schema defines fields + validations (the blueprint). new User({ name: "Alice" }) creates an instance (the object).
"Bundle data and the methods that operate on it together, and restrict direct access to the internal state."
State and methods that belong together are in one class. UserAccount holds passwordHash AND the login() method that uses it — they're co-located because they're co-dependent.
Internal details are private. Outside code accesses state only through controlled public methods. passwordHash is never exposed — only login() can compare it.
| Level | Java | Python | JavaScript | Accessible from |
|---|---|---|---|---|
| Public | public | name | name | Everyone |
| Protected | protected | _name | _name (convention) | Class + subclasses |
| Private | private | __name | #name | Class only |
Why not just make everything public? Public state lets external code set account.balance = -999 directly, bypassing all validation. Private fields force all changes through methods that enforce business rules. The object stays in a valid state at all times.
// ── Encapsulation — bundle state + behaviour, hide internals ──
public class UserAccount {
// ── Access Modifiers ──────────────────────────────────────
// private → only within this class
// protected → this class + subclasses + same package
// (default) → same package only
// public → everyone
private String email; // private: no direct access
private String passwordHash; // private: never exposed raw
private boolean verified;
private int failedLoginAttempts;
private static final int MAX_ATTEMPTS = 5;
public UserAccount(String email, String rawPassword) {
setEmail(email); // goes through validation
this.passwordHash = hash(rawPassword);
this.verified = false;
this.failedLoginAttempts = 0;
}
// ── Controlled getter — expose only what's needed ─────────
public String getEmail() { return email; }
public boolean isVerified() { return verified; }
public boolean isLocked() { return failedLoginAttempts >= MAX_ATTEMPTS; }
// ── Controlled setter — gate changes through business rules
public void setEmail(String email) {
if (email == null || !email.contains("@"))
throw new IllegalArgumentException("Invalid email: " + email);
this.email = email.toLowerCase().trim();
}
// ── Behaviour that guards internal state ──────────────────
public boolean login(String rawPassword) {
if (isLocked()) throw new IllegalStateException("Account locked");
if (hash(rawPassword).equals(passwordHash)) {
failedLoginAttempts = 0; // reset on success
return true;
}
failedLoginAttempts++;
if (isLocked())
System.out.println("⚠️ Account locked after " + MAX_ATTEMPTS + " failed attempts");
return false;
}
public void changePassword(String oldRaw, String newRaw) {
if (!login(oldRaw)) throw new SecurityException("Current password incorrect");
if (newRaw.length() < 8)
throw new IllegalArgumentException("Password must be ≥ 8 characters");
this.passwordHash = hash(newRaw);
System.out.println("Password changed successfully.");
}
public void verify() { this.verified = true; }
// passwordHash is NEVER returned — encapsulation protects it
private String hash(String raw) {
return Integer.toHexString(raw.hashCode()); // simplified
}
}
// Client code — works through the public interface only
UserAccount user = new UserAccount("alice@example.com", "secret123");
user.verify();
System.out.println(user.isVerified()); // true
System.out.println(user.login("wrong")); // false — 1 attempt
// user.passwordHash → compile error: passwordHash is private ✓"A subclass inherits the fields and methods of its parent, then extends or overrides them to specialise behaviour."
When class B extends A, B automatically gets all of A's non-private fields and methods. B can then: use inherited methods as-is, override them to change behaviour, extend them by calling super first, or add entirely new methods specific to B.
Inheritance creates an IS-A relationship: ElectricCar IS-A Vehicle. This means anywhere you use a Vehicle, you can use an ElectricCar — the foundation of polymorphism.
⚠️ Inheritance pitfall — Fragile Base Class: Changes to a parent class can unexpectedly break all subclasses. If you override a method and the parent changes its internal logic, your override may become wrong. This is why "favour composition over inheritance" is the modern advice — covered in its own section below.
// ── Inheritance — reuse and specialise a parent class ─────────
// Base class (superclass / parent)
public class Vehicle {
protected String make;
protected String model;
protected int year;
protected double fuelLevel; // 0.0 – 1.0
public Vehicle(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
this.fuelLevel = 1.0;
}
public void startEngine() {
System.out.println(make + " " + model + " engine started.");
}
public void refuel(double amount) {
fuelLevel = Math.min(1.0, fuelLevel + amount);
System.out.printf("Refuelled. Level: %.0f%%%n", fuelLevel * 100);
}
public String getInfo() {
return year + " " + make + " " + model;
}
}
// Subclass — inherits everything from Vehicle, adds its own state/behaviour
public class ElectricCar extends Vehicle {
private double batteryLevel; // 0.0 – 1.0
private int rangeKm;
public ElectricCar(String make, String model, int year, int rangeKm) {
super(make, model, year); // must call parent constructor first
this.batteryLevel = 1.0;
this.rangeKm = rangeKm;
}
// Override — redefine parent behaviour for this specialisation
@Override
public void startEngine() {
// ElectricCar has no combustion engine — completely different behaviour
System.out.println(make + " " + model + " motors activated silently.");
}
// Override — refuelling an EV means charging
@Override
public void refuel(double amount) {
batteryLevel = Math.min(1.0, batteryLevel + amount);
System.out.printf("Charging complete. Battery: %.0f%% | Range: ~%d km%n",
batteryLevel * 100, (int)(batteryLevel * rangeKm));
}
// New method — specific to ElectricCar, not in Vehicle
public int estimatedRange() {
return (int)(batteryLevel * rangeKm);
}
// super.getInfo() + new info — extending the parent's method
@Override
public String getInfo() {
return super.getInfo() + " [Electric, " + rangeKm + "km range]";
}
}
public class Truck extends Vehicle {
private double payloadTons;
public Truck(String make, String model, int year, double payloadTons) {
super(make, model, year);
this.payloadTons = payloadTons;
}
public void loadCargo(double tons) {
if (tons > payloadTons)
throw new IllegalArgumentException("Exceeds payload capacity");
System.out.printf("Loaded %.1f tonnes onto %s%n", tons, getInfo());
}
}
// ── Usage ─────────────────────────────────────────────────────
Vehicle car = new ElectricCar("Tesla", "Model 3", 2024, 500);
car.startEngine(); // "Tesla Model 3 motors activated silently."
car.refuel(0.5); // "Charging complete. Battery: 50% | Range: ~250 km"
System.out.println(car.getInfo()); // "2024 Tesla Model 3 [Electric, 500km range]"
Truck truck = new Truck("Tata", "T16", 2023, 16.0);
truck.startEngine(); // inherits Vehicle.startEngine()
truck.loadCargo(12.0);Throwable ← Exception ← RuntimeException ← IllegalArgumentException. Each level adds specificity. catch (Exception e) catches all subtype exceptions.
IOBase ← RawIOBase ← FileIO. Each subclass specialises for a different kind of I/O while inheriting the core read/write protocol.
class MyComponent extends React.Component. Inherits render lifecycle, setState, and all React hooks. You override render() and lifecycle methods.
class Customer(User) adds customer-specific fields to Django's built-in User. All User methods (authenticate, check_password) work unchanged on Customer.
"One interface, many implementations — the same method call produces different behaviour depending on the actual object type at runtime."
Dynamic dispatch, method overriding
A variable of type Shape holds a Circle. Calling shape.area() dispatches to Circle.area() at runtime. The compiler doesn't know which implementation runs.
Shape s = new Circle(); s.area()Method overloading
Same method name, different parameter types. The compiler picks the right version based on argument types at compile time. (Java/C++ only — Python and JS don't have true overloading.)
add(int,int) vs add(double,double)Generics / templates
A single generic class works with any type. List<Integer>, List<String> — same implementation, different type parameters.
List<T>, Result<T>, Optional<T>Python and JavaScript use structural polymorphism — if an object has the right method, it works, regardless of its class hierarchy. This is called duck typing: "if it walks like a duck and quacks like a duck, it's a duck."
# Any object with a describe() method works — no base class needed
def print_description(thing):
thing.describe() # polymorphic — resolved at runtime
print_description(Circle("red", 5)) # calls Circle.describe()
print_description(Star()) # Star doesn't extend Shape — doesn't matter!// ── Polymorphism — many forms, one interface ──────────────────
// Polymorphism = the same method call produces different behaviour
// depending on the actual runtime type of the object.
// ─── Shape hierarchy ──────────────────────────────────────────
abstract class Shape {
protected String colour;
public Shape(String colour) { this.colour = colour; }
// Abstract method — every shape MUST provide its own implementation
public abstract double area();
public abstract double perimeter();
// Non-abstract — shared by all shapes, calls the abstract methods
public void describe() {
System.out.printf("%s %s — area=%.2f, perimeter=%.2f%n",
colour, getClass().getSimpleName(), area(), perimeter());
}
}
class Circle extends Shape {
private final double radius;
public Circle(String colour, double radius) {
super(colour); this.radius = radius;
}
@Override public double area() { return Math.PI * radius * radius; }
@Override public double perimeter() { return 2 * Math.PI * radius; }
}
class Rectangle extends Shape {
private final double width, height;
public Rectangle(String colour, double width, double height) {
super(colour); this.width = width; this.height = height;
}
@Override public double area() { return width * height; }
@Override public double perimeter() { return 2 * (width + height); }
}
class Triangle extends Shape {
private final double a, b, c;
public Triangle(String colour, double a, double b, double c) {
super(colour); this.a = a; this.b = b; this.c = c;
}
@Override public double area() {
double s = (a + b + c) / 2;
return Math.sqrt(s * (s-a) * (s-b) * (s-c)); // Heron's formula
}
@Override public double perimeter() { return a + b + c; }
}
// ─── Runtime polymorphism — single loop handles all shapes ────
public class DrawingApp {
public static void main(String[] args) {
List<Shape> canvas = List.of(
new Circle("Red", 5),
new Rectangle("Blue", 4, 6),
new Triangle("Green", 3, 4, 5)
);
// SAME call: shape.describe() — DIFFERENT behaviour per type
double totalArea = 0;
for (Shape shape : canvas) {
shape.describe(); // ← polymorphic dispatch
totalArea += shape.area(); // ← polymorphic dispatch
}
System.out.printf("Total canvas area: %.2f%n", totalArea);
}
}
// ─── Method overloading — compile-time (ad-hoc) polymorphism ──
class Calculator {
public int add(int a, int b) { return a + b; }
public double add(double a, double b) { return a + b; }
public String add(String a, String b) { return a + b; }
// Same name, different signatures → resolved at compile time
}"Show only what's necessary. Hide the complexity behind a clean, simple interface."
These are closely related but distinct: Encapsulation is about how you hide implementation details — using access modifiers, private fields, controlled getters. Abstraction is about what you hide — the entire implementation, replaced by a simpler interface the caller programs against.
Hides internals of a class. You can still see the class exists and call its methods. The hiding is about state and implementation details within a class.
Hides entire implementations behind an interface. You only know the contract — PaymentGateway.processPayment(). Whether it's Stripe or Razorpay underneath is invisible.
The Template Method Pattern: Abstract classes naturally implement the Template Method pattern — the base class defines the algorithm skeleton (the abstract structure), and subclasses fill in the specific steps. This is a direct expression of abstraction in code.
// ── Abstraction — hide complexity, expose essential interface ───
// Abstraction = showing WHAT an object does, hiding HOW it does it.
// In Java: abstract classes and interfaces are the two tools.
// Abstract class — can have both abstract AND concrete methods
public abstract class PaymentGateway {
// Template method — defines the WHAT (the workflow)
// Subclasses fill in the HOW for each step
public final PaymentResult processPayment(PaymentRequest req) {
validateRequest(req); // concrete — shared logic
PaymentResult result = charge(req); // abstract — different per gateway
audit(req, result); // concrete — shared logic
return result;
}
// Abstract — subclasses MUST implement
protected abstract PaymentResult charge(PaymentRequest req);
// Concrete — shared across all gateways
private void validateRequest(PaymentRequest req) {
if (req.getAmount() <= 0)
throw new IllegalArgumentException("Amount must be positive");
if (req.getCurrency() == null)
throw new IllegalArgumentException("Currency required");
}
private void audit(PaymentRequest req, PaymentResult result) {
System.out.printf("[AUDIT] %s | %.2f %s | status=%s%n",
req.getGateway(), req.getAmount(),
req.getCurrency(), result.getStatus());
}
}
// Concrete implementation — provides the HOW
public class StripeGateway extends PaymentGateway {
@Override
protected PaymentResult charge(PaymentRequest req) {
// Stripe-specific HTTP calls, SDK calls, error handling...
System.out.println("[Stripe] Charging card via Stripe API...");
return new PaymentResult("SUCCESS", "ch_stripe_" + System.nanoTime());
}
}
public class RazorpayGateway extends PaymentGateway {
@Override
protected PaymentResult charge(PaymentRequest req) {
System.out.println("[Razorpay] Initiating Razorpay payment...");
return new PaymentResult("SUCCESS", "pay_rzp_" + System.nanoTime());
}
}
// ── Client uses the abstraction — doesn't know (or care) which gateway ─
PaymentGateway gateway = new StripeGateway(); // or RazorpayGateway
PaymentResult result = gateway.processPayment(
new PaymentRequest(999.00, "INR", "Stripe"));
System.out.println("Result: " + result.getStatus());
// No Stripe-specific code in the client. Pure abstraction.Connection, Statement, ResultSet abstract away every database vendor. Code written against JDBC runs unchanged on MySQL, Postgres, or SQLite.
ABCMeta and @abstractmethod enforce contracts. Subclasses that miss an abstract method raise TypeError at import time — caught before runtime.
req and res abstract raw Node.js IncomingMessage and ServerResponse. You call res.json() without knowing the HTTP wire format underneath.
AWS S3, GCS, Azure Blob all expose a similar put/get/delete abstraction. Infrastructure changes; your code doesn't.
"Interfaces define what a type can DO. Abstract classes define what a type IS, with some of the HOW already filled in."
| Feature | Interface / Protocol | Abstract Class |
|---|---|---|
| Multiple inheritance | ✓ implement many | ✗ extend one (Java/JS) |
| Constructor | ✗ | ✓ |
| Instance fields | ✗ (only constants) | ✓ |
| Concrete methods | default only (Java 8+) | ✓ fully |
| Relationship | "can do" (CAN-DO) | "is a kind of" (IS-A) |
| Instantiate directly | ✗ | ✗ |
// ── Interface vs Abstract Class — when to use which ───────────
// ─── INTERFACE ─────────────────────────────────────────────────
// • Pure contract — only method signatures (Java 8+: default methods too)
// • A class can implement MULTIPLE interfaces
// • No constructor, no instance state
// • Use when: defining a capability/role a class can play
public interface Serialisable {
byte[] serialise();
static <T> T deserialise(byte[] data, Class<T> type) { /* ... */ return null; }
default String toBase64() {
return Base64.getEncoder().encodeToString(serialise());
}
}
public interface Auditable {
String getAuditId();
LocalDateTime getCreatedAt();
LocalDateTime getUpdatedAt();
}
public interface Publishable {
void publish(String topic);
}
// A class can wear many interface "hats"
public class OrderEvent implements Serialisable, Auditable, Publishable {
private final String orderId;
private final LocalDateTime createdAt = LocalDateTime.now();
public OrderEvent(String orderId) { this.orderId = orderId; }
@Override public byte[] serialise() { return orderId.getBytes(); }
@Override public String getAuditId() { return orderId; }
@Override public LocalDateTime getCreatedAt() { return createdAt; }
@Override public LocalDateTime getUpdatedAt() { return createdAt; }
@Override public void publish(String topic) {
System.out.println("Published " + orderId + " to " + topic);
}
}
// ─── ABSTRACT CLASS ────────────────────────────────────────────
// • Partial implementation — mix of abstract + concrete methods
// • Can have constructor, fields, protected helpers
// • A class can extend only ONE abstract class (single inheritance)
// • Use when: sharing code + enforcing a contract across related classes
public abstract class Report {
protected final String title;
protected final LocalDate date;
protected Report(String title) {
this.title = title;
this.date = LocalDate.now();
}
// Template method — locked workflow
public final String generate() {
return buildHeader() + "\n" + buildBody() + "\n" + buildFooter();
}
private String buildHeader() {
return "=== " + title.toUpperCase() + " | " + date + " ===";
}
private String buildFooter() {
return "=== End of Report ===";
}
// Subclasses fill in the body
protected abstract String buildBody();
}
public class SalesReport extends Report {
private final List<Sale> sales;
public SalesReport(List<Sale> sales) { super("Sales Report"); this.sales = sales; }
@Override protected String buildBody() {
return sales.stream()
.map(s -> s.item() + ": ₹" + s.amount())
.collect(Collectors.joining("\n"));
}
}
// ─── Comparison Table ──────────────────────────────────────────
// Feature │ Interface │ Abstract Class
// ─────────────────────┼────────────────────┼──────────────────
// Multiple inheritance │ ✓ (implement many) │ ✗ (extend one)
// Constructors │ ✗ │ ✓
// Instance fields │ ✗ (only constants) │ ✓
// Partial impl │ default methods │ ✓ fully
// IS-A relationship │ "can do" │ "is a kind of"
// Use when │ defining a role │ sharing code"Favour object composition over class inheritance." — Gang of Four (Design Patterns, 1994)
Inheritance creates a compile-time binding between parent and child. It exposes parent internals to subclasses (violating encapsulation), creates a fragile dependency (the Fragile Base Class problem), and leads to deep hierarchies that are hard to reason about.
Worse, inheritance forces an all-or-nothing choice — a Penguin must inherit all FlyingAnimal behaviour even though it can't fly. You end up with throw-based "implementations" that violate Liskov.
The rule of thumb: Use inheritance only for genuine IS-A relationships where the subtype truly is a more specific version of the parent AND honours its full contract (Liskov). For everything else — capabilities, behaviours, shared utilities — use composition. When in doubt, compose.
// ── Composition vs Inheritance ────────────────────────────────
// ─── ❌ Inheritance overuse — "is-a" forced where it doesn't fit ─
class Animal {
public void eat() { System.out.println("Eating..."); }
public void sleep() { System.out.println("Sleeping..."); }
}
class FlyingAnimal extends Animal {
public void fly() { System.out.println("Flying..."); }
}
class SwimmingAnimal extends Animal {
public void swim() { System.out.println("Swimming..."); }
}
// What about a Duck that flies AND swims?
// FlyingSwimmingAnimal? Java has single inheritance — stuck!
// What about a Penguin (swims but can't fly)?
// ─── ✅ Composition — "has-a" capability objects ──────────────
// Define capabilities as independent pieces
@FunctionalInterface
interface FlyBehaviour { void fly(); }
@FunctionalInterface
interface SwimBehaviour { void swim(); }
@FunctionalInterface
interface SoundBehaviour { void makeSound(); }
// Concrete behaviour implementations
class WingFlight implements FlyBehaviour { public void fly() { System.out.println("Flying with wings!"); } }
class RocketPack implements FlyBehaviour { public void fly() { System.out.println("ROCKET PROPULSION!"); } }
class NoFlight implements FlyBehaviour { public void fly() { System.out.println("Can't fly."); } }
class BreaststrokeSwim implements SwimBehaviour { public void swim() { System.out.println("Swimming gracefully."); } }
class NoSwim implements SwimBehaviour { public void swim() { System.out.println("Can't swim."); } }
// Compose behaviours into animals
class Animal {
private final String name;
private final FlyBehaviour flyBehaviour;
private final SwimBehaviour swimBehaviour;
private final SoundBehaviour soundBehaviour;
public Animal(String name, FlyBehaviour fly, SwimBehaviour swim, SoundBehaviour sound) {
this.name = name;
this.flyBehaviour = fly;
this.swimBehaviour = swim;
this.soundBehaviour = sound;
}
public void performActions() {
System.out.print(name + ": ");
flyBehaviour.fly();
System.out.print(name + ": ");
swimBehaviour.swim();
System.out.print(name + ": ");
soundBehaviour.makeSound();
}
// Behaviours can be swapped at RUNTIME — inheritance can't do this
public Animal withFlyBehaviour(FlyBehaviour newFly) {
return new Animal(name, newFly, swimBehaviour, soundBehaviour);
}
}
// Compose freely
Animal duck = new Animal("Duck", new WingFlight(), new BreaststrokeSwim(), () -> System.out.println("Quack!"));
Animal penguin = new Animal("Penguin", new NoFlight(), new BreaststrokeSwim(), () -> System.out.println("Honk!"));
Animal eagle = new Animal("Eagle", new WingFlight(), new NoSwim(), () -> System.out.println("Screech!"));
Animal jetpack = new Animal("Human", new RocketPack(), new NoSwim(), () -> System.out.println("Woohoo!"));
duck.performActions();
penguin.performActions();
eagle.performActions();"Generics, descriptors, metaclasses, proxies, and language-level object protocols — the mechanics that power every framework."
Write one class that works safely with any type. Result<T> wraps any success value. List<T> holds any element type. The type parameter is checked at compile time — no runtime ClassCastException.
Result<User>, Optional<String>, Map<String, Integer>Fluent construction of complex objects step by step. Especially useful when a constructor would need too many parameters. Creates immutable objects with readable construction code.
HttpRequest.builder("POST", url).header(...).body(...).build()__get__, __set__, __set_name__ — intercept attribute access at the class level. Power @property, @classmethod, @staticmethod, and ORMs like Django's Field classes.
class Validated: def __set__(self, obj, val): validate(val)Classes that control class creation. type is the metaclass of all classes. Custom metaclasses enforce patterns at definition time — Singleton, ORMs, ABC enforcement.
class SingletonMeta(type): def __call__(cls, *a, **kw): ...Intercept fundamental object operations (get, set, has, delete) on any object. Powers Vue 3's reactivity, validation layers, and observability without modifying the target.
new Proxy(target, { set(obj, prop, val) { validate(val); ... } })Any object that implements [Symbol.iterator] (JS) or __iter__/__next__ (Python) can be iterated with for...of / for...in loops and spread operators.
class Range { [Symbol.iterator]() { ... } }; [...new Range(1,10)]// ── Advanced OOP — Generics, Covariance, Builder, Fluent API ──
import java.util.*;
import java.util.function.*;
// ─── 1. Generics — type-safe, reusable classes ────────────────
public class Result<T> {
private final T value;
private final String error;
private final boolean success;
private Result(T value, String error, boolean success) {
this.value = value; this.error = error; this.success = success;
}
public static <T> Result<T> ok(T value) { return new Result<>(value, null, true); }
public static <T> Result<T> fail(String err) { return new Result<>(null, err, false); }
public boolean isSuccess() { return success; }
public T getValue() { if (!success) throw new NoSuchElementException(); return value; }
public String getError() { return error; }
// Functor — transform the value if present
public <U> Result<U> map(Function<T, U> mapper) {
return success ? Result.ok(mapper.apply(value)) : Result.fail(error);
}
// Monad — chain Result-returning operations
public <U> Result<U> flatMap(Function<T, Result<U>> mapper) {
return success ? mapper.apply(value) : Result.fail(error);
}
}
// Usage — railway-oriented error handling
Result<Integer> result = Result.ok("42")
.map(Integer::parseInt)
.flatMap(n -> n > 0 ? Result.ok(n * 2) : Result.fail("Must be positive"));
// ─── 2. Builder Pattern — fluent construction ─────────────────
public class HttpRequest {
private final String method;
private final String url;
private final Map<String, String> headers;
private final String body;
private final int timeoutMs;
private HttpRequest(Builder b) {
this.method = b.method;
this.url = b.url;
this.headers = Collections.unmodifiableMap(b.headers);
this.body = b.body;
this.timeoutMs = b.timeoutMs;
}
public static Builder builder(String method, String url) {
return new Builder(method, url);
}
public static class Builder {
private final String method, url;
private final Map<String, String> headers = new LinkedHashMap<>();
private String body = "";
private int timeoutMs = 5000;
private Builder(String method, String url) {
this.method = method; this.url = url;
}
public Builder header(String k, String v) { headers.put(k, v); return this; }
public Builder body(String body) { this.body = body; return this; }
public Builder timeout(int ms) { timeoutMs = ms; return this; }
public HttpRequest build() { return new HttpRequest(this); }
}
}
HttpRequest req = HttpRequest.builder("POST", "https://api.example.com/orders")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body("{"item":"laptop","qty":1}")
.timeout(3000)
.build();
// ─── 3. Bounded generics — constrain the type parameter ───────
public <T extends Comparable<T>> T findMax(List<T> list) {
return list.stream().max(Comparator.naturalOrder())
.orElseThrow(() -> new NoSuchElementException("Empty list"));
}
Integer max = findMax(List.of(3, 1, 4, 1, 5, 9, 2, 6)); // 9Django's CharField, IntegerField are Python Descriptors. They intercept __set__ on model instances to validate data before it reaches the database.
Vue's reactive() wraps objects in a JavaScript Proxy. When you set state.count = 5, the Proxy's set trap fires, triggering DOM updates automatically.
ArrayList<User> uses generics to give you compile-time type safety. The JVM erases types at runtime (type erasure), but javac catches ClassCastException at compile time.
A metaclass-like decorator that inspects class annotations at definition time and generates __init__, __repr__, __eq__ automatically — metaprogramming applied to OOP.
| Concept | Core Question |
|---|---|
| Classes & Objects | "What blueprint does this data and behaviour belong to?" |
| Encapsulation | "Who is allowed to change this state?" |
| Inheritance | "Is this a more specific version of the parent?" |
| Polymorphism | "Can one call work for all types without if-else?" |
| Abstraction | "Can the caller ignore the implementation details?" |
| Composition | "Should I use HAS-A instead of IS-A here?" |
| Advanced OOP | "Can a language feature handle this pattern automatically?" |
Design patterns are proven OOP solutions to recurring design problems. Each pattern applies one or more OOP concepts to solve a specific structural challenge: