Loading W Code...
Most frequently asked Object-Oriented Programming questions in tech interviews.
The four pillars of OOP are:
Encapsulation: Bundling data and methods that operate on that data within a single unit (class), and restricting direct access to some components.
Abstraction: Hiding complex implementation details and showing only the necessary features of an object.
Inheritance: A mechanism where a new class inherits properties and behaviors from an existing class.
Polymorphism: The ability of objects to take on many forms. Same method name can behave differently based on the object.
Abstract Class:
Interface:
When to use:
Method Overloading (Compile-time Polymorphism):
class Calculator {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
int add(int a, int b, int c) { return a + b + c; }
}
Method Overriding (Runtime Polymorphism):
class Animal {
void sound() { System.out.println("Some sound"); }
}
class Dog extends Animal {
@Override
void sound() { System.out.println("Bark!"); }
}
SOLID is an acronym for 5 design principles:
S - Single Responsibility Principle A class should have only one reason to change (one job).
O - Open/Closed Principle Classes should be open for extension but closed for modification.
L - Liskov Substitution Principle Derived classes must be substitutable for their base classes.
I - Interface Segregation Principle Many specific interfaces are better than one general interface.
D - Dependency Inversion Principle Depend on abstractions, not concrete implementations.
These principles help create maintainable, scalable, and testable code.
Inheritance (IS-A relationship):
class Dog extends Animal { } // Dog IS-A Animal
Composition (HAS-A relationship):
class Car {
private Engine engine; // Car HAS-A Engine
private Wheel[] wheels;
}
Rule of thumb: "Favor composition over inheritance" - Gang of Four
Constructor: A special method called when an object is created. Used to initialize object state.
Types:
class Car {
Car() { } // No parameters
}
class Car {
String brand;
Car(String brand) {
this.brand = brand;
}
}
class Car {
String brand;
Car(Car other) {
this.brand = other.brand;
}
}
Key Points:
Java Access Modifiers:
| Modifier | Class | Package | Subclass | World |
|---|---|---|---|---|
| public | ✅ | ✅ | ✅ | ✅ |
| protected | ✅ | ✅ | ✅ | ❌ |
| default | ✅ | ✅ | ❌ | ❌ |
| private | ✅ | ❌ | ❌ | ❌ |
C++ Access Modifiers:
class Example {
public:
int publicVar;
protected:
int protectedVar;
private:
int privateVar;
};
Virtual Function (C++):
A function declared in base class with virtual keyword that can be overridden in derived class. Enables runtime polymorphism.
class Animal {
public:
virtual void sound() {
cout << "Some sound";
}
};
class Dog : public Animal {
public:
void sound() override {
cout << "Bark!";
}
};
Animal* a = new Dog();
a->sound(); // Output: Bark! (dynamic binding)
Pure Virtual Function: A virtual function with no implementation (= 0). Makes the class abstract.
class Shape {
public:
virtual double area() = 0; // Pure virtual
};
class Circle : public Shape {
public:
double area() override {
return 3.14 * r * r;
}
};
Key Points:
Diamond Problem: An ambiguity that arises when a class inherits from two classes that have a common base class.
A
/ \
B C
\ /
D
Class D inherits from both B and C, which both inherit from A. This creates ambiguity about which version of A's members D should use.
Solutions:
C++ - Virtual Inheritance:
class A { };
class B : virtual public A { };
class C : virtual public A { };
class D : public B, public C { }; // Only one copy of A
Java - No multiple inheritance of classes: Java avoids this by allowing only single class inheritance, but permits multiple interface implementation.
Python - MRO (Method Resolution Order):
class D(B, C):
pass
# Uses C3 linearization algorithm
print(D.__mro__)
this keyword:
class Car {
String brand;
Car(String brand) {
this.brand = brand; // this.brand = instance variable
}
void display() {
System.out.println(this.brand);
}
}
super keyword:
class Vehicle {
String type = "Vehicle";
void start() { System.out.println("Vehicle starting"); }
}
class Car extends Vehicle {
String type = "Car";
void display() {
System.out.println(super.type); // "Vehicle"
System.out.println(this.type); // "Car"
super.start(); // Calls parent method
}
}
Shallow Copy:
import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
shallow[0][0] = 99
print(original) # [[99, 2], [3, 4]] - Changed!
Deep Copy:
import copy
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0][0] = 99
print(original) # [[1, 2], [3, 4]] - Unchanged!
Key Point: Deep copy for complete independence, shallow copy for performance.
Singleton: Ensures a class has only one instance and provides global access to it.
Use Cases:
public class Singleton {
private static Singleton instance;
private Singleton() {} // Private constructor
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Thread-Safe Version:
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Factory Pattern: Creates objects without specifying exact class. Delegates creation to subclasses.
Benefits:
// Product interface
interface Shape {
void draw();
}
// Concrete products
class Circle implements Shape {
public void draw() { System.out.println("Drawing Circle"); }
}
class Square implements Shape {
public void draw() { System.out.println("Drawing Square"); }
}
// Factory
class ShapeFactory {
public Shape createShape(String type) {
if (type.equals("circle")) return new Circle();
if (type.equals("square")) return new Square();
return null;
}
}
// Usage
ShapeFactory factory = new ShapeFactory();
Shape shape = factory.createShape("circle");
shape.draw();
Static Binding (Early Binding):
Dynamic Binding (Late Binding):
class Animal {
static void staticMethod() { System.out.println("Animal static"); }
void instanceMethod() { System.out.println("Animal instance"); }
}
class Dog extends Animal {
static void staticMethod() { System.out.println("Dog static"); }
void instanceMethod() { System.out.println("Dog instance"); }
}
Animal a = new Dog();
a.staticMethod(); // "Animal static" - Static binding
a.instanceMethod(); // "Dog instance" - Dynamic binding
Cohesion: How closely related the responsibilities of a module are.
Coupling: How dependent modules are on each other.
Goal: High Cohesion + Low Coupling
Example of BAD design:
class UserService {
void registerUser() { }
void sendEmail() { } // Low cohesion
void generateReport() { } // Not related!
}
Example of GOOD design:
class UserService {
void registerUser() { }
void updateUser() { }
void deleteUser() { } // High cohesion
}
class EmailService {
void sendEmail() { } // Separate concern
}
Observer Pattern: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified automatically.
Use Cases:
// Subject interface
interface Subject {
void attach(Observer o);
void detach(Observer o);
void notifyObservers();
}
// Observer interface
interface Observer {
void update(String message);
}
// Concrete Subject
class NewsChannel implements Subject {
private List<Observer> observers = new ArrayList<>();
private String news;
public void attach(Observer o) { observers.add(o); }
public void detach(Observer o) { observers.remove(o); }
public void notifyObservers() {
for (Observer o : observers) {
o.update(news);
}
}
public void setNews(String news) {
this.news = news;
notifyObservers();
}
}
// Concrete Observer
class Subscriber implements Observer {
private String name;
public Subscriber(String name) { this.name = name; }
public void update(String message) {
System.out.println(name + " received: " + message);
}
}
// Usage
NewsChannel channel = new NewsChannel();
channel.attach(new Subscriber("Alice"));
channel.attach(new Subscriber("Bob"));
channel.setNews("Breaking News!"); // Both get notified
Key Points:
Strategy Pattern: Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Lets the algorithm vary independently from clients.
Use Cases:
// Strategy interface
interface PaymentStrategy {
void pay(int amount);
}
// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
public void pay(int amount) {
System.out.println("Paid " + amount + " using Credit Card: " + cardNumber);
}
}
class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
public void pay(int amount) {
System.out.println("Paid " + amount + " using PayPal: " + email);
}
}
class UPIPayment implements PaymentStrategy {
private String upiId;
public UPIPayment(String upiId) {
this.upiId = upiId;
}
public void pay(int amount) {
System.out.println("Paid " + amount + " using UPI: " + upiId);
}
}
// Context
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
// Usage - can switch strategies at runtime
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new UPIPayment("user@upi"));
cart.checkout(500);
cart.setPaymentStrategy(new CreditCardPayment("1234-5678"));
cart.checkout(1000);
Why use Strategy?
Decorator Pattern: Attaches additional responsibilities to an object dynamically. Provides a flexible alternative to subclassing for extending functionality.
Real-world example: Java I/O Streams
// Without understanding decorator, this looks confusing:
BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("file.txt")
)
);
// FileInputStream -> wrapped by InputStreamReader -> wrapped by BufferedReader
// Each wrapper adds functionality!
Custom Example - Coffee Shop:
// Component interface
interface Coffee {
String getDescription();
double getCost();
}
// Concrete component
class SimpleCoffee implements Coffee {
public String getDescription() { return "Simple Coffee"; }
public double getCost() { return 50; }
}
// Decorator base class
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
}
// Concrete decorators
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) { super(coffee); }
public String getDescription() {
return coffee.getDescription() + " + Milk";
}
public double getCost() {
return coffee.getCost() + 20;
}
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) { super(coffee); }
public String getDescription() {
return coffee.getDescription() + " + Sugar";
}
public double getCost() {
return coffee.getCost() + 5;
}
}
// Usage - stack decorators!
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription()); // Simple Coffee + Milk + Sugar
System.out.println(coffee.getCost()); // 75
Key Points:
These describe relationships between classes:
A general relationship where objects are aware of each other.
class Teacher {
void teach(Student student) {
// Teacher uses Student
}
}
A special form of association. Child can exist independently of parent.
class Department {
private List<Professor> professors; // Professors can exist without Department
public Department(List<Professor> professors) {
this.professors = professors;
}
}
// If Department is deleted, Professors still exist
A strong form of aggregation. Child cannot exist without parent.
class House {
private List<Room> rooms;
public House() {
// Rooms are created inside House
this.rooms = new ArrayList<>();
this.rooms.add(new Room("Bedroom"));
this.rooms.add(new Room("Kitchen"));
}
}
// If House is destroyed, Rooms are destroyed too
| Type | Relationship | Lifecycle | Example |
|---|---|---|---|
| Association | Uses-A | Independent | Teacher-Student |
| Aggregation | Has-A (weak) | Independent | Department-Professor |
| Composition | Has-A (strong) | Dependent | House-Room |
Interview Tip: Composition = "part-of", Aggregation = "has-a"
Decision Framework:
abstract class Animal {
protected String name; // Shared state
public void sleep() { // Shared implementation
System.out.println(name + " is sleeping");
}
abstract void makeSound(); // Must implement
}
interface Flyable {
void fly(); // Just the contract
}
// Unrelated classes can implement
class Bird implements Flyable { ... }
class Airplane implements Flyable { ... }
class Superman implements Flyable { ... }
Need shared code?
├── YES → Abstract Class
└── NO → Interface
Need multiple inheritance?
├── YES → Interface
└── NO → Either works
Is it "IS-A" or "CAN-DO"?
├── IS-A (Dog IS-A Animal) → Abstract Class
└── CAN-DO (Bird CAN-DO fly) → Interface
Interfaces can now have:
default methods (with implementation)static methodsThis blurs the line, but abstract classes still win for shared state.
Builder Pattern: Separates the construction of a complex object from its representation. Useful when an object has many optional parameters.
Problem it solves:
// Without Builder - Telescoping constructor anti-pattern
User user = new User("John", "Doe", 25, "john@email.com", "123 Street", "NYC", "USA", "10001");
// Which parameter is which? Hard to read!
Solution with Builder:
public class User {
private String firstName;
private String lastName;
private int age;
private String email;
private String address;
private User(Builder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.email = builder.email;
this.address = builder.address;
}
// Static inner Builder class
public static class Builder {
private String firstName; // Required
private String lastName; // Required
private int age; // Optional
private String email; // Optional
private String address; // Optional
public Builder(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public User build() {
return new User(this);
}
}
}
// Usage - clean and readable!
User user = new User.Builder("John", "Doe")
.age(25)
.email("john@email.com")
.address("NYC")
.build();
Real-world examples:
Benefits:
Adapter Pattern: Allows incompatible interfaces to work together. Acts as a bridge between two incompatible interfaces.
Real-world analogy: Power adapter that lets US devices work in EU sockets.
Use Cases:
// Existing interface your code expects
interface MediaPlayer {
void play(String filename);
}
// New interface from a library (incompatible)
interface AdvancedMediaPlayer {
void playMp4(String filename);
void playVlc(String filename);
}
// Advanced player implementation
class VlcPlayer implements AdvancedMediaPlayer {
public void playVlc(String filename) {
System.out.println("Playing VLC: " + filename);
}
public void playMp4(String filename) { }
}
class Mp4Player implements AdvancedMediaPlayer {
public void playMp4(String filename) {
System.out.println("Playing MP4: " + filename);
}
public void playVlc(String filename) { }
}
// ADAPTER - bridges the gap
class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedPlayer;
public MediaAdapter(String audioType) {
if (audioType.equals("vlc")) {
advancedPlayer = new VlcPlayer();
} else if (audioType.equals("mp4")) {
advancedPlayer = new Mp4Player();
}
}
public void play(String filename) {
String type = filename.substring(filename.lastIndexOf(".") + 1);
if (type.equals("vlc")) {
advancedPlayer.playVlc(filename);
} else if (type.equals("mp4")) {
advancedPlayer.playMp4(filename);
}
}
}
// Usage - client uses simple interface
class AudioPlayer implements MediaPlayer {
private MediaAdapter adapter;
public void play(String filename) {
String type = filename.substring(filename.lastIndexOf(".") + 1);
if (type.equals("mp3")) {
System.out.println("Playing MP3: " + filename);
} else if (type.equals("vlc") || type.equals("mp4")) {
adapter = new MediaAdapter(type);
adapter.play(filename);
}
}
}
AudioPlayer player = new AudioPlayer();
player.play("song.mp3"); // Native
player.play("movie.mp4"); // Through adapter
player.play("video.vlc"); // Through adapter
Key Points: