Skip to main content
Software Engineering Fundamentals

OOP Interview Questions: Object-Oriented Programming Guide

A complete guide to OOP from first principles — classes, the four pillars, composition vs inheritance, object relationships, and how OOP differs across Java, Python, and C++.

10 sections · 25 min read

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
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

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 {
    // 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

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 {
    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

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

// 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

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
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

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

// 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()

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
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

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 {
    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

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

// 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

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().

⚡ CS Fundamentals Resource Vault 2026

Object-Oriented Programming Skills Matrix & Interview Guide

Curated directly from real-world hiring loops across Tier-1 Big Tech, high-growth startups, and tech services. Master what interviewers test, compare architectural trade-offs side-by-side, and rehearse answers to the highest-frequency questions.

19
Core Topics
10
Key Differences
14
Top Questions
Shows why each topic matters and the exact questions interviewers use to test it.
Area Topic Level Why It Matters How It Shows Up in Hiring
Concepts The four pillars: encapsulation, abstraction, inheritance, polymorphism Foundation The most asked OOP question in existence. 'Explain the four pillars with examples from your own code.'
Concepts Class versus object Foundation The opening definitional question. 'Difference between a class and an object?'
Concepts Encapsulation and access modifiers Foundation Data hiding in practice. 'Why make fields private and expose getters?'
Concepts Abstraction versus encapsulation Core A distinction most candidates blur. 'These sound the same. What is the actual difference?'
Inheritance Types of inheritance and the diamond problem Core Why Java forbids multiple class inheritance. 'Why does Java not allow multiple inheritance?'
Inheritance Method overriding and dynamic dispatch Foundation Runtime polymorphism in practice. 'How does the JVM decide which method to call?'
Inheritance Composition over inheritance Core The design judgement question. 'When would you prefer composition? Usually.'
Polymorphism Compile-time versus runtime polymorphism Foundation Overloading versus overriding. 'Difference between overloading and overriding?'
Polymorphism Method overloading rules Core Signature rules and common traps. 'Can you overload by return type alone?' No.
Abstraction Abstract classes versus interfaces Foundation Asked in essentially every OOP round. 'When would you use each?' Have a real example ready.
Abstraction Interface default methods and modern changes Core How the answer changed in Java 8 and later. 'Can an interface have implementation now?'
Design SOLID principles Core The framing for every code review discussion. 'Which SOLID principle does this code violate?'
Design Design patterns: singleton, factory, observer, strategy Core The four asked most often by name. 'Implement a thread-safe singleton.'
Design Coupling and cohesion Core The vocabulary of good design. 'What is high cohesion and why do you want it?'
Design Association, aggregation and composition Core The three relationship types in UML. 'Difference between aggregation and composition?'
Practical Constructors, destructors and object lifecycle Foundation Language-specific but universally asked. 'What is a copy constructor? When is it called?'
Practical static, final and this Foundation Keyword semantics. 'Can a static method be overridden?' No, it is hidden.
Practical equals(), hashCode() and toString() Core The contract that breaks collections if ignored. 'What happens if you override equals but not hashCode?'
Practical Exception handling and custom exceptions Core Error design in object terms. 'Checked versus unchecked exceptions?'
Continue Learning

Ready to test your knowledge?

Apply what you learned with curated practice problems.

Find this useful?

This guide is completely free. If it helped, consider buying me a coffee — it keeps new content coming.

Support on Ko-fi
Buy me a coffee
Daily Practice
0/0