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.
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. An object (instance) is a specific realisation with its own independent state.
Variables stored inside the object — each instance has its own copy.
Functions defined inside the class — 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 |
| State | No runtime state | Has its own state |
| Created with | class keyword | new / () call |
// ── Classes & Objects ─────────────────────────────────────────
public class BankAccount {
private String accountId;
private String holderName;
private double balance;
private final String currency;
public BankAccount(String accountId, String holderName, String currency) {
this.accountId = accountId;
this.holderName = holderName;
this.currency = currency;
this.balance = 0.0;
}
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;
}
public double getBalance() { return balance; }
public String getAccountId() { return accountId; }
@Override public String toString() {
return String.format("BankAccount{id='%s', holder='%s', balance=%.2f %s}",
accountId, holderName, balance, currency);
}
@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(); }
}
public class Main {
public static void main(String[] args) {
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);
System.out.println(alice.equals(bob)); // false
}
}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.
new Thread(runnable) creates an object from the Thread class. Each Thread manages its own execution stack and state independently.
Mongoose models are classes. Schema defines fields + validations. new User({ name: "Alice" }) creates an instance.
"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 login() — 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 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.
// ── Encapsulation ─────────────────────────────────────────────
public class UserAccount {
private String email;
private String passwordHash;
private boolean verified;
private int failedLoginAttempts;
private static final int MAX_ATTEMPTS = 5;
public UserAccount(String email, String rawPassword) {
setEmail(email);
this.passwordHash = hash(rawPassword);
this.verified = false;
this.failedLoginAttempts = 0;
}
public String getEmail() { return email; }
public boolean isVerified() { return verified; }
public boolean isLocked() { return failedLoginAttempts >= MAX_ATTEMPTS; }
public void setEmail(String email) {
if (email == null || !email.contains("@"))
throw new IllegalArgumentException("Invalid email: " + email);
this.email = email.toLowerCase().trim();
}
public boolean login(String rawPassword) {
if (isLocked()) throw new IllegalStateException("Account locked");
if (hash(rawPassword).equals(passwordHash)) {
failedLoginAttempts = 0;
return true;
}
failedLoginAttempts++;
return false;
}
public void verify() { this.verified = true; }
private String hash(String raw) { return Integer.toHexString(raw.hashCode()); }
}"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, override, extend, or add new methods.
Inheritance creates an IS-A relationship: ElectricCar IS-A Vehicle. 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. This is why "favour composition over inheritance" is the modern advice.
// ── Inheritance ───────────────────────────────────────────────
public class Vehicle {
protected String make, model;
protected int year;
protected double fuelLevel = 1.0;
public Vehicle(String make, String model, int year) {
this.make = make; this.model = model; this.year = year;
}
public void startEngine() { System.out.println(make + " " + model + " engine started."); }
public void refuel(double amount) { fuelLevel = Math.min(1.0, fuelLevel + amount); }
public String getInfo() { return year + " " + make + " " + model; }
}
public class ElectricCar extends Vehicle {
private double batteryLevel = 1.0;
private int rangeKm;
public ElectricCar(String make, String model, int year, int rangeKm) {
super(make, model, year);
this.rangeKm = rangeKm;
}
@Override public void startEngine() {
System.out.println(make + " " + model + " motors activated silently. ⚡");
}
@Override public void refuel(double amount) {
batteryLevel = Math.min(1.0, batteryLevel + amount);
System.out.printf("Charged to %.0f%%. Range: ~%d km%n",
batteryLevel * 100, (int)(batteryLevel * rangeKm));
}
public int estimatedRange() { return (int)(batteryLevel * rangeKm); }
@Override public String getInfo() {
return super.getInfo() + " [Electric, " + rangeKm + "km range]";
}
}
Vehicle car = new ElectricCar("Tesla", "Model 3", 2024, 500);
car.startEngine();
car.refuel(0.5);
System.out.println(car.getInfo());
System.out.println(car instanceof Vehicle); // trueThrowable ← Exception ← RuntimeException ← IllegalArgumentException. Each level adds specificity.
IOBase ← RawIOBase ← FileIO. Each subclass specialises for a different kind of I/O while inheriting the core protocol.
class MyComponent extends React.Component. Inherits render lifecycle, setState, and all React hooks.
class Customer(User) adds customer-specific fields to the built-in User. All User methods work unchanged.
"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.
Shape s = new Circle(); s.area()Method overloading
Same method name, different parameter types. The compiler picks the right version based on argument types.
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.
# 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 ──────────────────────────────────────────────
abstract class Shape {
protected String colour;
public Shape(String colour) { this.colour = colour; }
public abstract double area();
public abstract double perimeter();
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 c, double w, double h) { super(c); width=w; height=h; }
@Override public double area() { return width * height; }
@Override public double perimeter() { return 2 * (width + height); }
}
// Same call — different behaviour at runtime
List<Shape> canvas = List.of(new Circle("Red",5), new Rectangle("Blue",4,6));
double totalArea = 0;
for (Shape shape : canvas) {
shape.describe(); // polymorphic dispatch
totalArea += shape.area();
}
System.out.printf("Total area: %.2f%n", totalArea);"Show only what's necessary. Hide the complexity behind a clean, simple interface."
Hides internals of a class using access modifiers. The hiding is about state and implementation details within a class.
Hides entire implementations behind an interface. You only know the contract — whether it's Stripe or Razorpay underneath is invisible.
// ── Abstraction — Template Method Pattern ─────────────────────
public abstract class PaymentGateway {
public final PaymentResult processPayment(PaymentRequest req) {
validateRequest(req);
PaymentResult result = charge(req); // abstract step
audit(req, result);
return result;
}
protected abstract PaymentResult charge(PaymentRequest req);
private void validateRequest(PaymentRequest req) {
if (req.getAmount() <= 0) throw new IllegalArgumentException("Amount must be positive");
}
private void audit(PaymentRequest req, PaymentResult result) {
System.out.printf("[AUDIT] %s | %.2f %s | %s%n",
req.getGateway(), req.getAmount(), req.getCurrency(), result.getStatus());
}
}
public class StripeGateway extends PaymentGateway {
@Override
protected PaymentResult charge(PaymentRequest req) {
System.out.println("[Stripe] Charging via Stripe API...");
return new PaymentResult("SUCCESS", "ch_stripe_" + System.nanoTime());
}
}
// Client programs to the abstraction — doesn't know it's Stripe
PaymentGateway gateway = new StripeGateway();
gateway.processPayment(new PaymentRequest(999.00, "INR", "Stripe"));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.
req and res abstract raw Node.js IncomingMessage. You call res.json() without knowing the HTTP wire format.
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) | ✓ |
| Relationship | "can do" (CAN-DO) | "is a kind of" (IS-A) |
// ── Interface vs Abstract Class ────────────────────────────────
// INTERFACE — pure contract, implement many
public interface Serialisable {
byte[] serialise();
default String toBase64() {
return java.util.Base64.getEncoder().encodeToString(serialise());
}
}
public interface Auditable { String getAuditId(); }
public interface Publishable { void publish(String topic); }
// One class, many roles
public class OrderEvent implements Serialisable, Auditable, Publishable {
private final String orderId;
public OrderEvent(String orderId) { this.orderId = orderId; }
@Override public byte[] serialise() { return orderId.getBytes(); }
@Override public String getAuditId() { return orderId; }
@Override public void publish(String topic) { System.out.println("Published " + orderId + " → " + topic); }
}
// ABSTRACT CLASS — shared code + contract
public abstract class Report {
protected final String title;
protected Report(String title) { this.title = title; }
public final String generate() {
return buildHeader() + "\n" + buildBody() + "\n" + buildFooter();
}
private String buildHeader() { return "=== " + title.toUpperCase() + " ==="; }
private String buildFooter() { return "=== End of Report ==="; }
protected abstract String buildBody();
}
public class SalesReport extends Report {
private final java.util.List<String> items;
public SalesReport(java.util.List<String> items) { super("Sales Report"); this.items = items; }
@Override protected String buildBody() { return String.join("\n", items); }
}"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, creates the Fragile Base Class problem, and leads to deep hierarchies that are hard to reason about.
The rule of thumb: Use inheritance only for genuine IS-A relationships. For everything else — capabilities, behaviours, shared utilities — use composition. When in doubt, compose.
// ── Composition vs Inheritance ────────────────────────────────
// Capabilities as independent interfaces
@FunctionalInterface interface FlyBehaviour { void fly(); }
@FunctionalInterface interface SwimBehaviour { void swim(); }
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."); } }
class Animal {
private final String name;
private final FlyBehaviour flyBehaviour;
private final SwimBehaviour swimBehaviour;
public Animal(String name, FlyBehaviour fly, SwimBehaviour swim) {
this.name = name; flyBehaviour = fly; swimBehaviour = swim;
}
public void performActions() {
System.out.print(name + ": "); flyBehaviour.fly();
System.out.print(name + ": "); swimBehaviour.swim();
}
}
Animal duck = new Animal("Duck", new WingFlight(), new BreaststrokeSwim());
Animal penguin = new Animal("Penguin", new NoFlight(), new BreaststrokeSwim());
Animal eagle = new Animal("Eagle", new WingFlight(), new NoSwim());
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. The type parameter is checked at compile time.
Result<User>, Optional<String>Fluent construction of complex objects step by step. Creates immutable objects with readable construction code.
HttpRequest.builder("POST", url).header(...).build()__get__, __set__, __set_name__ — intercept attribute access at the class level. Power @property, @classmethod, and Django Field classes.
class Validated: def __set__(self, obj, val): ...Classes that control class creation. Custom metaclasses enforce patterns at definition time — Singleton, ORMs, ABC.
class SingletonMeta(type): def __call__(cls, ...)Intercept fundamental object operations (get, set, has). Powers Vue 3 reactivity and validation layers without modifying the target.
new Proxy(target, { set(obj, prop, val) { ... } })Any object that implements [Symbol.iterator] (JS) or __iter__/__next__ (Python) can be iterated with for loops and spread operators.
class Range { [Symbol.iterator]() { ... } }// ── Advanced OOP — Result type + Builder ──────────────────────
import java.util.function.*;
public class Result<T> {
private final T value; private final String error; private final boolean success;
private Result(T v, String e, boolean ok) { value=v; error=e; success=ok; }
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 java.util.NoSuchElementException(); return value; }
public <U> Result<U> map(Function<T,U> f) { return success ? Result.ok(f.apply(value)) : Result.fail(error); }
public <U> Result<U> flatMap(Function<T,Result<U>> f){ return success ? f.apply(value) : Result.fail(error); }
}
// Builder Pattern
public class HttpRequest {
private final String method, url, body;
private final java.util.Map<String,String> headers;
private final int timeoutMs;
private HttpRequest(Builder b) { method=b.method; url=b.url; headers=java.util.Collections.unmodifiableMap(b.headers); body=b.body; 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 java.util.Map<String,String> headers = new java.util.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 b) { body=b; 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();Django'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 for 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.
| 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 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: