What is Object-Oriented Programming
Object-Oriented Programming (OOP) is a programming paradigm that organizes software around objects — entities that bundle data (attributes) and behavior (methods) together. Instead of writing a sequence of instructions (procedural style), you model your problem as a collection of interacting objects.
Three major paradigms exist side by side. Procedural programming (C, Pascal) structures code as a sequence of functions operating on shared data — simple but hard to scale. OOP (Java, Python, C++) groups data and behavior into self-contained objects that communicate by sending messages. Functional programming (Haskell, Elixir) treats computation as evaluation of mathematical functions and avoids shared state.
OOP dominates enterprise software, game development, and systems design interviews because it maps naturally to real-world entities. A bank system has Account objects, Transaction objects, and Customer objects. Each knows its own data and exposes only what other objects need.
The four pillars — Encapsulation, Inheritance, Polymorphism, and Abstraction — are the foundation. Every OOP interview question traces back to at least one of them.
Code example
# Procedural vs OOP — same problem, two styles
# Procedural: functions operating on raw data
def calculate_area(shape_type, width, height):
if shape_type == "rectangle":
return width * height
elif shape_type == "triangle":
return 0.5 * width * height
# OOP: objects that know how to describe themselves
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Triangle:
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
r = Rectangle(5, 3)
print(r.area()) # 15// Procedural vs OOP — same problem, two styles
// Procedural: functions operating on raw data
double calculateArea(String shapeType, double width, double height) {
if (shapeType.equals("rectangle")) return width * height;
if (shapeType.equals("triangle")) return 0.5 * width * height;
return 0;
}
// OOP: objects that know how to describe themselves
class Rectangle {
private final double width;
private final double height;
Rectangle(double width, double height) { this.width = width; this.height = height; }
double area() { return width * height; }
}
class Triangle {
private final double base;
private final double height;
Triangle(double base, double height) { this.base = base; this.height = height; }
double area() { return 0.5 * base * height; }
}
Rectangle r = new Rectangle(5, 3);
System.out.println(r.area()); // 15// Procedural vs OOP — same problem, two styles
// Procedural: functions operating on raw data
double calculateArea(const std::string& shapeType, double width, double height) {
if (shapeType == "rectangle") return width * height;
if (shapeType == "triangle") return 0.5 * width * height;
return 0;
}
// OOP: objects that know how to describe themselves
class Rectangle {
double width_, height_;
public:
Rectangle(double w, double h) : width_(w), height_(h) {}
double area() const { return width_ * height_; }
};
class Triangle {
double base_, height_;
public:
Triangle(double b, double h) : base_(b), height_(h) {}
double area() const { return 0.5 * base_ * height_; }
};
int main() {
Rectangle r(5, 3);
std::cout << r.area(); // 15
} Classes and Objects
A class is a blueprint that defines the structure and behavior shared by all objects of that type. An object is a live instance of a class — allocated in memory, holding its own copy of the instance variables defined by the class.
Every class typically has: fields (instance variables holding state), a constructor (special method that initializes the object), methods (functions defining behavior), and optionally a destructor/finalizer (cleanup when the object is garbage collected or goes out of scope).
The this keyword (self in Python) refers to the current object instance. It distinguishes instance variables from local variables with the same name and allows methods to call other methods on the same object.
Object lifecycle: memory is allocated → the constructor runs → the object lives and is used → when no references remain, the garbage collector (or manual free in C++) reclaims the memory → the destructor/finalizer runs if defined.
Code example
class BankAccount:
# Class variable — shared by ALL instances
interest_rate = 0.04
# Constructor — runs when object is created
def __init__(self, owner, balance=0):
self.owner = owner # instance variable
self.balance = balance # instance variable
self._transactions = [] # private by convention
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self.balance += amount
self._transactions.append(("deposit", amount))
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
self._transactions.append(("withdraw", amount))
def __repr__(self):
return f"BankAccount({self.owner!r}, balance={self.balance})"
# Creating objects (instances)
acc1 = BankAccount("Alice", 1000)
acc2 = BankAccount("Bob")
acc1.deposit(500)
acc2.deposit(200)
print(acc1) # BankAccount('Alice', balance=1500)
print(BankAccount.interest_rate) # 0.04 — class variableclass BankAccount {
// Static field — shared by ALL instances
static double interestRate = 0.04;
private final String owner; // instance fields
private double balance;
private final List<String> transactions = new ArrayList<>();
// Constructor — runs when object is created
public BankAccount(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive");
balance += amount;
transactions.add("deposit: " + amount);
}
public void withdraw(double amount) {
if (amount > balance) throw new IllegalArgumentException("Insufficient funds");
balance -= amount;
transactions.add("withdraw: " + amount);
}
@Override
public String toString() {
return "BankAccount(" + owner + ", balance=" + balance + ")";
}
}
BankAccount acc1 = new BankAccount("Alice", 1000);
BankAccount acc2 = new BankAccount("Bob");
acc1.deposit(500);
acc2.deposit(200);
System.out.println(acc1); // BankAccount(Alice, balance=1500.0)
System.out.println(BankAccount.interestRate); // 0.04 — static field#include <iostream>
#include <string>
#include <vector>
class BankAccount {
// Static member — shared by ALL instances
static inline double interestRate = 0.04;
std::string owner_; // instance members
double balance_;
std::vector<std::string> transactions_;
public:
// Constructor — runs when object is created
BankAccount(std::string owner, double balance = 0)
: owner_(std::move(owner)), balance_(balance) {}
void deposit(double amount) {
if (amount <= 0) throw std::invalid_argument("Deposit must be positive");
balance_ += amount;
transactions_.push_back("deposit: " + std::to_string(amount));
}
void withdraw(double amount) {
if (amount > balance_) throw std::invalid_argument("Insufficient funds");
balance_ -= amount;
transactions_.push_back("withdraw: " + std::to_string(amount));
}
friend std::ostream& operator<<(std::ostream& os, const BankAccount& a) {
return os << "BankAccount(" << a.owner_ << ", balance=" << a.balance_ << ")";
}
};
int main() {
BankAccount acc1("Alice", 1000);
BankAccount acc2("Bob");
acc1.deposit(500);
acc2.deposit(200);
std::cout << acc1 << "
"; // BankAccount(Alice, balance=1500)
std::cout << BankAccount::interestRate; // 0.04 — static member
} Encapsulation
Encapsulation bundles data and the methods that operate on it into a single unit (the class), and controls access to that data from the outside. It protects an object's internal state from accidental corruption and hides implementation details.
Access modifiers control visibility. Public members are accessible from anywhere. Protected members (single underscore _ in Python, protected in Java/C++) are accessible within the class and its subclasses. Private members (double underscore __ in Python, private in Java/C++) are accessible only within the class.
Getters and setters (properties in Python) provide controlled access to private fields. They let you add validation, logging, or computed values without changing the public interface. If you expose a raw field and later need to add validation, you must change every caller — but if you exposed a getter, you only change the getter.
Information hiding (a related concept) means hiding not just data but also implementation choices. A Stack class exposes push() and pop() — callers don't need to know if it's backed by an array or a linked list.
Code example
class Temperature:
def __init__(self, celsius):
self._celsius = None # private storage
self.celsius = celsius # use the setter
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15: # validation in setter
raise ValueError("Below absolute zero")
self._celsius = value
@property
def fahrenheit(self): # computed property
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5/9 # delegates to celsius setter
t = Temperature(100)
print(t.fahrenheit) # 212.0
t.fahrenheit = 32
print(t.celsius) # 0.0
# t._celsius = -999 # possible but bad practice — bypass the guardclass Temperature {
private double celsius; // private storage
public double getCelsius() { return celsius; }
public void setCelsius(double value) { // validation in setter
if (value < -273.15) throw new IllegalArgumentException("Below absolute zero");
this.celsius = value;
}
public double getFahrenheit() { // computed getter
return celsius * 9.0 / 5 + 32;
}
public void setFahrenheit(double value) {
setCelsius((value - 32) * 5.0 / 9); // delegates to celsius setter
}
}
Temperature t = new Temperature();
t.setCelsius(100);
System.out.println(t.getFahrenheit()); // 212.0
t.setFahrenheit(32);
System.out.println(t.getCelsius()); // 0.0#include <iostream>
#include <stdexcept>
class Temperature {
double celsius_ = 0; // private storage
public:
void setCelsius(double value) { // validation in setter
if (value < -273.15) throw std::invalid_argument("Below absolute zero");
celsius_ = value;
}
double getCelsius() const { return celsius_; }
double getFahrenheit() const { // computed getter
return celsius_ * 9.0 / 5 + 32;
}
void setFahrenheit(double value) {
setCelsius((value - 32) * 5.0 / 9); // delegates to celsius setter
}
};
int main() {
Temperature t;
t.setCelsius(100);
std::cout << t.getFahrenheit() << "
"; // 212
t.setFahrenheit(32);
std::cout << t.getCelsius(); // 0
} Inheritance
Inheritance lets a new class (child/subclass) acquire the attributes and methods of an existing class (parent/superclass), then extend or specialize them. It models IS-A relationships: a Dog IS-A Animal, a SavingsAccount IS-A BankAccount.
Types of inheritance: Single (one parent), Multiple (two or more parents — supported in Python and C++, not Java), Multilevel (A → B → C), Hierarchical (one parent, many children), Hybrid (combination).
The super() function calls a parent class method. It's critical in constructors to ensure the parent is properly initialized before the child adds its own setup.
Method overriding: a child class redefines a parent method with the same name and signature. At runtime, Python/Java/C++ calls the most derived version (dynamic dispatch). To prevent a method from being overridden in Java, use final. In C++, use virtual in the parent to enable override.
The Liskov Substitution Principle (from SOLID) says a subclass should be usable wherever the parent is expected without breaking the program — inheritance should never surprise the caller.
Code example
class Animal:
def __init__(self, name, sound):
self.name = name
self.sound = sound
def speak(self):
return f"{self.name} says {self.sound}"
def __str__(self):
return self.name
class Dog(Animal): # single inheritance
def __init__(self, name):
super().__init__(name, "Woof") # call parent constructor
self.tricks = []
def learn_trick(self, trick):
self.tricks.append(trick)
def speak(self): # override parent method
base = super().speak() # reuse parent logic
return base + "!"
class GuideDog(Dog): # multilevel inheritance
def __init__(self, name, owner):
super().__init__(name)
self.owner = owner
def guide(self):
return f"{self.name} guides {self.owner}"
d = Dog("Rex")
d.learn_trick("sit")
print(d.speak()) # Rex says Woof!
print(d.tricks) # ['sit']
g = GuideDog("Buddy", "Alice")
print(g.guide()) # Buddy guides Alice
print(g.speak()) # Buddy says Woof! — inherited from Dog// Inheritance — IS-A relationship
class Animal {
protected String name;
protected String sound;
Animal(String name, String sound) {
this.name = name;
this.sound = sound;
}
public String speak() {
return name + " says " + sound;
}
@Override
public String toString() {
return name;
}
}
class Dog extends Animal { // single inheritance
private final List<String> tricks = new ArrayList<>();
Dog(String name) {
super(name, "Woof"); // call parent constructor
}
public void learnTrick(String trick) {
tricks.add(trick);
}
@Override
public String speak() { // override parent method
return super.speak() + "!"; // reuse parent logic
}
}
class GuideDog extends Dog { // multilevel inheritance
private final String owner;
GuideDog(String name, String owner) {
super(name);
this.owner = owner;
}
public String guide() {
return name + " guides " + owner;
}
}
Dog d = new Dog("Rex");
d.learnTrick("sit");
System.out.println(d.speak()); // Rex says Woof!
GuideDog g = new GuideDog("Buddy", "Alice");
System.out.println(g.guide()); // Buddy guides Alice
System.out.println(g.speak()); // Buddy says Woof! — inherited from Dog// Inheritance — IS-A relationship
class Animal {
protected:
std::string name;
std::string sound;
public:
Animal(std::string name, std::string sound)
: name(std::move(name)), sound(std::move(sound)) {}
virtual std::string speak() const { // virtual = can be overridden
return name + " says " + sound;
}
};
class Dog : public Animal { // single inheritance
std::vector<std::string> tricks;
public:
Dog(const std::string& name)
: Animal(name, "Woof") {} // call parent constructor
void learnTrick(const std::string& trick) {
tricks.push_back(trick);
}
std::string speak() const override { // override parent method
return Animal::speak() + "!"; // reuse parent logic
}
};
class GuideDog : public Dog { // multilevel inheritance
std::string owner;
public:
GuideDog(const std::string& name, const std::string& owner)
: Dog(name), owner(owner) {}
std::string guide() const {
return name + " guides " + owner;
}
};
int main() {
Dog d("Rex");
d.learnTrick("sit");
std::cout << d.speak() << "
"; // Rex says Woof!
GuideDog g("Buddy", "Alice");
std::cout << g.guide() << "
"; // Buddy guides Alice
std::cout << g.speak() << "
"; // Buddy says Woof! — inherited from Dog
} Polymorphism
Polymorphism means "many forms" — the same interface produces different behavior depending on the actual object type. It's what lets you write generic code that works with objects you haven't seen yet.
Compile-time polymorphism (static dispatch) is method overloading: same method name, different parameter lists. The compiler picks the right version based on argument types at compile time. Python doesn't support this natively (no type signatures), but Java and C++ do.
Runtime polymorphism (dynamic dispatch) is method overriding via inheritance. The correct method is determined at runtime based on the actual object type, not the declared variable type. This is the backbone of every design pattern that uses a common interface.
Duck typing (Python's approach): if an object has the method I call, it works — we don't check the type. This enables structural polymorphism without formal inheritance.
Virtual functions in C++: by default, C++ uses static dispatch. You must declare a method virtual in the base class to get dynamic dispatch. Python and Java always use dynamic dispatch for instance methods.
Code example
# Runtime polymorphism — same call, different behavior
class Shape:
def area(self):
raise NotImplementedError("Subclass must implement area()")
def describe(self):
return f"I am a {type(self).__name__} with area {self.area():.2f}"
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.h
class Triangle(Shape):
def __init__(self, base, height):
self.base, self.height = base, height
def area(self):
return 0.5 * self.base * self.height
# Generic function — works for ANY Shape subclass
def print_all_areas(shapes):
for shape in shapes:
print(shape.describe()) # dynamic dispatch picks right area()
shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 8)]
print_all_areas(shapes)
# I am a Circle with area 78.54
# I am a Rectangle with area 24.00
# I am a Triangle with area 12.00// Runtime polymorphism — same call, different behavior
abstract class Shape {
public abstract double area();
public String describe() {
return String.format("I am a %s with area %.2f",
getClass().getSimpleName(), area());
}
}
class Circle extends Shape {
private final double radius;
Circle(double r) { radius = r; }
public double area() { return 3.14159 * radius * radius; }
}
class Rectangle extends Shape {
private final double w, h;
Rectangle(double w, double h) { this.w = w; this.h = h; }
public double area() { return w * h; }
}
class Triangle extends Shape {
private final double base, height;
Triangle(double b, double h) { base = b; height = h; }
public double area() { return 0.5 * base * height; }
}
// Generic method — works for ANY Shape subclass
void printAllAreas(List<? extends Shape> shapes) {
for (Shape shape : shapes)
System.out.println(shape.describe()); // dynamic dispatch picks area()
}
List<Shape> shapes = List.of(new Circle(5), new Rectangle(4, 6), new Triangle(3, 8));
printAllAreas(shapes);
// I am a Circle with area 78.54
// I am a Rectangle with area 24.00
// I am a Triangle with area 12.00// Runtime polymorphism — same call, different behavior
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0; // pure virtual
std::string describe() const {
std::ostringstream os;
os << "I am a " << typeid(*this).name() << " with area "
<< std::fixed << std::setprecision(2) << area();
return os.str();
}
};
class Circle : public Shape {
double radius;
public:
explicit Circle(double r) : radius(r) {}
double area() const override { return 3.14159 * radius * radius; }
};
class Rectangle : public Shape {
double w, h;
public:
Rectangle(double w, double h) : w(w), h(h) {}
double area() const override { return w * h; }
};
class Triangle : public Shape {
double base, height;
public:
Triangle(double b, double h) : base(b), height(h) {}
double area() const override { return 0.5 * base * height; }
};
// Generic function — works for ANY Shape subclass
void printAllAreas(const std::vector<std::unique_ptr<Shape>>& shapes) {
for (const auto& shape : shapes)
std::cout << shape->describe() << "
"; // virtual dispatch picks area()
}
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5));
shapes.push_back(std::make_unique<Rectangle>(4, 6));
shapes.push_back(std::make_unique<Triangle>(3, 8));
printAllAreas(shapes);
} Abstraction
Abstraction means hiding complexity behind a simple interface. You expose what the caller needs and hide how it works. The caller of .sort() doesn't need to know whether it's Timsort or quicksort — they just call sort().
Abstract classes define a partial implementation and declare abstract methods that subclasses must implement. You cannot instantiate an abstract class directly. In Python, use the abc module. In Java, use the abstract keyword.
Interfaces (Java/C#) define a contract — a set of method signatures — with no implementation. A class can implement multiple interfaces. Interfaces enable polymorphism across unrelated class hierarchies.
When to use abstract class vs interface: Use an abstract class when subclasses share common code or state. Use an interface when you want to define a capability that unrelated classes can implement (Serializable, Comparable).
HAS-A vs IS-A: Inheritance models IS-A (Dog IS-A Animal). Composition models HAS-A (Car HAS-A Engine). Prefer HAS-A when the relationship isn't truly hierarchical.
Code example
from abc import ABC, abstractmethod
# Abstract class — defines contract + some shared logic
class DataExporter(ABC):
def __init__(self, filename):
self.filename = filename
@abstractmethod
def serialize(self, data):
"""Convert data to the target format — must implement."""
pass
def export(self, data): # template method — shared logic
content = self.serialize(data)
with open(self.filename, 'w') as f:
f.write(content)
print(f"Exported to {self.filename}")
class JSONExporter(DataExporter):
def serialize(self, data):
import json
return json.dumps(data, indent=2)
class CSVExporter(DataExporter):
def serialize(self, data):
lines = [",".join(str(v) for v in row) for row in data]
return "
".join(lines)
# exporter = DataExporter("x.txt") # TypeError: can't instantiate abstract class
j = JSONExporter("out.json")
c = CSVExporter("out.csv")
# Both use the same export() but different serialize()// Abstract class — defines contract + some shared logic
abstract class DataExporter {
protected final String filename;
DataExporter(String filename) { this.filename = filename; }
// Must be implemented by subclasses
public abstract String serialize(String data);
// Template method — shared logic reused by every subclass
public void export(String data) {
String content = serialize(data);
try (java.io.PrintWriter out = new java.io.PrintWriter(filename)) {
out.print(content);
}
System.out.println("Exported to " + filename);
}
}
class JSONExporter extends DataExporter {
JSONExporter(String filename) { super(filename); }
public String serialize(String data) {
return "{"data": "" + data + ""}";
}
}
class CSVExporter extends DataExporter {
CSVExporter(String filename) { super(filename); }
public String serialize(String data) {
return data.replace(",", ";").replace(" ", ",");
}
}
// DataExporter e = new DataExporter("x.txt"); // compile error: abstract
JSONExporter j = new JSONExporter("out.json");
CSVExporter c = new CSVExporter("out.csv");
// Both use the same export() but different serialize()// Abstract class — defines contract + some shared logic
class DataExporter {
protected:
std::string filename;
public:
explicit DataExporter(std::string fname) : filename(std::move(fname)) {}
virtual ~DataExporter() = default;
// Pure virtual — must be implemented by subclasses
virtual std::string serialize(const std::string& data) = 0;
// Template method — shared logic reused by every subclass
void exportData(const std::string& data) {
std::string content = serialize(data);
std::ofstream out(filename);
out << content;
std::cout << "Exported to " << filename << "
";
}
};
class JSONExporter : public DataExporter {
public:
using DataExporter::DataExporter;
std::string serialize(const std::string& data) override {
return "{"data": "" + data + ""}";
}
};
class CSVExporter : public DataExporter {
public:
using DataExporter::DataExporter;
std::string serialize(const std::string& data) override {
std::string out;
for (char ch : data) out += (ch == ' ') ? ',' : ch;
return out;
}
};
// DataExporter e("x.txt"); // compile error: abstract
int main() {
JSONExporter j("out.json");
CSVExporter c("out.csv");
// Both use the same exportData() but different serialize()
} Composition vs Inheritance
"Favour object composition over class inheritance" — this quote from the Gang of Four Design Patterns book is one of the most important principles in OOP. It's not that inheritance is bad; it's that composition is more flexible in most cases.
Problems with deep inheritance hierarchies: the fragile base class problem (changing a parent breaks children), tight coupling (subclasses depend on parent internals), the gorilla-banana problem ("you wanted a banana but you got a gorilla holding the banana and the entire jungle"). Multiple inheritance adds the diamond problem.
Composition models HAS-A: a Car HAS-A Engine. The Car class holds a reference to an Engine object and delegates engine-related behavior to it. You can swap engines at runtime. You can use different Engine types without changing Car. You can test Car and Engine independently.
When inheritance IS right: a true IS-A relationship where the subclass really is a specialization, the parent hierarchy is stable and shallow, you need to reuse both interface and implementation.
Mixin classes (Python): thin classes that add one capability. Combine them via multiple inheritance for fine-grained composition. LoggingMixin adds logging, SerializableMixin adds JSON export — drop them into any class that needs them.
Code example
# Composition — Engine is a component, not a parent
class Engine:
def __init__(self, horsepower, fuel_type):
self.horsepower = horsepower
self.fuel = fuel_type
def start(self):
return f"{self.fuel} engine starting ({self.horsepower}hp)"
class ElectricMotor:
def __init__(self, kw):
self.kw = kw
def start(self):
return f"Electric motor spinning up ({self.kw}kW)"
class Car:
def __init__(self, model, engine): # inject the engine
self.model = model
self._engine = engine # HAS-A relationship
def start(self):
return f"{self.model}: {self._engine.start()}"
# Swap the powertrain at construction time — no subclassing needed
gas_car = Car("Sedan", Engine(200, "petrol"))
elec_car = Car("Model S", ElectricMotor(450))
print(gas_car.start()) # Sedan: petrol engine starting (200hp)
print(elec_car.start()) # Model S: Electric motor spinning up (450kW)
# Both share the Car interface — no inheritance required// Composition — Engine is a component, not a parent
interface PowerSource {
String start();
}
class Engine implements PowerSource {
private final int horsepower;
private final String fuelType;
Engine(int hp, String fuel) { horsepower = hp; fuelType = fuel; }
public String start() {
return fuelType + " engine starting (" + horsepower + "hp)";
}
}
class ElectricMotor implements PowerSource {
private final int kw;
ElectricMotor(int kw) { this.kw = kw; }
public String start() {
return "Electric motor spinning up (" + kw + "kW)";
}
}
class Car {
private final String model;
private final PowerSource engine; // inject the engine (HAS-A)
Car(String model, PowerSource engine) {
this.model = model;
this.engine = engine;
}
public String start() {
return model + ": " + engine.start();
}
}
// Swap the powertrain at construction time — no subclassing needed
Car gasCar = new Car("Sedan", new Engine(200, "petrol"));
Car elecCar = new Car("Model S", new ElectricMotor(450));
System.out.println(gasCar.start()); // Sedan: petrol engine starting (200hp)
System.out.println(elecCar.start()); // Model S: Electric motor spinning up (450kW)
// Both share the Car interface — no inheritance required// Composition — Engine is a component, not a parent
class Engine {
int horsepower;
std::string fuelType;
public:
Engine(int hp, std::string fuel) : horsepower(hp), fuelType(std::move(fuel)) {}
std::string start() const {
return fuelType + " engine starting (" + std::to_string(horsepower) + "hp)";
}
};
class ElectricMotor {
int kw;
public:
explicit ElectricMotor(int kw) : kw(kw) {}
std::string start() const {
return "Electric motor spinning up (" + std::to_string(kw) + "kW)";
}
};
// Variant = inject ANY engine type without inheritance
using PowerSource = std::variant<Engine, ElectricMotor>;
class Car {
std::string model;
PowerSource engine; // inject the engine (HAS-A)
public:
Car(std::string model, PowerSource engine)
: model(std::move(model)), engine(std::move(engine)) {}
std::string start() const {
return model + ": " + std::visit([](const auto& e) { return e.start(); }, engine);
}
};
int main() {
// Swap the powertrain at construction time — no subclassing needed
Car gasCar("Sedan", Engine(200, "petrol"));
Car elecCar("Model S", ElectricMotor(450));
std::cout << gasCar.start() << "
"; // Sedan: petrol engine starting (200hp)
std::cout << elecCar.start() << "
"; // Model S: Electric motor spinning up (450kW)
} Object Relationships
OOP objects relate to each other in three ways, each with different ownership semantics.
Association is the loosest relationship: objects know about each other but neither owns the other. A Student associates with a Course — the Student can switch courses, and the Course exists independently. Both can exist without each other.
Aggregation is a "whole-part" relationship where the part can exist independently. A Department HAS-A list of Employees. If the Department dissolves, the Employees still exist. The lifecycle of the part is independent of the whole.
Composition is the strongest "whole-part" relationship — the part cannot exist without the whole. A House HAS-A Room. Destroy the House, the Rooms are destroyed too. In code, the whole creates and owns the part directly.
UML notation: association → plain arrow. Aggregation → hollow diamond at the "whole" end. Composition → filled diamond at the "whole" end. Multiplicity is written as 1, *, 0..1, 1..* at each end.
Code example
# Association: Teacher and Course — independent lifecycles
class Teacher:
def __init__(self, name):
self.name = name
class Course:
def __init__(self, title, teacher: Teacher):
self.title = title
self.teacher = teacher # reference, not ownership
# Aggregation: Department has Employees, but Employees exist independently
class Employee:
def __init__(self, name):
self.name = name
class Department:
def __init__(self, name):
self.name = name
self.employees = [] # aggregation — Employee created outside
def add_employee(self, emp: Employee):
self.employees.append(emp)
# Composition: House creates and owns its Rooms
class Room:
def __init__(self, name, area):
self.name = name
self.area = area
class House:
def __init__(self, address, room_specs):
self.address = address
# House creates its own rooms — composition
self.rooms = [Room(name, area) for name, area in room_specs]
def total_area(self):
return sum(r.area for r in self.rooms)
h = House("123 Main St", [("Living Room", 25), ("Bedroom", 15)])
print(h.total_area()) # 40// Association: Teacher and Course — independent lifecycles
class Teacher {
private final String name;
Teacher(String name) { this.name = name; }
}
class Course {
private final String title;
private final Teacher teacher; // reference, not ownership
Course(String title, Teacher teacher) {
this.title = title;
this.teacher = teacher;
}
}
// Aggregation: Department has Employees, but Employees exist independently
class Employee {
private final String name;
Employee(String name) { this.name = name; }
}
class Department {
private final String name;
private final List<Employee> employees = new ArrayList<>(); // created outside
Department(String name) { this.name = name; }
void addEmployee(Employee emp) { employees.add(emp); }
}
// Composition: House creates and owns its Rooms
class Room {
private final String name;
private final double area;
Room(String name, double area) { this.name = name; this.area = area; }
}
class House {
private final String address;
private final List<Room> rooms = new ArrayList<>(); // House creates its own
House(String address, String[][] specs) {
this.address = address;
for (String[] spec : specs)
rooms.add(new Room(spec[0], Double.parseDouble(spec[1])));
}
double totalArea() {
double sum = 0;
for (Room room : rooms) sum += room.area;
return sum;
}
}
House h = new House("123 Main St", new String[][]{{"Living Room", "25"}, {"Bedroom", "15"}});
System.out.println(h.totalArea()); // 40.0// Association: Teacher and Course — independent lifecycles
class Teacher {
std::string name;
public:
explicit Teacher(std::string name) : name(std::move(name)) {}
};
class Course {
std::string title;
Teacher* teacher; // reference, not ownership
public:
Course(std::string title, Teacher* teacher)
: title(std::move(title)), teacher(teacher) {}
};
// Aggregation: Department has Employees, but Employees exist independently
class Employee {
std::string name;
public:
explicit Employee(std::string name) : name(std::move(name)) {}
};
class Department {
std::string name;
std::vector<Employee*> employees; // created outside
public:
explicit Department(std::string name) : name(std::move(name)) {}
void addEmployee(Employee* emp) { employees.push_back(emp); }
};
// Composition: House creates and owns its Rooms
class Room {
public:
std::string name;
double area;
Room(std::string name, double area) : name(std::move(name)), area(area) {}
};
class House {
std::string address;
std::vector<Room> rooms; // House owns its Rooms
public:
House(std::string address, std::vector<Room> rooms)
: address(std::move(address)), rooms(std::move(rooms)) {}
double totalArea() const {
double sum = 0;
for (const auto& room : rooms) sum += room.area;
return sum;
}
};
int main() {
House h("123 Main St", {{Room("Living Room", 25), Room("Bedroom", 15)}});
std::cout << h.totalArea() << "
"; // 40
} OOP in Different Languages
Java enforces strict OOP: everything (except primitives) is an object inside a class. Single inheritance only — use interfaces for multiple type contracts. All methods are virtual by default (use final to prevent overriding). Garbage collected.
Python is multi-paradigm: OOP is available but not mandatory. Multiple inheritance supported with MRO (Method Resolution Order using C3 linearization). No true private members — underscore is a convention. Dynamic typing means duck typing works naturally. Everything is an object, including functions and classes themselves.
C++ gives you the most control: manual memory management (or smart pointers), true multiple inheritance, explicit virtual/non-virtual dispatch, and operator overloading. More power = more responsibility.
Key differences table: Java — single inheritance, interfaces, always virtual, GC. Python — multiple inheritance, duck typing, convention-based privacy, GC. C++ — multiple inheritance, explicit virtual, manual memory, templates for generic programming.
Code example
# Python multiple inheritance with MRO
class Flyable:
def move(self):
return "flying"
class Swimmable:
def move(self):
return "swimming"
class Duck(Flyable, Swimmable): # multiple inheritance
def move(self):
# MRO: Duck → Flyable → Swimmable
return f"Duck can do both: {Flyable.move(self)} and {Swimmable.move(self)}"
print(Duck.__mro__) # shows resolution order
d = Duck()
print(d.move()) # Duck can do both: flying and swimming
# Java equivalent would be:
# interface Flyable { String move(); }
# interface Swimmable { String move(); }
# class Duck implements Flyable, Swimmable {
# public String move() { return "Duck moves"; }
# }
# — Java cannot call two conflicting interface methods directly
# Python's super() chain and explicit class calls give more control// Java — single inheritance + interfaces for multiple contracts
interface Flyable {
default String move() { return "flying"; }
}
interface Swimmable {
default String move() { return "swimming"; }
}
class Duck implements Flyable, Swimmable {
// Two interfaces define move() — must disambiguate explicitly
@Override
public String move() {
return "Duck can do both: " + Flyable.super.move() + " and " + Swimmable.super.move();
}
}
// class Duck extends Animal, Bird { } // compile error: no multiple class inheritance
Duck d = new Duck();
System.out.println(d.move()); // Duck can do both: flying and swimming// C++ — true multiple inheritance with the diamond problem
class Flyable {
public:
virtual std::string move() { return "flying"; }
};
class Swimmable {
public:
virtual std::string move() { return "swimming"; }
};
// Duck inherits two move() implementations
class Duck : public Flyable, public Swimmable {
public:
std::string move() override {
// No ambiguity — Duck's own override wins
return "Duck can do both: " + Flyable::move() + " and " + Swimmable::move();
}
};
// The diamond problem: if both Flyable and Swimmable derived from a common
// base (e.g. LivingThing), C++ needs virtual inheritance to avoid two copies:
// class Duck : public virtual Flyable, public virtual Swimmable { };
Duck d;
std::cout << d.move() << "
"; // Duck can do both: flying and swimming Common OOP Interview Questions
These are the questions that come up most often in OOP rounds. For each, know the definition, a code example, and a real-world analogy.
What are the four pillars of OOP? Encapsulation (data hiding + bundling), Inheritance (IS-A reuse), Polymorphism (one interface, many forms), Abstraction (hide complexity, expose interface).
What is the difference between abstract class and interface? Abstract class has partial implementation and state; interface has only method signatures (pre-Java 8). Use abstract class for shared code, interface for type contracts across unrelated hierarchies.
Can we override static methods? No — static methods are bound to the class, not the instance. You can hide a static method in a subclass (method hiding), but dynamic dispatch does not apply.
What is method hiding vs method overriding? Overriding affects runtime behavior via dynamic dispatch (instance methods). Hiding replaces a static method in the subclass — calling via superclass reference uses the parent version.
What is the diamond problem? In multiple inheritance, if two parents define the same method and a child inherits from both, which version runs? Python solves this with C3 MRO. Java avoids it by disallowing multiple class inheritance (interfaces are fine).
What is object cloning? Shallow copy duplicates the object but shares references to nested objects. Deep copy recursively duplicates everything. Python: copy.copy() vs copy.deepcopy(). Java: implement Cloneable and override clone().