01

What is object-oriented programming?

Interview-ready answer

Object-oriented programming (OOP) is a programming paradigm that organizes software around objects. An object combines state, represented by data, with behavior, represented by operations or methods that can use or modify that data. Programs are designed as cooperating objects and their interactions.

Understand it clearly

Core idea

In OOP, an object represents a conceptual or real-world entity within a program. Rather than keeping data and the operations on that data entirely separate, OOP groups related state and behavior together.

State and behavior

An object's state describes its current condition, while its behavior defines what the object can do. Methods may read or change the object's state and may communicate with other objects.

  • State: Data or attributes held by an object, such as its current values or properties.
  • Behavior: Operations or methods that act on the object's state or interact with other objects.

Relationship to procedural programming

Procedural programming generally organizes code around procedures or functions and the sequence of actions they perform. OOP instead emphasizes objects that combine related data and behavior. In practice, many programs can use both object-oriented and procedural techniques.

Quick comparison
BasisObject-oriented programmingProcedural programming
Organizing principleObjects that combine state and behaviorProcedures or functions and the operations they perform
Primary focusModeling entities and their interactionsDefining sequences of steps to process data
Typical unitObjectProcedure or function
02

What is encapsulation, and why is making every field private not enough by itself?

Interview-ready answer

Encapsulation is an object-oriented design principle that combines an object's state with the operations that manage it, while hiding implementation details behind a controlled public interface. Its purpose is to protect valid state, reduce coupling, and let clients use an object through its behavior rather than its internal representation. Making fields private is a useful mechanism, but it is not sufficient by itself. A class can still expose its internals through unrestricted setters, getters that return mutable objects, or methods that allow invalid state changes. Proper encapsulation requires designing meaningful operations, validating changes, preserving invariants, and avoiding representation leaks.

Bad vs Better encapsulation (Java)java
import java.util.*;

// Bad: private field but exposes mutable list directly
class TeamBad {
    private final List<String> members = new ArrayList<>();

    public List<String> getMembers() { // leaks internal list
        return members;
    }

    public void addMember(String member) {
        members.add(member);
    }
}

// Better: expose a read-only view and control mutation
class TeamGood {
    private final List<String> members = new ArrayList<>();

    public List<String> getMembers() {
        return Collections.unmodifiableList(members);
    }

    public void addMember(String member) {
        if (member == null || member.isBlank()) {
            throw new IllegalArgumentException("invalid member");
        }
        members.add(member);
    }

    public boolean removeMember(String member) {
        return members.remove(member);
    }
}

// A behavior-oriented API can avoid exposing the collection entirely
class TeamBehavioral {
    private final Set<String> members = new LinkedHashSet<>();

    public boolean hire(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("invalid member");
        }
        return members.add(name);
    }

    public boolean fire(String name) {
        return members.remove(name);
    }

    public boolean isMember(String name) {
        return members.contains(name);
    }

    public int size() {
        return members.size();
    }
}
Understand it clearly

What encapsulation means

Encapsulation separates what an object does from how it stores or implements that behavior. Clients should rely on the public contract of methods rather than on fields, collection types, or other internal implementation choices.

A well-encapsulated class keeps its state consistent by controlling the operations that read or modify it. This allows the implementation to change without forcing clients to change, provided the public contract remains compatible.

  • Hide representation: Keep internal data structures and storage details out of the public contract.
  • Expose behavior: Provide operations that express the object’s responsibility and intent.
  • Protect invariants: Ensure state changes are validated and cannot leave the object in an invalid state.

Why private fields alone are insufficient

Private visibility prevents code outside the class from directly accessing a field. It does not ensure that the class controls its state meaningfully.

For example, a public setter for every field may allow arbitrary changes without validation or coordination with related fields. Similarly, a getter that returns a mutable internal collection lets callers change the collection without using the class’s intended operations.

  • Unrestricted setters: They can permit invalid values or illegal state transitions.
  • Mutable representation leaks: Returning an internal mutable array, collection, or mutable object can give callers indirect write access to private state.
  • Accessor-based coupling: Getters and setters can make clients depend on storage-oriented details instead of behavior.

How to apply encapsulation

Design the public API around meaningful commands and queries. For example, an operation such as addMember can validate its input and enforce membership rules, whereas exposing a writable members collection cannot reliably do so.

When data must be exposed, avoid giving callers mutable access to internal state. Depending on the language and requirements, return an immutable value, a defensive copy, a read-only view, or an abstraction that does not reveal the underlying representation.

  • Validate mutations: Check inputs and preserve invariants within the methods that change state.
  • Limit exposure: Expose only the information and operations clients need.
  • Avoid mutable leaks: Do not return internal mutable objects when external mutation would bypass the class’s rules.

Private fields versus encapsulation

Private fields are one tool used to support encapsulation. Encapsulation is the broader design goal: a stable, behavior-oriented interface that protects the object’s representation and correctness.

Simple data carriers may intentionally expose data more directly when they have no invariants or behavior to protect. That is a design choice, not the same as strong encapsulation.

Quick comparison
BasisEncapsulationPrivate fields only
PurposeHides representation and provides controlled, behavior-oriented access.Prevents direct access to fields from outside the class.
State changesChanges occur through operations that can validate inputs and preserve invariants.May still be uncontrolled through setters or leaked mutable references.
Internal exposureAvoids exposing mutable implementation details.Does not prevent getters from returning mutable internal objects.
Client couplingClients depend primarily on behavior and contracts.Clients may still depend on storage-oriented getter and setter semantics.
03

What is the difference between abstraction and encapsulation?

Interview-ready answer

Abstraction presents the essential operations of an object or component while hiding unnecessary implementation details. It focuses on what the component does. Encapsulation bundles state with the operations that manage it and controls access to that state. It focuses on protecting and maintaining the object’s internal state. For example, an interface can abstract a capability such as calculating an area without revealing how each shape performs the calculation. A class can encapsulate its fields by making them private and validating changes through methods. The concepts complement each other: abstraction simplifies how clients use a component, while encapsulation protects its implementation and invariants.

Java example showing abstraction and encapsulationjava
// Abstraction: define a contract for calculating an area.
interface Shape {
    double area();
}

// Encapsulation: keep state private and validate updates.
class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        setRadius(radius);
    }

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        if (radius < 0) {
            throw new IllegalArgumentException("radius must be non-negative");
        }
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

public class Main {
    public static void main(String[] args) {
        Shape shape = new Circle(2.5);
        System.out.println(shape.area());
    }
}
Understand it clearly

Abstraction

Abstraction provides a simplified view of a component by exposing a useful contract and omitting details that a client does not need to know. It answers the question, “What can this component do?”

In object-oriented programming, interfaces, abstract classes, and public API contracts are common ways to express abstraction. An abstraction can have multiple implementations without changing the code that depends only on its contract.

  • Main goal: Reduce complexity by exposing essential behavior.
  • Example: A Shape contract declares area() without specifying how a circle, rectangle, or another shape calculates it.

Encapsulation

Encapsulation groups an object’s data and the operations that work on that data. It also limits direct access to internal representation so the object can control how its state is created and changed.

Access modifiers such as private and protected are language-specific mechanisms commonly used to support encapsulation. Encapsulation does not require getters and setters; behavior-oriented methods are often preferable when they better preserve the object’s rules.

  • Main goal: Protect internal state and enforce valid object state.
  • Example: A Circle keeps its radius private and validates a new radius before storing it.

How they work together

The two concepts address different concerns and are commonly used together. A client can depend on an abstract contract, while each concrete implementation encapsulates its own state and internal logic.

Encapsulation helps implementations change without exposing internal representation. Abstraction helps clients use a component without depending on a particular implementation.

  • Abstraction controls: The operations a client can rely on through a contract.
  • Encapsulation controls: Direct access to an object’s internal data and implementation details.
Quick comparison
BasisAbstractionEncapsulation
FocusWhat a component does.How an object’s state and implementation are protected.
Primary purposePresent an essential, simpler contract.Maintain valid state and reduce exposure of internals.
Typical mechanismsInterfaces, abstract classes, and API contracts.Private state, access control, and methods that manage state changes.
ExampleShape exposes area() without revealing its calculation.Circle validates its private radius before updating it.
04

What is polymorphism?

Interview-ready answer

Polymorphism is an object-oriented programming concept in which code can use objects through a common interface or base type, while each concrete type provides its own behavior for the same operation. For example, code can call processPayment on a PaymentMethod without needing to know whether the actual object is a CreditCard, PayPal, or BankTransfer implementation.

Understand it clearly

Definition

The word polymorphism means “many forms.” In OOP, it allows different object types to respond to the same operation in type-specific ways. The calling code relies on a shared contract, such as an interface or base class, rather than on a particular concrete class.

How it works

A common interface or base class declares an operation, and concrete classes implement or override that operation. When code invokes the operation through the common type, the behavior associated with the actual object is used.

  • Common contract: A type such as PaymentMethod defines processPayment.
  • Concrete implementations: Classes such as CreditCard, PayPal, and BankTransfer provide their own payment behavior.
  • Caller code: The caller works with PaymentMethod and can use any compatible implementation without changing its logic.

Example

A notification system can define a Notification interface with a sendNotification operation. Email, SMS, and Push implementations can each deliver the message through their respective channel. The caller invokes the same operation regardless of the notification type.

Benefits

Polymorphism reduces coupling between client code and concrete classes. It makes systems easier to extend because a new implementation can often be added by following the existing interface, without modifying code that already depends on that interface.

05

What is inheritance, and when does an 'is-a' relationship justify using it?

Interview-ready answer

Inheritance is an object-oriented mechanism in which a derived class extends a base class, forming a subtype relationship. It is appropriate when the derived type is genuinely a specialized form of the base type and can be used wherever the base type is expected without violating its contract. This is the practical meaning of an “is-a” relationship and supports polymorphism. Use inheritance to model a stable shared abstraction, not merely to reuse implementation; for “has-a” or “uses-a” relationships, composition is usually a better fit.

Java examples: inheritance, a contract mismatch, and compositionjava
// Correct "is-a" example
class Animal {
    void eat() {
        System.out.println("eating");
    }
}

class Bird extends Animal {
    void fly() {
        System.out.println("flying");
    }
}

// Potential LSP violation when Rectangle allows independent dimensions
class Rectangle {
    protected int width;
    protected int height;

    void setWidth(int width) {
        this.width = width;
    }

    void setHeight(int height) {
        this.height = height;
    }

    int area() {
        return width * height;
    }
}

class Square extends Rectangle {
    @Override
    void setWidth(int width) {
        this.width = width;
        this.height = width;
    }

    @Override
    void setHeight(int height) {
        this.width = height;
        this.height = height;
    }
}

// Composition for a "has-a" relationship
class Engine {
    void start() {
        System.out.println("engine started");
    }
}

class Car {
    private final Engine engine;

    Car(Engine engine) {
        this.engine = engine;
    }

    void start() {
        engine.start();
    }
}
Understand it clearly

What inheritance means

Inheritance lets a derived class reuse and extend the behavior and accessible members of a base class. It also establishes a type relationship: code written against the base type can work with compatible derived objects.

The exact inheritance rules, such as member visibility, multiple inheritance support, and method-overriding behavior, depend on the programming language.

When “is-a” justifies inheritance

An “is-a” statement is a useful starting point, but it is not sufficient by itself. The derived class must preserve the promises made by the base class so that replacing a base object with a derived object does not break correct client code.

This requirement is commonly expressed by the Liskov Substitution Principle: a subtype should honor the behavioral contract of its supertype.

  • Specialization: The derived type represents a more specific kind of the base type, such as a Bird being an Animal.
  • Substitutability: A derived object can be passed to code expecting the base type without surprising changes in expected behavior.
  • Shared abstraction: The base class defines meaningful common behavior or a common interface that clients need to use polymorphically.
  • Stable semantics: The hierarchy reflects the domain model rather than being created only to avoid duplicated code.

When to prefer composition

Prefer composition when one object contains, uses, or delegates work to another object. Composition keeps responsibilities separate and avoids forcing a subtype relationship where none exists.

Inheritance can couple a derived class to assumptions and changes in the base class. If the goal is only implementation reuse, delegation to a contained object is often more flexible.

  • Has-a relationship: A Car has an Engine, so the Car should contain an Engine rather than extend it.
  • Behavior reuse only: If two classes need the same helper behavior but are not substitutable types, extract that behavior into a separate component.
  • Contract mismatch: Avoid inheritance when overriding a base operation would require changing its expected meaning or restricting valid base-class behavior.

A common contract mismatch

A Square may satisfy the everyday “is-a Rectangle” statement, but it can violate a mutable Rectangle contract if Rectangle allows width and height to change independently. Making Square setters update both dimensions can surprise code that expects those dimensions to remain independent.

The issue is not the names alone; it is whether the base-class contract was designed so that Square remains substitutable.

Quick comparison
BasisInheritanceComposition
Relationship modeledAn “is-a” subtype relationship.A “has-a” or “uses-a” relationship.
Reuse approachA derived class extends a base class.An object delegates work to contained or referenced components.
Best fitA stable shared abstraction with substitutable specialized types.Flexible reuse or collaboration between separate responsibilities.
Main riskA derived class may break the base-class contract or become tightly coupled to base-class changes.May require explicit delegation or additional component wiring.
06

What is the difference between method overloading and method overriding?

Interview-ready answer

Method overloading defines multiple methods with the same name but different parameter lists. The compiler selects the appropriate overload from the method call's arguments, so it is commonly called compile-time polymorphism. Method overriding occurs when a subclass provides its own implementation of an inherited instance method with the same signature; the implementation is selected at runtime based on the actual object, enabling runtime polymorphism.

Java examples: overloading and overridingjava
class Calculator {
    // Overloaded methods: same name, different parameter lists
    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;
    }
}

class Animal {
    String speak() {
        return "generic sound";
    }
}

class Dog extends Animal {
    @Override
    String speak() {
        return "bark";
    }
}

public class Demo {
    public static void main(String[] args) {
        Calculator c = new Calculator();
        System.out.println(c.add(1, 2));        // calls add(int, int)
        System.out.println(c.add(1.5, 2.5));    // calls add(double, double)

        Animal a = new Dog();
        System.out.println(a.speak());          // calls Dog.speak() at runtime
    }
}
Understand it clearly

Method overloading

Overloading uses the same method name with different parameter lists. The parameter lists must differ by number, types, or order of parameters; changing only the return type does not create a valid overload in Java.

It does not require inheritance, although methods with the same name and different parameter lists may also occur across a class hierarchy. Overload resolution is performed at compile time using the declared types of the arguments.

Method overriding

Overriding occurs when a subclass redefines an inherited instance method with the same name and parameter types. In Java, the overriding method must have the same return type or a covariant return type.

When a method is overridden, a call through a superclass reference is dynamically dispatched to the implementation for the actual object at runtime.

Rules that depend on the language

The following rules are stated for Java. An overriding method cannot reduce the visibility of the inherited method. A final method cannot be overridden. Static methods are hidden rather than overridden, and private methods are not inherited, so they cannot be overridden.

Java also restricts checked exceptions declared by an overriding method: it cannot declare broader checked exceptions than the inherited method. These restrictions do not arise merely from overloading.

When to use each

Use overloading to offer related operations with different input forms, such as accepting two integers, two floating-point values, or three integers. Use overriding when a subtype needs behavior specific to that subtype while preserving the superclass method contract.

Quick comparison
BasisMethod overloadingMethod overriding
DefinitionMultiple methods share a name but have different parameter lists.A subclass supplies a new implementation of an inherited instance method.
InheritanceNot required.Required.
Parameter listMust be different.Must be the same.
Return type in JavaMay differ, but return type alone cannot distinguish overloads.Must be the same or covariant.
Selection timeSelected at compile time from the call's argument types.Selected at runtime according to the actual object's class.
PolymorphismCommonly called compile-time or static polymorphism.Runtime or dynamic polymorphism.
Static, private, and final methods in JavaCan be overloaded when their parameter lists differ.Static methods are hidden, private methods are not overridden, and final methods cannot be overridden.
07

What is the difference between compile-time polymorphism and runtime polymorphism?

Interview-ready answer

Compile-time polymorphism is resolved by the compiler using information available at the call site, such as argument types and number of arguments. Common examples are method/function overloading and operator overloading. Runtime polymorphism is resolved while the program runs: a call through a base-class or interface reference invokes the implementation for the object’s actual runtime type, typically through method overriding and dynamic dispatch.

Java — overloading and overridingjava
class PolymorphismDemo {
    static class Calculator {
        int add(int a, int b) {
            return a + b;
        }

        double add(double a, double b) {
            return a + b;
        }
    }

    static class Animal {
        void speak() {
            System.out.println("Animal speaks");
        }
    }

    static class Dog extends Animal {
        @Override
        void speak() {
            System.out.println("Dog barks");
        }
    }

    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        System.out.println(calculator.add(1, 2));
        System.out.println(calculator.add(1.0, 2.0));

        Animal animal = new Dog();
        animal.speak();
    }
}
C++ — overloading and virtual overridingcpp
#include <iostream>

void func(int x) {
    std::cout << "int: " << x << '
';
}

void func(double x) {
    std::cout << "double: " << x << '
';
}

struct Base {
    virtual void speak() {
        std::cout << "Base
";
    }

    virtual ~Base() = default;
};

struct Derived : Base {
    void speak() override {
        std::cout << "Derived
";
    }
};

int main() {
    func(10);
    func(3.14);

    Base* base = new Derived();
    base->speak();
    delete base;
}
Understand it clearly

Compile-time polymorphism

Compile-time polymorphism, also called static polymorphism, selects the target operation before the program executes. For example, when overloaded methods have different parameter lists, the compiler chooses the applicable overload from the argument types at the call site.

The selected overload is fixed in the generated program for that call. It does not require inheritance. Templates in C++ are another form of compile-time polymorphism; generic mechanisms in other languages can have different implementation and dispatch behavior.

  • Typical mechanisms: Function or method overloading, operator overloading, and C++ templates.
  • Selection basis: Compile-time information, such as declared argument types, argument count, and available overloads.

Runtime polymorphism

Runtime polymorphism, also called dynamic polymorphism, selects an overridden implementation according to the actual object at runtime. It allows code written against a common base class or interface to work with multiple concrete implementations.

For example, if an Animal reference refers to a Dog object, calling speak() invokes Dog.speak() when the method participates in dynamic dispatch. The language runtime may implement this with a dispatch table, but that is an implementation detail rather than a universal requirement.

  • Typical mechanisms: Method overriding through virtual methods, abstract methods, or interface methods, depending on the language.
  • Requirement: A common polymorphic type relationship, such as a base class or interface; the exact language mechanism varies.

Trade-offs

Compile-time polymorphism is useful when the operation can be selected from static type information. It often enables direct calls and compiler optimization, although optimization results depend on the compiler and build settings.

Runtime polymorphism is useful when behavior must vary with the concrete object type, such as when processing different implementations through one interface. Dynamic dispatch can add indirection, though modern compilers and runtimes may optimize some calls when the target is known.

  • Use compile-time polymorphism: When the desired operation is determined by the call’s declared types and signatures.
  • Use runtime polymorphism: When clients should depend on a common abstraction while concrete implementations can vary at runtime.
Quick comparison
BasisCompile-time polymorphismRuntime polymorphism
Binding timeResolved by the compiler before execution.Resolved during execution based on the object’s runtime type.
Common mechanismsOverloading, operator overloading, and C++ templates.Overriding with virtual, abstract, or interface methods.
Inheritance or interface relationshipNot required.Typically uses a common base type or interface.
Call selectionBased on compile-time information at the call site.Based on the concrete object referenced at runtime.
Performance characteristicsCan use direct calls and may be optimized or inlined.May use dynamic dispatch and an indirect call; implementations may optimize known targets.
08

What is the difference between an abstract class and an interface, and when would you choose each?

Interview-ready answer

An abstract class is a base class that can combine shared state, constructors, implemented methods, and abstract methods. An interface defines a contract or capability that classes can implement, often alongside another base class. Choose an abstract class when related subclasses need common implementation or state. Choose an interface when different types should expose the same behavior, especially when a class needs to adopt multiple contracts. The exact rules depend on the language; for example, Java interfaces can include default, static, and private methods, but they do not provide per-object instance fields or constructors.

Java example: abstract class and interfacejava
// Abstract class with state and shared implementation
abstract class Vehicle {
    protected int speed;

    protected Vehicle(int initialSpeed) {
        this.speed = initialSpeed;
    }

    public void brake() {
        speed = Math.max(0, speed - 10);
    }

    public abstract void accelerate();
}

// Interface representing a capability
interface Flyable {
    default void takeOff() {
        System.out.println("Taking off");
    }

    static void checkWeather() {
        System.out.println("Checking weather");
    }
}

// Plane inherits shared Vehicle behavior and implements Flyable.
class Plane extends Vehicle implements Flyable {
    public Plane(int initialSpeed) {
        super(initialSpeed);
    }

    @Override
    public void accelerate() {
        speed += 50;
    }
}

class Main {
    public static void main(String[] args) {
        Plane plane = new Plane(0);
        plane.accelerate();
        plane.takeOff();
        Flyable.checkWeather();
    }
}
Understand it clearly

Abstract class

An abstract class is an incomplete class intended to be extended. It can contain instance fields, constructors, concrete methods, and abstract methods. Subclasses inherit its shared implementation and can supply the missing behavior.

An abstract class is useful when subclasses belong to the same conceptual family and need common state or a common implementation path. In Java, a class can extend only one class, abstract or concrete.

Interface

An interface defines a contract: a type that implements it agrees to provide its required behavior. It is commonly used to model a capability or role that can apply to otherwise unrelated classes.

In Java, interfaces can declare abstract methods and can also contain default, static, and private methods. Default methods can provide reusable behavior, but an interface does not have constructors or per-instance fields.

When to choose each

Use an abstract class when derived classes must share state, initialization through a constructor, protected helper methods, or substantial common implementation. It is also appropriate when the base class defines part of an algorithm and subclasses fill in selected steps.

Use an interface when the main goal is to define an API or capability that many types can adopt. A class can implement multiple interfaces in Java, so interfaces are appropriate when a type must satisfy several independent contracts.

  • Abstract class: Choose it for a shared base implementation and state among closely related subclasses.
  • Interface: Choose it for a role, capability, or contract that may be implemented by unrelated classes.

Design considerations

Adding or changing behavior in a base class can affect all subclasses, so a base hierarchy should remain focused and stable. Interfaces reduce coupling to a particular implementation, although default methods can still create conflicts when a class inherits incompatible defaults; the class must resolve such conflicts explicitly.

These distinctions are language-specific in detail. The comparison and example below use Java rules.

Quick comparison
BasisAbstract classInterface
Primary purposeProvide a shared base with common implementation and possibly state.Define a contract or capability.
Inheritance in JavaA class can extend one class.A class can implement multiple interfaces.
Instance stateCan declare instance fields and manage object state.Cannot declare per-instance fields; fields are constants.
MethodsCan contain abstract and concrete methods with normal class access control.Can declare abstract methods and can include default, static, and private methods.
ConstructorsCan declare constructors to initialize inherited state.Has no constructors.
Typical useRelated types need shared state, initialization, or implementation.Different types need to conform to the same API or capability.
09

Why is composition often preferred over inheritance?

Understand it clearly

Give an example.

10

What happens when a base-type reference refers to a derived object and an overridden method is called?

11

What is the difference between object identity and value equality?

Interview-ready answer

Object identity asks whether two references denote the very same object. Value equality asks whether two objects are considered equal by their values or logical state, even when they are separate instances. For example, in Python, `is` checks identity and `==` checks equality; in Java, `==` compares object references while `equals()` compares values when the type implements it accordingly.

Python — identity versus equalitypython
a = [1, 2]
b = a          # Same object
c = [1, 2]     # Different object with equal contents

print(a is b)  # True: same identity
print(a == b)  # True: equal values

print(a is c)  # False: different identities
print(a == c)  # True: equal values

b.append(3)
print(a)       # [1, 2, 3], because a and b refer to the same object
print(c)       # [1, 2]
Java — identity versus equalityjava
import java.util.Arrays;
import java.util.List;

String s1 = new String("x");
String s2 = new String("x");

System.out.println(s1 == s2);       // false: different object references
System.out.println(s1.equals(s2));  // true: same character content

List<String> list = Arrays.asList("a");
System.out.println(list.contains(new String("a"))); // true: uses equals()
Understand it clearly

Object identity

An object has an identity that distinguishes it from every other object instance. Two references have the same identity only when they refer to that exact instance.

When two references denote the same mutable object, a change made through one reference is observable through the other because both access the same object.

Value equality

Value equality means that two objects represent the same value or equivalent state. They may be different instances stored independently.

The precise meaning of equality is defined by the language and the type. For user-defined types, an equality operation should normally be reflexive, symmetric, transitive, and consistent.

Language-specific expressions

The general distinction is universal, but the syntax used to test it depends on the language.

  • Java: `==` compares object references, while `equals()` is used for logical equality.
  • Python: `is` checks identity, while `==` invokes value-equality behavior such as `__eq__`.
  • C#: `ReferenceEquals(a, b)` checks whether two references denote the same object; `Equals` and some overloaded `==` operators can express value equality.
  • C++: Pointer comparison can test whether two pointers refer to the same object, while `operator==` can define value equality for an object type.

Practical use and collection behavior

Use identity when the exact instance matters, such as checking a sentinel or determining whether two references share mutable state. Use value equality when the question is whether two values are logically equivalent.

For hash-based collections, equality and hashing must agree. In Java, types that override `equals()` should provide a compatible `hashCode()`. In Python, hashable objects used as dictionary keys or set elements must follow the language's equality and hashing rules.

Quick comparison
BasisObject identityValue equality
Question answeredAre these two references the same object instance?Do these two objects represent the same value or equivalent state?
Different instancesDifferent instances never have the same identity.Different instances can be equal.
Typical checksPython: `is`; Java: `==` for object references.Python: `==`; Java: `equals()`.
Mutable objectsReferences to the same object observe the same mutations.Two separate objects that are initially equal can cease to be equal if one is mutated.
Typical useShared-instance, sentinel, or reference-specific checks.Comparing domain values and working with value-based collection operations.
12

What is the purpose of a constructor, and why should an object be valid after construction?

Interview-ready answer

A constructor establishes an object’s initial valid state. It initializes required fields, validates inputs, and enforces the class invariants—the conditions that must hold for the object to behave correctly. After successful construction, the object should be ready to use so callers and instance methods do not need to handle an uninitialized or internally inconsistent state. If that state cannot be established, construction should report failure using the language-appropriate mechanism, such as an exception or a factory method that returns an error result.

C++ example (RAII and throwing on invalid input)cpp
#include <cstdio>
#include <stdexcept>
#include <string>

class FileWrapper {
public:
    explicit FileWrapper(const std::string& path) {
        file_ = std::fopen(path.c_str(), "r");
        if (!file_) {
            throw std::runtime_error("cannot open file");
        }
        // Invariant: file_ != nullptr.
    }

    ~FileWrapper() {
        if (file_) {
            std::fclose(file_);
        }
    }

    FileWrapper(const FileWrapper&) = delete;
    FileWrapper& operator=(const FileWrapper&) = delete;

    FILE* get() const { return file_; }

private:
    FILE* file_ = nullptr;
};

// Construction either succeeds with a valid FileWrapper or throws.
Java example (validate and throw)java
public class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("name required");
        }
        if (age < 0) {
            throw new IllegalArgumentException("age must be non-negative");
        }

        this.name = name;
        this.age = age;
    }
}
Python example (raise on bad input)python
class Vector:
    def __init__(self, coords):
        if not coords:
            raise ValueError("coords must not be empty")
        self.coords = tuple(coords)

# Vector([1, 2, 3]) creates a valid object.
# Vector([]) raises ValueError.
Understand it clearly

Purpose of a constructor

A constructor runs when an object is created. Its main responsibility is to establish the minimum state required by the class contract: initialize essential data, validate constructor arguments, and set up required dependencies or resources.

This is where class invariants should be established. For example, if a class requires a non-empty name or a non-negative age, the constructor can reject invalid input rather than allowing an object that violates those rules.

  • Initialize state: Assign required fields and dependencies.
  • Validate input: Reject values that would violate the class contract.
  • Establish invariants: Ensure the conditions required for correct object behavior hold after successful construction.

Why the object must be valid after construction

A successfully constructed object should be immediately usable according to its public contract. This lets callers use it without first checking whether an initialization step was missed, and it lets instance methods rely on the object’s invariants.

Allowing a publicly accessible object to remain partially initialized increases complexity and creates more opportunities for incorrect use. Keeping invalid states from escaping construction improves encapsulation and makes the class easier to reason about and test.

  • Predictable behavior: Clients can assume that successful construction produces a usable object.
  • Simpler methods: Methods can rely on established invariants instead of repeatedly checking initialization state.
  • Stronger encapsulation: The class controls and protects its own validity rules.

Failure during construction

If a constructor cannot establish a valid state, it should not silently leave a usable reference to an invalid object. In languages with exceptions, a constructor can throw or raise an exception. Where explicit error handling is preferred, a factory method can perform the work and return either a valid object or an error result.

Resources acquired during construction require careful cleanup if a later step fails. The exact mechanism is language-dependent: C++ commonly relies on RAII and automatic destruction of fully constructed members, while other languages use structured cleanup or resource-management constructs.

Two-phase initialization—creating an object and requiring a later init call—should generally be avoided when it exposes an object before it is ready. It may be necessary in some APIs, but the not-ready state must then be made explicit and controlled.

  • Exception-based failure: Throw or raise when the required valid state cannot be created.
  • Factory-based failure: Use a factory that returns an object only on success and otherwise returns an explicit error result.
  • Resource safety: Ensure resources acquired before failure are released by the language-appropriate cleanup mechanism.
13

How do access modifiers support encapsulation and data hiding?

Understand it clearly

Page 2

14

What is the difference between association, aggregation, and composition?

Interview-ready answer

Association is a general relationship in which objects are connected or interact, without implying ownership. Aggregation and composition are whole–part forms of association. In aggregation, parts can exist independently of the whole and may be shared. In composition, the whole exclusively owns its parts in the model, and a part’s lifetime is tied to that whole.

Association (Java)java
class Course {
  private final String title;

  Course(String title) {
    this.title = title;
  }

  String getTitle() {
    return title;
  }
}

class Student {
  private final String name;

  Student(String name) {
    this.name = name;
  }

  void enroll(Course course) {
    System.out.println(name + " enrolls in " + course.getTitle());
  }
}
Aggregation (Java)java
import java.util.List;

class Player {
  private final String name;

  Player(String name) {
    this.name = name;
  }
}

class Team {
  private final List<Player> players;

  Team(List<Player> players) {
    this.players = players;
  }
}

// Player objects are supplied externally and can exist without a Team.
Composition (Java)java
import java.util.ArrayList;
import java.util.List;

class House {
  private static class Room {
    private final String name;

    Room(String name) {
      this.name = name;
    }
  }

  private final List<Room> rooms = new ArrayList<>();

  House(int count) {
    for (int i = 0; i < count; i++) {
      rooms.add(new Room("Room" + i));
    }
  }
}

// Room is an internal part of House in this model.
Understand it clearly

Association

Association represents a general structural relationship between classes or objects. For example, a Student may be associated with a Course. Neither object necessarily owns or controls the lifecycle of the other.

A temporary use of an object, such as passing it to a method, is often modeled as a dependency rather than a persistent association in UML. In ordinary code, both may be implemented with references, so the intended domain relationship matters more than the syntax.

Aggregation

Aggregation is a whole–part relationship with independent lifecycles. The whole groups or refers to parts, but the parts can continue to exist if the whole is removed and can potentially be shared by other wholes.

For example, a Team aggregates Player objects when players exist independently of a particular team. Aggregation does not imply that the whole is responsible for creating or destroying its parts.

Composition

Composition is the strongest whole–part relationship. A part belongs to one composite at a time in the model, and its lifecycle is dependent on that composite. When the composite is destroyed, its parts are conceptually destroyed as well.

Composition is a design relationship, not merely a coding pattern. Creating a member object inside a constructor can support composition, but it does not by itself establish the relationship; the model must also treat that object as an exclusive, lifecycle-bound part.

Practical interpretation

Use association when objects simply collaborate. Use aggregation when the whole groups independent parts. Use composition when a part has no meaningful independent lifecycle outside its whole.

The exact implementation of ownership and cleanup depends on the language and runtime. For example, garbage-collected languages reclaim memory automatically, but the composition relationship still expresses lifecycle and exclusive ownership in the object model.

  • UML notation: Association is typically shown with a plain line, aggregation with a hollow diamond at the whole end, and composition with a filled diamond at the whole end.
Quick comparison
BasisRelationshipMeaning and lifecycle
AssociationGeneral connection or collaboration between objectsNo ownership or lifecycle dependency is implied.
AggregationWhole–part relationship with independent partsParts can outlive the whole and may be shared or reassigned.
CompositionWhole–part relationship with exclusive, lifecycle-bound partsA part belongs to its composite and is conceptually removed with it.
15

What is the difference between shallow copy and deep copy?

Interview-ready answer

A shallow copy creates a new outer object but keeps references to the same nested objects. A deep copy creates a new object graph for the mutable nested objects, so changes made through the copy do not affect the original. The exact behavior depends on the language and copying mechanism; immutable values may safely be shared even in a deep copy.

Python example: shallow copy and deep copypython
import copy

orig = [1, [2, 3]]
sh = copy.copy(orig)
dp = copy.deepcopy(orig)

# Replacing a top-level element affects only the shallow copy.
sh[0] = 10

# Mutating a nested list affects orig because it is shared with sh.
sh[1][0] = 20

print('orig after shallow changes:', orig)
print('sh:', sh)
print('dp (unchanged):', dp)

# Mutating a nested list in the deep copy does not affect orig.
dp[1][1] = 99
print('orig after deep change:', orig)
print('dp:', dp)
Java example: shallow clone and explicit deep copyjava
class Node implements Cloneable {
    int value;
    Node child;

    Node(int value, Node child) {
        this.value = value;
        this.child = child;
    }

    // Shallow clone: referenced objects are not cloned.
    @Override
    public Node clone() throws CloneNotSupportedException {
        return (Node) super.clone();
    }

    // Recursive deep copy for an acyclic Node structure.
    Node deepCopy() {
        return new Node(value, child == null ? null : child.deepCopy());
    }
}

Node original = new Node(1, new Node(2, null));
Node shallow = original.clone();
shallow.child.value = 99;
System.out.println(original.child.value); // 99: child is shared

Node deep = original.deepCopy();
deep.child.value = 5;
System.out.println(original.child.value); // 99: original is unchanged
Understand it clearly

Shallow copy

A shallow copy duplicates only the outer container or object. Its fields or elements that refer to other objects still refer to the same nested objects as the original.

Therefore, changing a top-level field or element in the shallow copy does not change the original, but mutating a shared nested object is visible through both references.

Deep copy

A deep copy duplicates the outer object and recursively copies relevant nested mutable objects. This gives the copy independent mutable state, so a mutation in the copied structure does not affect the corresponding structure in the original.

Deep-copy implementations commonly preserve relationships within the copied graph. For example, they may need to track already copied objects to handle cyclic references without recursing indefinitely.

Language and implementation considerations

Copy behavior is language- and API-specific. A language may provide separate shallow- and deep-copy utilities, or it may require the programmer to define how referenced objects are copied.

Immutable objects can usually be shared safely because they cannot be modified. Objects representing external resources or runtime-specific state may not have a meaningful general-purpose deep-copy operation.

  • Python: copy.copy() performs a shallow copy, while copy.deepcopy() performs a recursive copy and uses memoization to handle repeated references and cycles.
  • Java: Object.clone(), when implemented using super.clone(), copies fields and is shallow for referenced objects. A deep copy requires explicit copying of referenced mutable objects.
  • C++: Copy constructors and copy-assignment operators define copy behavior. Classes that own resources must implement appropriate copy semantics when independent ownership is required.

Choosing between them

Use a shallow copy when sharing nested data is intentional or when that data is immutable. Use a deep copy when the new structure must be independently mutable.

Deep copying generally requires more time and memory because it traverses and allocates objects throughout the structure.

Quick comparison
BasisShallow copyDeep copy
What is copiedA new outer object is created; nested object references are retained.The outer object and relevant nested mutable objects are copied recursively.
Nested mutable objectsShared with the original.Independent from the original when copied by the deep-copy operation.
Effect of nested mutationA mutation is visible through both the original and the copy.A mutation in the copy does not affect the original.
CostUsually lower, because fewer objects are allocated.Usually higher, because more of the object graph is traversed and allocated.
Cyclic referencesExisting references are retained.The implementation must track previously copied objects to preserve cycles and avoid infinite recursion.
16

What is an immutable object, and what advantages can immutability provide?

Interview-ready answer

An immutable object is an object whose observable state cannot change after it is created. Instead of modifying the existing object, an operation that represents a change returns a new object. Immutability makes objects safer to share, easier to reason about, and suitable for stable equality and hash-based collections when their equality-relevant state is immutable.

Java immutable class examplejava
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

public final class Person {
    private final String name;
    private final List<String> tags;

    public Person(String name, List<String> tags) {
        this.name = Objects.requireNonNull(name);
        this.tags = Collections.unmodifiableList(
            new ArrayList<>(Objects.requireNonNull(tags))
        );
    }

    public String getName() {
        return name;
    }

    public List<String> getTags() {
        return tags;
    }

    public Person withName(String newName) {
        return new Person(newName, tags);
    }

    public Person withAddedTag(String tag) {
        List<String> newTags = new ArrayList<>(tags);
        newTags.add(tag);
        return new Person(name, newTags);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person other = (Person) o;
        return name.equals(other.name) && tags.equals(other.tags);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, tags);
    }
}
Understand it clearly

Definition

An object is immutable when its externally observable state remains unchanged throughout its lifetime. Its fields may reference other objects, but those referenced objects must not allow the immutable object's observable state to be changed.

Advantages

Because an immutable object's state does not change, it can be shared without callers needing to coordinate updates. This reduces unintended side effects and makes code easier to understand, test, and use in concurrent programs.

  • Safer sharing: Immutable objects can be shared between threads without synchronization for changes to that object's state.
  • Simpler reasoning: A reference always represents the same value, which reduces bugs caused by unexpected mutation.
  • Stable map and set usage: An immutable object whose equals and hashCode depend only on immutable state is safe to use as a key in hash-based collections.
  • Caching and reuse: Instances can often be safely reused because their value does not change.
  • Functional style: Operations can return new values rather than modify existing ones, which supports composition and predictable behavior.

Implementing immutability

The exact mechanisms depend on the programming language. In languages such as Java, the class must prevent both direct field changes and indirect changes through mutable objects supplied by callers.

  • Prevent mutation: Do not expose setters or other methods that alter the object's state; return a new instance for value-changing operations.
  • Protect fields: Keep state private and initialize it during construction. In Java, fields are commonly declared final to prevent reassignment.
  • Defend mutable state: Copy mutable constructor inputs and avoid exposing mutable internal objects directly. Return copies or immutable representations when necessary.
  • Control inheritance: In Java, making the class final is a common way to prevent subclasses from weakening the immutability contract.

Trade-offs

Creating a new object for each logical change can increase allocation and copying costs, especially for large objects or frequent updates. Builders and data structures that share unchanged internal structure can reduce these costs when appropriate.

17

What do high cohesion and low coupling mean in object-oriented design?

Interview-ready answer

High cohesion means a class or module has closely related responsibilities that serve a focused purpose. Low coupling means classes or modules have minimal, well-defined dependencies on one another. Together, they make a system easier to understand, test, change, and reuse because changes are more likely to stay within the relevant module.

Understand it clearly

High cohesion

Cohesion describes how strongly the responsibilities within one class or module belong together. A highly cohesive class groups related data and behavior around a clear responsibility.

A cohesive class is not defined only by being small; its operations should support the same purpose. For example, a repository can focus on storing and retrieving a particular kind of data rather than also handling user-interface formatting or unrelated business rules.

Low coupling

Coupling describes the degree of dependency between classes or modules. Low coupling means a unit relies on other units through limited, clear contracts instead of their internal implementation details.

Low coupling does not mean that modules never interact. It means their necessary interactions are controlled, so a change in one module is less likely to require changes throughout the system.

Why they matter

High cohesion makes a unit easier to understand because its code addresses a focused concern. Low coupling reduces the ripple effect of changes because fewer units depend directly on one another.

Used together, these properties improve maintainability, testability, and reuse. A focused module with a narrow, stable interface can be tested or replaced more independently.

Design practices

Define meaningful class boundaries, keep related behavior and data together, and avoid mixing unrelated concerns in one class. Expose only the operations that clients need, while keeping implementation details encapsulated.

  • Improve cohesion: Split a class when it contains unrelated responsibilities, such as persistence, presentation formatting, and business-rule processing.
  • Reduce coupling: Depend on clear interfaces or abstractions where appropriate, and pass required dependencies explicitly rather than relying on global state.
  • Watch for warning signs: Unrelated methods in one class can indicate low cohesion; frequent changes across several modules for one feature can indicate excessive coupling.
Quick comparison
BasisHigh CohesionLow Coupling
FocusRelationships among responsibilities within one class or module.Dependencies between separate classes or modules.
Desired conditionResponsibilities are closely related and support a focused purpose.Dependencies are limited, explicit, and based on clear contracts.
Primary benefitImproves local clarity and makes the unit easier to maintain.Reduces ripple effects and supports independent change.
Poor-design signOne class mixes unrelated concerns.A change in one module forces changes in many other modules.
Typical practicesSeparation of concerns and focused class responsibilities.Encapsulation, narrow interfaces, and explicit dependencies.
18

What is the Single Responsibility Principle, and how can you identify a class with too many responsibilities?

Interview-ready answer

The Single Responsibility Principle (SRP) states that a class should have one responsibility: one cohesive purpose and one primary reason to change. A class has too many responsibilities when it combines unrelated concerns, such as business rules, persistence, notification, and formatting, so that changes in any of those areas require modifying the same class. Typical signs are low cohesion, methods and fields that serve unrelated purposes, dependencies on unrelated collaborators, a vague or overly broad class name, and change history showing that different kinds of changes repeatedly affect the class. Class size alone is not proof of an SRP violation; the key question is whether its members support the same responsibility and change together for the same reason.

SRP example — before and after (Java)java
/* Before: UserManager combines persistence and notification concerns. */
public class UserManager {
    private final Database db;
    private final EmailClient emailClient;

    public UserManager(Database db, EmailClient emailClient) {
        this.db = db;
        this.emailClient = emailClient;
    }

    public void createUser(User user) {
        db.insert("users", user.toMap());
        emailClient.send(user.getEmail(), "Welcome", "Thanks for signing up!");
    }

    public void deleteUser(String userId) {
        db.delete("users", userId);
        emailClient.sendAdminNotification("User deleted: " + userId);
    }
}

/* After: persistence and notification are separate responsibilities. */
public class UserRepository {
    private final Database db;

    public UserRepository(Database db) {
        this.db = db;
    }

    public void save(User user) {
        db.insert("users", user.toMap());
    }

    public void delete(String userId) {
        db.delete("users", userId);
    }
}

public class EmailService {
    private final EmailClient emailClient;

    public EmailService(EmailClient emailClient) {
        this.emailClient = emailClient;
    }

    public void sendWelcome(String to) {
        emailClient.send(to, "Welcome", "Thanks for signing up!");
    }

    public void notifyAdmin(String message) {
        emailClient.sendAdminNotification(message);
    }
}

/* This class coordinates the use case without implementing persistence or email details. */
public class UserManager {
    private final UserRepository userRepository;
    private final EmailService emailService;

    public UserManager(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }

    public void createUser(User user) {
        userRepository.save(user);
        emailService.sendWelcome(user.getEmail());
    }

    public void deleteUser(String userId) {
        userRepository.delete(userId);
        emailService.notifyAdmin("User deleted: " + userId);
    }
}
Understand it clearly

Meaning of SRP

SRP is one of the SOLID principles. It is commonly expressed as: a class should have one reason to change. More precisely, its methods and data should form one cohesive responsibility, often associated with a single kind of concern or stakeholder-driven change.

SRP does not mean every class must contain only one method or be small. A class can contain several related operations when they support the same abstraction and tend to change together.

How to identify too many responsibilities

Look at the class's behavior, dependencies, tests, and change history. The strongest evidence is that independent changes in different concerns require edits to the same class.

  • Mixed concerns: The class performs unrelated work, such as storing data, applying business rules, sending notifications, and formatting output.
  • Low cohesion: Its methods and fields naturally fall into separate groups, with little shared data or purpose.
  • Unrelated dependencies: It depends on collaborators from distinct concerns, such as a repository, mail client, and formatter, without one cohesive role explaining those dependencies.
  • Multiple reasons to change: Database changes, notification-policy changes, and presentation changes each require modifying the same class.
  • Broad naming: The class name is vague or does not accurately describe all of its behavior.
  • Difficult focused testing: Tests require many unrelated collaborators or mocks because the class coordinates several independent concerns.

Refactoring an SRP violation

First, identify groups of methods and data that change for different reasons. Extract those groups into focused classes, then let a higher-level coordinator compose them when an operation legitimately requires several responsibilities.

Refactor incrementally and preserve behavior with tests. Interfaces can be useful at boundaries where callers need to depend on an abstraction, but they are not required merely because a class is extracted.

  • Separate cohesive groups: Move persistence, notification, formatting, or other independent concerns into classes with clear, focused APIs.
  • Keep orchestration explicit: A coordinating class may call the focused components to complete a use case without taking ownership of their internal responsibilities.
  • Avoid over-splitting: Do not create separate classes for operations that are naturally part of the same cohesive abstraction.

Benefits and trade-offs

Applying SRP usually improves maintainability, readability, and testability because each class has a clearer purpose and smaller change surface. It can also reduce coupling between unrelated concerns.

However, splitting classes introduces more types and collaboration paths. Apply SRP to improve cohesion and isolate independent change, not simply to minimize the number of methods in each class.

19

How does the Open/Closed Principle help a system support new behaviour without changing existing client logic?

Interview-ready answer

The Open/Closed Principle helps by making client code depend on a stable abstraction rather than on specific implementations. To introduce new behaviour, add a new implementation, strategy, or decorator that satisfies the existing contract, then supply it through configuration, a factory, or dependency injection. The client continues to call the same abstraction, so its business logic does not need to change.

Java example: add new behavior without changing the clientjava
interface PaymentStrategy {
    void pay(double amount);
}

class CreditCard implements PaymentStrategy {
    public void pay(double amount) {
        System.out.println("Paid " + amount + " with Credit Card");
    }
}

class PayPal implements PaymentStrategy {
    public void pay(double amount) {
        System.out.println("Paid " + amount + " with PayPal");
    }
}

// Client depends only on the abstraction.
class Checkout {
    private final PaymentStrategy strategy;

    Checkout(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    void processOrder(double amount) {
        strategy.pay(amount);
    }
}

// New behavior added without modifying Checkout.
class Bitcoin implements PaymentStrategy {
    public void pay(double amount) {
        System.out.println("Paid " + amount + " with Bitcoin");
    }
}

public class App {
    public static void main(String[] args) {
        // Composition chooses the behavior supplied to the client.
        PaymentStrategy strategy = new Bitcoin();
        Checkout checkout = new Checkout(strategy);
        checkout.processOrder(250.0);
    }
}
Understand it clearly

Core idea

The Open/Closed Principle states that software entities should be open for extension and closed for modification. In practice, this means designing a stable extension point so that new behaviour can be introduced by adding code rather than repeatedly editing established client logic.

A module is not necessarily closed to every possible change. It is closed with respect to the variations anticipated by its abstraction or extension mechanism.

How the client remains unchanged

A client depends on an interface or abstract type that defines the operations it needs. It does not need to know which concrete class performs those operations.

When a new implementation conforms to the same contract, it can be substituted for an existing implementation. The client invokes the same methods, while the selected implementation provides different runtime behaviour.

  • Abstraction: Defines the stable contract used by the client.
  • New implementation: Adds behaviour by implementing the existing contract.
  • Composition: Selects and supplies the implementation outside the client, for example through a factory or dependency injection.

Example

In the example, Checkout depends on PaymentStrategy. Adding Bitcoin creates another PaymentStrategy implementation, so Checkout does not need to be modified. The application composition code chooses whether Checkout receives CreditCard, PayPal, or Bitcoin.

Design considerations

OCP reduces the chance that a new variation will disturb stable client code, but it does not eliminate all changes. The composition layer may need to select or register the new implementation, and the abstraction itself may need revision if the new requirement cannot be expressed by the existing contract.

Abstractions should represent meaningful, likely points of variation. Creating interfaces or extension mechanisms for every small possibility can add unnecessary indirection and complexity.

20

What is dynamic binding, and how does it enable runtime polymorphism?

Interview-ready answer

Dynamic binding, or late binding, is the runtime selection of the method implementation to execute based on an object’s actual type rather than the reference variable’s declared type. It enables runtime polymorphism because a base-class or interface reference can refer to different concrete objects, and an overridden method call invokes the appropriate implementation for the object currently referenced.

Java example demonstrating dynamic bindingjava
class Animal {
    void speak() {
        System.out.println("Animal speaks");
    }
}

class Dog extends Animal {
    @Override
    void speak() {
        System.out.println("Woof");
    }
}

class Cat extends Animal {
    @Override
    void speak() {
        System.out.println("Meow");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a1 = new Dog();
        Animal a2 = new Cat();

        a1.speak(); // Prints "Woof"
        a2.speak(); // Prints "Meow"

        Animal[] zoo = { new Dog(), new Animal(), new Cat() };
        for (Animal a : zoo) {
            a.speak();
        }
    }
}
Understand it clearly

Definition

In object-oriented programming, dynamic binding determines the target of an overridable method call while the program is running. The compiler verifies that the method can be called through the declared reference type, while runtime dispatch selects the implementation associated with the actual object type.

Dynamic binding contrasts with static binding, in which the method target is determined at compile time.

How it enables runtime polymorphism

A reference declared as a base class or interface can hold objects of multiple implementing or derived types. When code calls an overridden method through that reference, the same call site can produce different behavior for different objects.

This lets client code depend on a common abstraction instead of concrete classes. New implementations can be used by the same client code as long as they conform to the required base class or interface.

  • Declared type: Determines which members may be called through the reference.
  • Actual object type: Determines which overridden implementation runs for a dynamically dispatched call.

Runtime dispatch

Languages use runtime dispatch mechanisms to locate the correct overridden method. The exact mechanism is language- and implementation-specific; for example, many compiled object-oriented language implementations use method tables, while dynamic languages may use runtime method lookup.

In Java, ordinary instance-method calls are generally dynamically bound, except for methods such as static, final, and private methods, which are not overridden in the same way.

Trade-offs

Dynamic binding improves extensibility and loose coupling because code can operate on abstractions while object-specific behavior remains in concrete classes. It can introduce dispatch overhead, although runtime systems may optimize many calls when the target can be determined safely.

21

What is the difference between static members and instance members, and when would you use each?

Interview-ready answer

Static members belong to the class or type and are shared by all its objects. Instance members belong to individual objects, so each object has its own instance fields and can use methods that operate on its own state. Use static members for class-wide constants, counters, or utility behavior; use instance members for data and behavior that vary from one object to another.

Example (Java): static vs instance membersjava
public class Widget {
    // Static: shared by all Widget objects
    private static int totalCreated = 0;
    public static final double VERSION = 1.0;

    // Instance: each Widget has its own values
    private final int id;
    private String name;

    public Widget(String name) {
        this.id = ++totalCreated;
        this.name = name;
    }

    // Static method: class-level behavior
    public static int getTotalCreated() {
        return totalCreated;
    }

    // Instance method: operates on this object's state
    public String describe() {
        return "Widget{" + "id=" + id + ", name='" + name + "'}";
    }

    public static synchronized void resetTotalCreated() {
        totalCreated = 0;
    }

    public static void main(String[] args) {
        Widget a = new Widget("A");
        Widget b = new Widget("B");

        System.out.println(Widget.getTotalCreated()); // 2
        System.out.println(a.describe());             // a's instance data
    }
}
Understand it clearly

Core difference

A static field has one value associated with the class, rather than a separate value for every object. A static method is called on the class and does not have a this object, so it can directly access only static members.

An instance field is stored separately in each object. An instance method runs for a particular object, can access that object's instance and static members, and can use the object's state.

Access and lifecycle

Static members are normally accessed through the class name, such as Widget.getTotalCreated(). Instance members are accessed through an object reference, such as widget.describe().

In Java, static fields are initialized when their class is initialized, and instance fields are initialized as part of creating an object. Precise storage layout and lifetime details depend on the language runtime and implementation.

When to use each

Choose static when data or behavior is genuinely shared across all objects, or when an operation does not need object-specific state. Choose instance members when the state or behavior belongs to a particular object.

  • Static members: Class-wide constants, shared counters, shared caches, factory methods, and stateless utility methods.
  • Instance members: Properties that differ per object, such as an object's identifier or name, and behavior that depends on that object's state.
  • Mutable shared state: Use static mutable fields carefully because they are shared. If multiple threads can access them, coordinate access with appropriate synchronization or concurrency mechanisms.

Inheritance and polymorphism

Instance methods can participate in runtime polymorphism when they are overridable: a call can dispatch to an implementation based on the object's runtime type. In Java, static methods are associated with the class and are hidden rather than overridden, so they do not use instance-method dynamic dispatch.

Quick comparison
BasisStatic membersInstance members
AssociationBelong to the class or type.Belong to individual objects.
Number of copiesOne shared value per class for a static field.A separate field value for each object.
AccessNormally accessed through the class name.Accessed through an object reference.
Method stateNo this object; directly accesses static members only.Runs on a specific object and can access that object's state.
Polymorphism in JavaStatic methods are hidden, not overridden.Overridable instance methods can use dynamic dispatch.
Typical useConstants, utilities, counters, and other class-wide behavior.Per-object state and behavior.
22

A payment application supports Card, UPI, and Wallet payments. How would you design it so new payment methods can be added easily?

Interview-ready answer

Design the payment module around a common PaymentProvider interface and a PaymentService that delegates to the appropriate provider through a registry or factory. Card, UPI, and Wallet are separate implementations of the same contract. To add a new method, implement the interface, register it under a method key, and provide any adapter needed for an external gateway SDK. The orchestration code remains unchanged, following the Open/Closed Principle.

Java example: interface, registry, and orchestratorjava
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

public final class PaymentRequest {
    public final String idempotencyKey;
    public final String method;
    public final long amount;

    public PaymentRequest(String idempotencyKey, String method, long amount) {
        this.idempotencyKey = idempotencyKey;
        this.method = method;
        this.amount = amount;
    }
}

public final class PaymentResponse {
    public final boolean success;
    public final String providerReference;
    public final String errorCode;

    public PaymentResponse(boolean success, String providerReference, String errorCode) {
        this.success = success;
        this.providerReference = providerReference;
        this.errorCode = errorCode;
    }
}

public interface PaymentProvider {
    String key();
    PaymentResponse process(PaymentRequest request);
}

public final class ProviderRegistry {
    private final Map<String, PaymentProvider> providers = new ConcurrentHashMap<>();

    public void register(PaymentProvider provider) {
        providers.put(normalize(provider.key()), provider);
    }

    public Optional<PaymentProvider> lookup(String key) {
        return Optional.ofNullable(providers.get(normalize(key)));
    }

    private String normalize(String key) {
        return key.toUpperCase(Locale.ROOT);
    }
}

public final class PaymentService {
    private final ProviderRegistry registry;

    public PaymentService(ProviderRegistry registry) {
        this.registry = registry;
    }

    public PaymentResponse pay(PaymentRequest request) {
        if (request == null || request.method == null || request.amount <= 0) {
            return new PaymentResponse(false, null, "invalid_request");
        }

        return registry.lookup(request.method)
                .map(provider -> provider.process(request))
                .orElseGet(() -> new PaymentResponse(false, null, "unsupported_method"));
    }
}

public final class CardProvider implements PaymentProvider {
    @Override
    public String key() {
        return "CARD";
    }

    @Override
    public PaymentResponse process(PaymentRequest request) {
        // Call a card gateway through an adapter and map its result.
        return new PaymentResponse(true, "gateway-reference", null);
    }
}

// Application wiring
ProviderRegistry registry = new ProviderRegistry();
registry.register(new CardProvider());
registry.register(new UpiProvider());
registry.register(new WalletProvider());

PaymentService service = new PaymentService(registry);
PaymentResponse response = service.pay(
        new PaymentRequest("idem-1", "CARD", 1000)
);
Understand it clearly

Core design

Use the Strategy pattern to represent each payment method behind a common interface. PaymentService should contain shared workflow concerns, such as request validation, provider selection, idempotency handling, and normalized error handling, while individual providers contain method-specific gateway logic.

A registry or factory maps a payment method key, such as CARD, UPI, or WALLET, to its PaymentProvider implementation. This prevents PaymentService from depending directly on concrete provider classes.

  • PaymentProvider: Defines the common operation for processing a payment.
  • PaymentService: Coordinates the payment flow and delegates to the selected provider.
  • ProviderRegistry: Stores and resolves providers by payment-method key.
  • Adapter: Wraps a third-party payment SDK and translates between application models and SDK-specific models.

Adding a payment method

A new payment method should be introduced without modifying PaymentService or existing providers. Create a new PaymentProvider implementation, adapt its external gateway if necessary, and register it with the registry during application configuration or dependency-injection setup.

  • Implement: Create a provider that implements the common PaymentProvider contract.
  • Adapt: Keep gateway-specific request conversion, response conversion, and error translation inside the provider or an adapter.
  • Register: Associate the provider with a unique payment-method key so it can be resolved at runtime.
  • Verify: Test that the provider follows the common contract, including success, failure, timeout, and idempotency behavior where supported.

Operational considerations

Keep provider-specific details out of the public response model so callers receive a stable application-level result. Store credentials securely, avoid logging sensitive payment data, and add provider-level monitoring.

Timeouts and carefully controlled retries are important for external gateways. Retries must be compatible with idempotency so that a transient failure does not unintentionally create duplicate charges.

  • Security: Protect credentials and avoid exposing sensitive payment information in logs or errors.
  • Resilience: Apply timeouts and use idempotency-aware retry behavior for gateway calls.
  • Observability: Record provider-level latency, outcomes, and structured diagnostic information.
23

A notification service supports Email and SMS. How would you add Push Notification without filling the client code with conditionals?

Interview-ready answer

Use polymorphism: define a NotificationSender interface, provide EmailSender, SmsSender, and PushSender implementations, and have NotificationService obtain the appropriate sender from an injected registry or factory. Client code calls the service with a channel key and notification; it does not contain Email/SMS/Push conditionals. To add Push, implement PushSender and register it during application configuration.

Java examplejava
import java.util.HashMap;
import java.util.Map;

public final class NotificationExample {
    public interface NotificationSender {
        void send(Notification notification);
    }

    public static final class Notification {
        private final String recipient;
        private final String subject;
        private final String body;

        public Notification(String recipient, String subject, String body) {
            this.recipient = recipient;
            this.subject = subject;
            this.body = body;
        }

        public String recipient() {
            return recipient;
        }

        public String subject() {
            return subject;
        }

        public String body() {
            return body;
        }
    }

    public static final class EmailSender implements NotificationSender {
        @Override
        public void send(Notification notification) {
            // Send through an email provider.
        }
    }

    public static final class SmsSender implements NotificationSender {
        @Override
        public void send(Notification notification) {
            // Send through an SMS provider.
        }
    }

    public static final class PushSender implements NotificationSender {
        @Override
        public void send(Notification notification) {
            // Map the notification to a push payload and send it.
        }
    }

    public static final class SenderRegistry {
        private final Map<String, NotificationSender> senders = new HashMap<>();

        public void register(String channelKey, NotificationSender sender) {
            senders.put(channelKey, sender);
        }

        public NotificationSender resolve(String channelKey) {
            NotificationSender sender = senders.get(channelKey);
            if (sender == null) {
                throw new IllegalArgumentException("Unsupported channel: " + channelKey);
            }
            return sender;
        }
    }

    public static final class NotificationService {
        private final SenderRegistry registry;

        public NotificationService(SenderRegistry registry) {
            this.registry = registry;
        }

        public void send(String channelKey, Notification notification) {
            registry.resolve(channelKey).send(notification);
        }
    }

    public static void main(String[] args) {
        SenderRegistry registry = new SenderRegistry();
        registry.register("email", new EmailSender());
        registry.register("sms", new SmsSender());
        registry.register("push", new PushSender());

        NotificationService service = new NotificationService(registry);
        service.send("push", new Notification("recipient", "Title", "Body"));
    }
}
Understand it clearly

Use a common abstraction

Define an interface such as NotificationSender with a send(Notification) method. Each delivery channel implements this interface and owns its transport-specific behavior.

NotificationService depends on the abstraction and delegates sending to the selected implementation. This is a Strategy-style use of polymorphism: the service invokes the same operation regardless of whether the selected sender is email, SMS, or push.

  • EmailSender: Sends notifications through the email transport.
  • SmsSender: Sends notifications through the SMS transport.
  • PushSender: Builds and sends the push-specific payload through the push transport.

Resolve the sender outside client code

Inject a registry, factory, or dependency-injection configuration into NotificationService. A registry can map channel keys such as email, sms, and push to NotificationSender instances.

The client calls NotificationService.send(channelKey, notification). The service resolves the sender and delegates to it, so the client remains independent of concrete channel classes and contains no branching logic.

  • Registry: Appropriate when sender instances are configured at startup and selected by a key.
  • Factory: Useful when sender creation itself depends on configuration or request-specific context.
  • Dependency injection: Can construct and supply the registry or sender implementations during application setup.

Add Push without changing callers

Create PushSender, implement the common interface, and register it under the push key in application wiring. Existing client code and the NotificationService delegation logic do not need to change.

This supports the Open/Closed Principle in practice: behavior is extended by adding a new implementation and configuration rather than editing client-side conditional branches.

  • Validation: The resolver should reject or otherwise handle an unsupported channel key.
  • Channel-specific data: Keep payload mapping, credentials, provider calls, and delivery constraints inside the relevant sender or its collaborators.

Operational boundaries

A shared Notification model should contain only data common to callers. Each sender can translate that model into the format required by its transport. If a channel needs additional data, model that requirement explicitly rather than assuming every channel accepts identical fields.

Retry behavior, observability, and fallback rules should be placed deliberately. A sender may handle transport-level failures, while a separate orchestration policy can decide whether a failed push should trigger another channel.

  • Testing: Unit-test NotificationService with a test registry or test sender, and test each concrete sender independently.
  • Configuration: Inject provider-specific configuration into sender implementations rather than exposing it to clients.
24

A Bird base class defines fly(), but some birds cannot fly. What is wrong with the design, and how would you improve it?

Interview-ready answer

The problem is that flight is modeled as a universal property of Bird, although it is only a capability of some birds. A Penguin is still a valid Bird but cannot honor a fly() contract. This creates an incorrect abstraction and can violate the Liskov Substitution Principle if clients expect every Bird to fly. Remove fly() from the Bird base class. Model flight as an opt-in capability: for example, let flying birds implement a Flyable interface, and make code that requires flight depend on Flyable rather than Bird. If flight behavior must be shared or changed at runtime, use composition with a FlightBehavior strategy, but expose that behavior only on bird types that can fly. Do not use exceptions or no-op implementations to pretend that non-flying birds support flight.

Java examples: capability interface and strategy compositionjava
public final class BirdDesignExample {

    // Approach 1: model flight as an opt-in capability.
    interface Flyable {
        void fly();
    }

    abstract static class Bird {
        private final String name;

        protected Bird(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public void eat() {
            // Common bird behavior.
        }
    }

    static final class Sparrow extends Bird implements Flyable {
        Sparrow(String name) {
            super(name);
        }

        @Override
        public void fly() {
            System.out.println(getName() + " flies.");
        }
    }

    static final class Penguin extends Bird {
        Penguin(String name) {
            super(name);
        }
        // Penguin is a Bird, but it is not Flyable.
    }

    static void letItFly(Flyable bird) {
        bird.fly();
    }

    // Approach 2: compose a flight behavior for bird types that can fly.
    interface FlightBehavior {
        void fly(String birdName);
    }

    static final class WingFlight implements FlightBehavior {
        @Override
        public void fly(String birdName) {
            System.out.println(birdName + " flies using its wings.");
        }
    }

    static final class FlyingBird extends Bird implements Flyable {
        private FlightBehavior flightBehavior;

        FlyingBird(String name, FlightBehavior flightBehavior) {
            super(name);
            this.flightBehavior = flightBehavior;
        }

        @Override
        public void fly() {
            flightBehavior.fly(getName());
        }

        public void setFlightBehavior(FlightBehavior flightBehavior) {
            this.flightBehavior = flightBehavior;
        }
    }
}
Understand it clearly

Why the base design is incorrect

A base class should contain behavior that all of its valid subtypes can support. Declaring fly() on Bird says that every Bird can fly, which is false for birds such as penguins.

A non-flying subclass is then forced into a misleading implementation: it may throw an exception, do nothing, or require callers to check its concrete type before calling fly(). Each outcome weakens the class contract.

  • Incorrect abstraction: Flight is a selective capability, not an essential property of every bird.
  • Substitution problem: A Bird reference cannot safely be treated as flyable when some Bird subtypes cannot fulfill fly().

Relevant design principle

This is primarily an Liskov Substitution Principle issue. If Bird promises fly(), every subtype must preserve that promise. A non-flying bird cannot do so meaningfully.

It can also indicate an overly broad interface: clients that only need common bird behavior should not be coupled to flight. Single Responsibility Principle may support separating flight behavior, but it is not the central problem; the key issue is the invalid base-class contract.

Improved design

Keep Bird focused on behavior shared by all birds, such as eating or sleeping. Represent flight separately, so only birds that can fly expose a flight operation.

A Flyable capability interface is usually the simplest solution. Client code that needs flight should accept Flyable, not Bird. Alternatively, composition with a FlightBehavior strategy is useful when multiple flight implementations are shared or when the behavior must be changed at runtime.

  • Capability interface: Flying bird classes implement Flyable; non-flying bird classes do not.
  • Composition: A flying bird delegates to a FlightBehavior object. Non-flying birds should not expose a fly operation merely to delegate to a failure behavior.
Quick comparison
BasisCapability interfaceFlightBehavior composition
Best useUse when flight is simply an opt-in capability and callers need a compile-time contract.Use when flight implementations are shared, configurable, or changeable at runtime.
Client dependencyClients that need flight depend on Flyable.Clients interact with a flying bird or another object that owns a FlightBehavior.
Non-flying birdsThey do not implement Flyable.They should not be given a public flight operation solely to report that they cannot fly.
25

What is upcasting and downcasting, and why can downcasting be unsafe?

Interview-ready answer

Upcasting means treating an object of a subtype as one of its supertypes, such as assigning a Cat object to an Animal reference. It is normally implicit and safe because the object is already an instance of the supertype. Downcasting means converting a supertype reference back to a subtype reference. It requires an explicit cast because the reference may actually point to a different subtype. In Java, an invalid downcast throws ClassCastException at runtime. Downcasting should be used only when the runtime type is known or checked; otherwise, prefer polymorphism or a better-typed design.

Java example showing upcasting, a valid downcast, and an invalid downcastjava
class Animal {
    void speak() {
        System.out.println("Animal");
    }
}

class Cat extends Animal {
    @Override
    void speak() {
        System.out.println("Meow");
    }

    void purr() {
        System.out.println("Purr");
    }
}

class Dog extends Animal {
    @Override
    void speak() {
        System.out.println("Woof");
    }
}

public class CastDemo {
    public static void main(String[] args) {
        Animal a = new Cat();       // Upcast: Cat to Animal
        a.speak();                  // Prints "Meow"

        if (a instanceof Cat) {
            Cat cat = (Cat) a;      // Valid downcast
            cat.purr();
        }

        Animal b = new Dog();
        Cat wrong = (Cat) b;        // Throws ClassCastException at runtime
    }
}
Understand it clearly

Upcasting

Upcasting assigns or passes a subtype object through a superclass or interface reference. For example, a Cat can be used as an Animal because Cat is an Animal.

Upcasting changes the static type of the reference, not the object itself. The runtime object remains a Cat, so overridden instance methods still use the Cat implementation.

  • Safety: It is safe when the subtype relationship is valid, because every subtype object is also an object of its supertypes.
  • Syntax: In Java, it is implicit; no cast syntax is required.

Downcasting

Downcasting converts a reference whose static type is a superclass or interface into a reference to a more specific subtype. It is used when subtype-specific operations are needed.

The cast is explicit because the compiler cannot generally prove that the referenced runtime object has the requested subtype.

  • Example: A reference declared as Animal can be cast to Cat only if the object it refers to is actually a Cat or a subtype of Cat.

Why downcasting can be unsafe

A supertype reference can refer to many different concrete subtypes. Therefore, a cast to one particular subtype may be incorrect even though the types are related at compile time.

In Java, an invalid checked downcast fails at runtime with ClassCastException. In languages with unchecked casts, the consequences of an invalid cast are language- and implementation-dependent.

  • Runtime mismatch: If an Animal reference points to a Dog, casting it to Cat is invalid.
  • Design coupling: Frequent downcasts make code depend on concrete implementations, which reduces the benefits of polymorphism and can make the code more fragile.

Using downcasts safely

When a downcast is genuinely required, verify the runtime type before casting, such as with instanceof in Java. Prefer defining the needed operation on the supertype or interface when all relevant implementations can support it.

Generics and well-designed APIs can also preserve type information and reduce the need for casts.

  • Runtime check: Use instanceof before a Java downcast when the runtime type is uncertain.
  • Preferred design: Use polymorphism so callers invoke behavior through the common supertype rather than inspecting and casting concrete subtypes.
Quick comparison
BasisUpcastingDowncasting
DirectionSubtype reference to supertype referenceSupertype reference to subtype reference
Cast syntax in JavaImplicitExplicit
Runtime safetySafe for a valid subtype-to-supertype relationshipSafe only when the runtime object is an instance of the target subtype
Failure in JavaDoes not fail because of the upcast itselfInvalid casts throw ClassCastException
Typical purposeUse objects through a common superclass or interfaceAccess subtype-specific behavior when the concrete runtime type is known