🧩

Top 51 OOP Concepts Interview Questions & Answers (2026)

51 Questions 28 Beginner 15 Intermediate 8 Advanced

About OOP Concepts

Top 100 Object-Oriented Programming interview questions covering encapsulation, inheritance, polymorphism, abstraction, SOLID principles, design patterns, and best practices. Companies hiring for OOP Concepts roles test this knowledge at every stage — from first-round screens that check the basics to final-round conversations about real-world trade-offs.

What to Expect in a OOP Concepts Interview

Expect a mix of conceptual and practical OOP Concepts questions: clear definitions and core-concept checks for junior roles, hands-on scenario questions for mid-level roles, and architecture or trade-off discussions for senior roles. Interviewers usually move from foundational topics toward the kind of problems you'd actually face on the job.

How to Use This Guide

Work through the OOP Concepts questions in order — Beginner, then Intermediate, then Advanced — so each concept builds on the last. Every question has its own page; bookmark the ones that trip you up and revisit them the day before your interview.

Curated by Tech Baithak Editorial Team  ·  Last updated: May 2026

Beginner 28 questions

Core concepts every OOP Concepts developer must know.

01

What is Object-Oriented Programming (OOP)?

Object-Oriented Programming (OOP) is a programming paradigm that organizes software design around objects — instances of classes that combine data (attributes/fields) and behavior (methods/functions) into single, cohesive units. OOP emerged in the 1960s with Simula and became mainstream with Smalltalk, C++, Java, Python, and many modern languages. Core idea: instead of thinking about a program as a sequence of instructions (procedural), think of it as a collection of interacting objects, each responsible for its own data and behavior. This mirrors how we conceptualize the real world — a car has properties (color, speed) and behaviors (accelerate, brake). Four pillars: Encapsulation (data + behavior in one unit), Abstraction (hide complexity), Inheritance (reuse code), Polymorphism (one interface, many forms). Benefits: code reusability via inheritance and composition; modularity — changes in one class don't affect others; maintainability — easier to understand and modify; scalability — add new features by extending; real-world modeling — concepts map naturally to code. Contrast with procedural: procedural programs have data and procedures separately; OOP bundles them. OOP excels for large, complex systems with many interacting components. Pure OOP languages: Smalltalk, Ruby (everything is an object). Multi-paradigm: Python, Java, C++, C# — support OOP plus other paradigms.

Open this question on its own page
02

What is a class?

A class is a blueprint, template, or prototype from which objects are created. It defines the structure and behavior that its instances (objects) will have — specifying what data they hold (attributes/fields) and what operations they can perform (methods). Analogy: a class is like an architectural blueprint for a house; objects are the actual houses built from that blueprint. Many different houses (objects) can be built from the same blueprint (class). Example (Java): public class BankAccount { // Attributes (state): private String accountNumber; private String owner; private double balance; // Constructor: public BankAccount(String accountNumber, String owner, double initialBalance) { this.accountNumber = accountNumber; this.owner = owner; this.balance = initialBalance; } // Methods (behavior): public void deposit(double amount) { if (amount > 0) { balance += amount; } } public boolean withdraw(double amount) { if (amount > 0 && balance >= amount) { balance -= amount; return true; } return false; } public double getBalance() { return balance; } public String toString() { return "Account[" + accountNumber + ", " + owner + ", " + balance + "]"; } }. Class components: attributes/fields (data/state), constructors (initialization), methods (behavior), access modifiers (visibility), static members (shared across all instances). Class vs object: class = definition; object = instance. BankAccount account = new BankAccount("1234", "Alice", 1000.0);

Open this question on its own page
03

What is an object?

An object is an instance of a class — a specific realization of the class blueprint created in memory at runtime. Each object has its own copy of the class's instance variables (state) and shares the class's methods (behavior). Creating objects: // From the BankAccount class: BankAccount aliceAccount = new BankAccount("1001", "Alice", 5000.0); BankAccount bobAccount = new BankAccount("1002", "Bob", 2500.0); // Each object is independent: aliceAccount.deposit(1000); // Only Alice's account changes System.out.println(aliceAccount.getBalance()); // 6000.0 System.out.println(bobAccount.getBalance()); // 2500.0 (unchanged). Object identity (reference) vs equality (value): BankAccount a1 = new BankAccount("1001", "Alice", 1000); BankAccount a2 = new BankAccount("1001", "Alice", 1000); // Same data, different objects a1 == a2 // false -- different references in memory a1.equals(a2) // true if equals() is overridden to compare content. Object state: the current values of all its attributes. State can change over time through method calls. Object behavior: methods define what the object can do. Object identity: each object has a unique identity (memory address). Encapsulation in objects: the object's internal state is protected — only changed through its methods (if fields are private). null: a special reference meaning "no object" — dereferencing null throws NullPointerException (Java) or causes undefined behavior (C++).

Open this question on its own page
04

What is encapsulation?

Encapsulation is the bundling of data (attributes) and the methods that operate on that data within a single unit (class), and restricting direct access to some of the object's components. It hides the internal state and requires all interaction to occur through well-defined interfaces (public methods). Information hiding: internal implementation details are hidden from outside. External code sees only what's necessary (the public interface). Example (Java): public class Temperature { private double celsius; // Hidden -- can't access directly from outside // Controlled access via methods: public void setCelsius(double temp) { if (temp < -273.15) { // Absolute zero validation throw new IllegalArgumentException("Temperature below absolute zero!"); } this.celsius = temp; } public double getCelsius() { return celsius; } public double getFahrenheit() { // Derived property -- not stored return celsius * 9.0/5.0 + 32; } }. Benefits of encapsulation: (1) Data protection: prevents invalid state — validation in setters ensures invariants are maintained; (2) Flexibility: can change internal representation without affecting users of the class; (3) Reduced complexity: users don't need to understand implementation; (4) Easier maintenance: changes are localized within the class. Access modifiers for encapsulation: private (class only), protected (class + subclasses), package-private/internal (same package/module), public (everywhere). Getter/setter (accessor/mutator) pattern: private fields + public getters (and optionally setters). Avoid "anemic" classes that are just bags of getters/setters — put behavior where the data is.

Open this question on its own page
05

What is abstraction in OOP?

Abstraction means showing only the essential features of an object while hiding the unnecessary implementation details. It focuses on WHAT an object does rather than HOW it does it — providing a simplified interface to complex underlying code. Real-world analogy: a car's steering wheel abstracts complex steering mechanisms; you don't need to know about power steering hydraulics to drive. A TV remote abstracts the complex electronics — you just press buttons. In OOP, abstraction is achieved through: (1) Abstract classes: cannot be instantiated; define partial implementation with abstract methods that subclasses must implement: abstract class Shape { protected String color; // Constructor: public Shape(String color) { this.color = color; } // Abstract method -- no implementation: public abstract double area(); public abstract double perimeter(); // Concrete method -- shared implementation: public void displayInfo() { System.out.println("Shape: " + getClass().getSimpleName() + ", Color: " + color + ", Area: " + area()); } }; (2) Interfaces: pure abstraction — only method signatures, no implementation (in Java before default methods): interface Printable { void print(); void printToFile(String path); } class Document implements Printable { public void print() { /* implementation hidden */ } public void printToFile(String path) { /* implementation hidden */ } }. Difference from encapsulation: Encapsulation = hiding data and bundling; Abstraction = hiding complexity and showing only interface. They complement each other but are distinct concepts.

Open this question on its own page
06

What is inheritance in OOP?

Inheritance is a mechanism where a new class (subclass/derived/child) acquires the properties and behaviors of an existing class (superclass/base/parent). It models an "is-a" relationship and enables code reuse. Types of inheritance: (1) Single: one class inherits from one parent (Java supports this); (2) Multiple: one class inherits from multiple parents (C++, Python support this; Java uses interfaces instead); (3) Multilevel: A → B → C (C inherits from B which inherits from A); (4) Hierarchical: multiple classes inherit from the same parent; (5) Hybrid: combination. Example (Java): class Vehicle { protected String brand; protected int speed; public Vehicle(String brand) { this.brand = brand; this.speed = 0; } public void accelerate(int amount) { this.speed += amount; } public void brake(int amount) { this.speed = Math.max(0, speed - amount); } } class Car extends Vehicle { private int doors; public Car(String brand, int doors) { super(brand); // Call parent constructor this.doors = doors; } @Override public String toString() { return brand + " (Car, " + doors + " doors) at " + speed + " km/h"; } } class Motorcycle extends Vehicle { private boolean hasSidecar; public Motorcycle(String brand, boolean hasSidecar) { super(brand); this.hasSidecar = hasSidecar; } }. super keyword: call parent constructor or method. Method overriding: redefine inherited method in child class. When NOT to use inheritance: "is-a" must be true. Dog is an Animal ✓. Car is an Engine ✗ (use composition). Avoid deep inheritance hierarchies (>3 levels) — brittle.

Open this question on its own page
07

What is polymorphism?

Polymorphism (Greek: "many forms") allows objects of different types to be treated through a common interface. The correct method is selected at runtime based on the actual object type. "One interface, multiple implementations." Types: (1) Runtime polymorphism (dynamic dispatch/method overriding): achieved through inheritance and virtual methods. The actual method called is determined at runtime: abstract class Animal { public abstract void speak(); } class Dog extends Animal { public void speak() { System.out.println("Woof!"); } } class Cat extends Animal { public void speak() { System.out.println("Meow!"); } } class Duck extends Animal { public void speak() { System.out.println("Quack!"); } } // Polymorphic code: List<Animal> animals = new ArrayList<>(); animals.add(new Dog()); animals.add(new Cat()); animals.add(new Duck()); for (Animal a : animals) { a.speak(); // Correct method called for each actual type! } // Woof! Meow! Quack!; (2) Compile-time polymorphism (method overloading / ad-hoc polymorphism): multiple methods with same name, different parameters: class Calculator { public int add(int a, int b) { return a + b; } public double add(double a, double b) { return a + b; } public int add(int a, int b, int c) { return a + b + c; } } // Compiler selects correct version based on argument types.. Parametric polymorphism (generics/templates): List<Integer> ints; List<String> strings;. Polymorphism enables the Open/Closed Principle — add new types without changing existing code that processes them.

Open this question on its own page
08

What is the difference between overloading and overriding?

Method Overloading and Method Overriding are both ways to have multiple methods with the same name, but they serve different purposes: Overloading (compile-time polymorphism): defining multiple methods with the SAME name but DIFFERENT parameter lists within the SAME class. The compiler selects the right method based on argument types and count: class MathHelper { public int multiply(int a, int b) { return a * b; } public double multiply(double a, double b) { return a * b; } public int multiply(int a, int b, int c) { return a * b * c; } // INVALID -- only return type differs (not allowed!): // public long multiply(int a, int b) { return (long)a * b; } } MathHelper m = new MathHelper(); m.multiply(2, 3); // int version m.multiply(2.0, 3.0); // double version m.multiply(2, 3, 4); // 3-param version. Overriding (runtime polymorphism): redefining a method from a PARENT class in a CHILD class with the SAME name and SAME parameter list: class Logger { public void log(String message) { System.out.println("[INFO] " + message); } } class DebugLogger extends Logger { @Override // Annotation ensures proper override -- compiler error if wrong public void log(String message) { System.out.println("[DEBUG] " + Thread.currentThread().getName() + ": " + message); } } Logger logger = new DebugLogger(); logger.log("test"); // Calls DebugLogger.log() -- runtime dispatch. Key differences: Overloading = same class, different signature, compile-time; Overriding = parent-child, same signature, runtime. @Override annotation (Java) catches errors where you think you're overriding but aren't.

Open this question on its own page
09

What is a constructor?

A constructor is a special method that is automatically called when an object is created. It initializes the object's state (attributes) with valid initial values. Characteristics: has the same name as the class; no return type (not even void); automatically called with new; can have parameters; can be overloaded. Types of constructors: (1) Default constructor: no parameters — provided by the compiler if no constructor is defined: class Point { int x, y; public Point() { this.x = 0; this.y = 0; } // Explicit default }; (2) Parameterized constructor: public Point(int x, int y) { this.x = x; this.y = y; }; (3) Copy constructor: initializes from another instance: public Point(Point other) { this.x = other.x; this.y = other.y; }; (4) Delegating constructor (this()): one constructor calls another: public Point() { this(0, 0); } // Calls parameterized constructor; (5) Static factory methods (alternative): public static Point origin() { return new Point(0, 0); } public static Point fromCoordinates(int x, int y) { return new Point(x, y); }. Constructor vs method: constructor creates and initializes; method operates on existing objects. Inheritance and constructors: child class constructor MUST call parent constructor (explicitly via super() or implicitly via default parent constructor). Constructors are NOT inherited but can be chained. Destructor/finalizer: some languages have destructors (C++ ~ destructor, Java finalize()) called on destruction. In Java, prefer try-with-resources over finalize.

Open this question on its own page
10

What is the difference between an interface and an abstract class?

Both define contracts and cannot be instantiated directly, but they serve different purposes with key differences: Abstract class: can have both abstract (unimplemented) and concrete (implemented) methods; can have instance variables with state; can have constructors; a class can extend only ONE abstract class (single inheritance in Java/C#); use when sharing code among closely related classes: abstract class Animal { protected String name; // State public Animal(String name) { this.name = name; } // Concrete method -- shared behavior: public void breathe() { System.out.println(name + " breathes"); } // Abstract -- each animal speaks differently: public abstract void speak(); } class Dog extends Animal { public Dog(String name) { super(name); } public void speak() { System.out.println(name + " says Woof!"); } }. Interface: all methods are public and abstract by default (before Java 8 default methods); can only have constants (public static final fields — no instance variables); no constructors; a class can implement MULTIPLE interfaces; models "can-do" capabilities across unrelated classes: interface Flyable { void fly(); default void land() { System.out.println("Landing..."); } } interface Swimmable { void swim(); } class Duck extends Animal implements Flyable, Swimmable { public Duck(String name) { super(name); } public void speak() { System.out.println(name + " quacks"); } public void fly() { System.out.println(name + " flies"); } public void swim() { System.out.println(name + " swims"); } }. Rule of thumb: Abstract class = "is-a" relationship, shared implementation; Interface = "can-do" capability, contract definition.

Open this question on its own page
11

What is the "this" keyword?

The this keyword refers to the current instance of the class — the object on which the method is being called. It resolves ambiguity, enables method chaining, and can pass the current object to other methods. Uses of this: (1) Disambiguate field from parameter: class Person { private String name; private int age; public Person(String name, int age) { this.name = name; // this.name = field, name = parameter this.age = age; } }; (2) Call another constructor (constructor chaining): class Rectangle { int width, height, depth; public Rectangle(int width, int height) { this.width = width; this.height = height; } public Rectangle(int width, int height, int depth) { this(width, height); // Calls 2-param constructor this.depth = depth; } }; (3) Pass current object to a method: class EventSource { public void register(EventListener listener) { listener.setSource(this); // Pass this object } }; (4) Return current object (method chaining/fluent API): class QueryBuilder { private String table, condition, orderBy; public QueryBuilder from(String table) { this.table = table; return this; // Return this for chaining } public QueryBuilder where(String condition) { this.condition = condition; return this; } public QueryBuilder orderBy(String field) { this.orderBy = field; return this; } } // Usage: QueryBuilder qb = new QueryBuilder() .from("users") .where("active = true") .orderBy("name");. In Python, the equivalent is self (explicit first parameter). In JavaScript, this depends on how the function is called (more complex).

Open this question on its own page
12

What is the difference between composition and inheritance?

Inheritance ("is-a" relationship) creates a hierarchical relationship where a subclass inherits from a parent. Composition ("has-a" relationship) builds complex objects by containing references to simpler objects. Inheritance example: class Vehicle { void startEngine() { /* ... */ } } class Car extends Vehicle { // Car IS-A Vehicle } // Problem: what if Car needs GPS, which isn't a Vehicle? // Can't inherit from both Vehicle and GPSDevice. Composition example: class Engine { void start() { System.out.println("Engine started"); } void stop() { System.out.println("Engine stopped"); } } class GPS { String getLocation() { return "Lat: 40.7128, Long: -74.0060"; } } class Car { private Engine engine; // Car HAS-A Engine private GPS gps; // Car HAS-A GPS public Car() { this.engine = new Engine(); this.gps = new GPS(); } public void start() { engine.start(); } public String navigate() { return gps.getLocation(); } }. Why "Favor composition over inheritance" (GoF principle): (1) Composition is more flexible — can change components at runtime; (2) No fragile base class problem — parent changes don't unexpectedly break children; (3) Avoids deep inheritance hierarchies (hard to understand, brittle); (4) Easier to test — inject mock components; (5) Better encapsulation — don't expose parent's protected internals. When to use inheritance: genuine "is-a" relationship, need polymorphism, sharing a common interface, when subclass truly IS a specialization of parent. The Liskov Substitution Principle (LSP) is the litmus test — if child can't be used everywhere parent is, don't inherit.

Open this question on its own page
13

What is method overriding?

Method overriding allows a subclass to provide a specific implementation for a method that is already defined in its parent class. The subclass method has the same name, return type, and parameter list as the parent method. Rules: method must have same name and signature; return type must be the same or a covariant (subtype of parent's return type); access modifier can be same or more permissive (can't restrict access); cannot override static methods (they can be hidden, not overridden). Example: class Payment { public String processPayment(double amount) { return "Processing payment of $" + amount; } public double calculateFee(double amount) { return amount * 0.02; // 2% fee } } class CreditCardPayment extends Payment { @Override public String processPayment(double amount) { // Custom implementation double fee = calculateFee(amount); return "Processing credit card payment of $" + amount + " (Fee: $" + fee + ")"; } @Override public double calculateFee(double amount) { return amount * 0.03; // Credit cards: 3% fee } } class PayPalPayment extends Payment { @Override public String processPayment(double amount) { return "Processing PayPal payment of $" + amount; } // No override of calculateFee -- uses parent's 2% } // Polymorphism: Payment p = new CreditCardPayment(); p.processPayment(100); // Calls CreditCardPayment's version p.calculateFee(100); // Returns 3.0. super in override: call parent's original method from override: @Override public String processPayment(double amount) { String parentResult = super.processPayment(amount); return parentResult + " [Credit Card]"; }. final methods: cannot be overridden — use for security or performance. @Override annotation verifies override is correct at compile time — always use it.

Open this question on its own page
14

What are access modifiers?

Access modifiers control the visibility and accessibility of classes, methods, and fields. They are the primary mechanism for enforcing encapsulation. Java's four access levels (most to least restrictive): (1) private: accessible only within the same class. Not accessible from subclasses or other classes: private double balance; // Only BankAccount can access private void updateLedger() {}; (2) package-private (default, no keyword): accessible within the same package — the default when no modifier is specified: double interestRate; // Any class in same package void calculateInterest() {}; (3) protected: accessible within the same package AND by subclasses (even in different packages): protected String ownerName; // Accessible by subclasses protected void sendAlert() {}; (4) public: accessible from anywhere — any class in any package: public String getAccountNumber() { return accountNumber; } public void deposit(double amount) {}. Visibility matrix: private < default (package) < protected < public. Best practices: fields should almost always be private; getters/setters can be public if needed; helper methods should be private; constructor visibility depends on design (private for Singleton, public for normal, protected for abstract). Other OOP languages: C# has internal (assembly-wide), protected internal; Swift has fileprivate, internal (module); Python uses naming convention (_single = protected, __double = private via name mangling — not enforced).

Open this question on its own page
15

What is a static method and a static variable?

Static members belong to the CLASS rather than any specific instance (object). All instances share the same static member, and static members exist even when no instances have been created. Static variable (class variable): class Student { private String name; private int grade; // Static -- shared by ALL Student instances: private static int totalStudents = 0; private static final int MAX_GRADE = 100; // Constant public Student(String name, int grade) { this.name = name; this.grade = Math.min(grade, MAX_GRADE); totalStudents++; // Increment shared counter } public static int getTotalStudents() { return totalStudents; } }. Static method (class method): belongs to the class; can only access other static members directly (no access to instance fields/this); called on the class, not an object: Student s1 = new Student("Alice", 90); Student s2 = new Student("Bob", 85); Student.getTotalStudents(); // 2 -- called on class // s1.getTotalStudents(); // Works but misleading -- don't do this. Common uses: utility/helper methods (Math.sqrt(), Collections.sort()); factory methods (getInstance()); constants (Math.PI); counters/registries shared across all instances; Singleton patterns. Static initializer block: static { // Runs once when class is first loaded totalStudents = loadFromDatabase(); }. Cannot: use this or super in static methods; override static methods (they are hidden, not overridden); access non-static members without an instance reference.

Open this question on its own page
16

What is the difference between stack and heap memory in OOP?

In OOP languages, objects are primarily stored in the heap, while primitive values and references live on the stack. Understanding this explains how objects are managed: Stack memory: fast, LIFO allocation; stores: local variables, method call frames, primitive values, object references (not the objects themselves); automatically managed — freed when method returns; limited size (stack overflow on deep recursion). Heap memory: slower, dynamically managed; stores: actual object instances; managed by garbage collector (Java/C#/Python) or manually (C++) or by ARC (Swift); larger than stack; where all new allocations go. How objects work: // In Java: void processOrder() { int amount = 100; // Stack: amount=100 Order order = new Order(); // Stack: order=reference; Heap: actual Order object order.setAmount(amount); // Heap object is modified // When method returns: // Stack frame removed -- amount gone, order reference gone // Heap object remains until GC collects it }. Garbage collection: Java, C#, Python automatically reclaim heap memory when objects are no longer referenced. GC tracks references — when count reaches zero, memory is freed. Memory leak: when objects remain referenced (preventing GC) but are no longer needed. Common cause: event listeners not removed, static collections growing unbounded, long-lived objects holding references to short-lived ones. Stack overflow: infinite recursion exceeds stack size — stack frames can't be allocated. OutOfMemoryError: heap is full — GC can't reclaim enough memory. In Java, heap size configurable: -Xmx2g.

Open this question on its own page
17

What is the difference between == and .equals() in Java?

This is a fundamental Java gotcha: == operator: compares REFERENCES — checks if two variables point to the SAME object in memory (same memory address). For primitives, compares values. String s1 = new String("hello"); String s2 = new String("hello"); System.out.println(s1 == s2); // false -- different objects in heap System.out.println(s1 == s1); // true -- same reference // Primitives: int a = 5, b = 5; System.out.println(a == b); // true -- value comparison for primitives. .equals() method: compares CONTENT — what the objects contain. Object's default equals() uses == (reference equality). You MUST override it for content equality: System.out.println(s1.equals(s2)); // true -- same content! String literal optimization: String s3 = "hello"; String s4 = "hello"; // Same object from string pool: System.out.println(s3 == s4); // true (string pool) System.out.println(s3.equals(s4)); // true. Overriding equals() correctly: public class Point { int x, y; @Override public boolean equals(Object obj) { if (this == obj) return true; // Optimization: same reference if (obj == null || getClass() != obj.getClass()) return false; Point other = (Point) obj; return this.x == other.x && this.y == other.y; } @Override public int hashCode() { // MUST override hashCode when overriding equals! return Objects.hash(x, y); } }. hashCode contract: if a.equals(b), then a.hashCode() MUST equal b.hashCode(). This ensures correct behavior in HashMap, HashSet, etc. null safety: "expected".equals(variable) instead of variable.equals("expected") — avoids NullPointerException when variable is null.

Open this question on its own page
18

What is a final keyword in Java?

The final keyword in Java prevents modification, depending on what it's applied to: (1) Final variable (constant): can only be assigned once — cannot be reassigned: final int MAX_SIZE = 100; MAX_SIZE = 200; // Compile error! // Instance final (must be initialized in constructor): class Circle { private final double radius; public Circle(double radius) { this.radius = radius; } // radius can never change after construction }; (2) Final method: cannot be overridden by subclasses: class BankAccount { final void close() { // Security: can't override this critical operation this.status = "CLOSED"; saveToAuditLog(); } } class SavingsAccount extends BankAccount { // Can't override close() -- compile error if tried }; (3) Final class: cannot be extended/subclassed: final class ImmutablePoint { private final int x, y; public ImmutablePoint(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } // No setters! } // class ExtendedPoint extends ImmutablePoint {} // Error!. Examples: String, Integer, all wrapper classes in Java are final. Final fields for immutability: a class is truly immutable if all fields are final AND the referenced objects are also immutable (deep immutability). Immutable objects are thread-safe by design. Effectively final (Java 8+): local variables used in lambdas must be final or effectively final (never reassigned): int count = 0; // Effectively final list.forEach(item -> System.out.println(count)); // OK count++; // Now NOT effectively final -- lambda use would fail.

Open this question on its own page
19

What is object cloning?

Object cloning creates a copy of an existing object. Two types with very different semantics: Shallow copy: creates a new object with the same field values. Primitive fields are copied by value. Reference fields — only the reference is copied (both original and copy point to the SAME nested objects): class Address { String city; String street; } class Person { String name; Address address; } Person original = new Person(); original.name = "Alice"; original.address = new Address("New York", "5th Ave"); // Shallow copy: Person copy = (Person) original.clone(); // Works if Person implements Cloneable copy.name = "Bob"; // Fine -- primitives/Strings are independent original.name; // Still "Alice" copy.address.city = "London"; // Danger! original.address.city; // Also "London"! Same Address object!. Deep copy: creates a new object AND recursively copies all nested objects — fully independent: class Person implements Cloneable { String name; Address address; @Override protected Person clone() throws CloneNotSupportedException { Person cloned = (Person) super.clone(); // Deep copy of address: cloned.address = new Address(this.address.city, this.address.street); return cloned; } } // Alternatively, deep copy via serialization or copy constructor.. Java Cloneable interface: marker interface — must be implemented for clone() to work; calling clone() on a non-Cloneable object throws CloneNotSupportedException. Many consider Java's Cloneable design flawed (effective Java recommends copy constructors or static factories instead). Copy constructor (preferred): public Person(Person other) { this.name = other.name; this.address = new Address(other.address); }.

Open this question on its own page
20

What is toString() and why override it?

The toString() method returns a string representation of an object. It's defined in the Object class (the root of all Java classes) and is automatically called when you print an object or concatenate it with a string. Default behavior: class Point { int x, y; } Point p = new Point(); System.out.println(p); // Prints: "Point@1b6d3586" -- useless! // ClassName@hexHashCode -- not meaningful for debugging. Overriding toString(): class Point { int x, y; public Point(int x, int y) { this.x = x; this.y = y; } @Override public String toString() { return "Point(" + x + ", " + y + ")"; } } Point p = new Point(3, 4); System.out.println(p); // "Point(3, 4)" -- meaningful! System.out.println("Location: " + p); // Auto-calls toString() logger.debug("Current position: {}", p); // Also calls toString(). Why override: (1) Debugging — immediately see object state in logs and debugger; (2) Logging — meaningful log messages; (3) Error messages — describe objects in exceptions; (4) Display — show object to users. Best practices for toString(): include the most important fields; don't include sensitive data (passwords, credit card numbers); be concise but informative; use established formats: @Override public String toString() { return "User{id=" + id + ", name='" + name + "', email='" + email + "'}"; }. Libraries: Lombok @ToString, Apache Commons ToStringBuilder, Google Guava Objects.toStringHelper(). @Override annotation ensures you're actually overriding, not creating a new method.

Open this question on its own page
21

What is type casting in OOP?

Type casting converts an object reference from one type to another. In OOP, this is primarily relevant for class hierarchies. Two directions: Upcasting (implicit, always safe): cast a subclass reference to a superclass type. No explicit cast needed. Always succeeds — subclass IS-A superclass: Dog myDog = new Dog(); Animal animal = myDog; // Upcast -- implicit and safe animal.speak(); // Calls Dog's speak() via polymorphism animal.fetch(); // ERROR! -- Animal type doesn't have fetch() even though object is a Dog. Downcasting (explicit, potentially unsafe): cast a superclass reference to a subclass type. Must be explicit. Fails with ClassCastException if the object isn't actually that type: Animal animal = new Dog(); // We know it's a Dog Dog dog = (Dog) animal; // Explicit downcast -- works dog.fetch(); // Now we can call Dog-specific methods // Dangerous: Animal animal2 = new Cat(); Dog dog2 = (Dog) animal2; // ClassCastException at runtime!. instanceof operator (safe check before downcast): Animal animal = getAnimal(); if (animal instanceof Dog) { Dog dog = (Dog) animal; // Safe! dog.fetch(); } // Modern Java 16+ pattern matching: if (animal instanceof Dog dog) { // No separate cast! dog.fetch(); }. Python duck typing: no type casting needed — if an object has the method, it works regardless of declared type. Design note: frequent downcasting suggests a design smell — often indicates missing polymorphism. Consider adding a method to the base class or using the Visitor pattern.

Open this question on its own page
22

What is method hiding vs method overriding?

Both define a method in a subclass with the same signature as a parent class method, but they behave very differently: Method overriding (instance methods): replaces the parent's method. The correct version is determined at RUNTIME based on the actual object type — true polymorphism: class Parent { public void display() { System.out.println("Parent display"); } } class Child extends Parent { @Override public void display() { System.out.println("Child display"); } } Parent obj = new Child(); obj.display(); // "Child display" -- RUNTIME decision. Method hiding (static methods): the child class defines a static method with the same signature. No overriding — the version called is determined at COMPILE TIME based on the reference type: class Parent { public static void staticMethod() { System.out.println("Parent static"); } } class Child extends Parent { public static void staticMethod() { System.out.println("Child static"); // HIDES, doesn't override } } Parent p = new Child(); p.staticMethod(); // "Parent static" -- compile-time based on reference type! Child c = new Child(); c.staticMethod(); // "Child static" -- based on reference type. Why this distinction matters: static methods are resolved at compile time; instance methods are resolved at runtime. "Overriding" static methods is a common misconception — you can hide them but not override. The @Override annotation would fail on a static method in Java. Field hiding: similar — subclass fields with same name HIDE parent fields, not override. Access: parent.field vs child.field — type of reference determines which field is accessed.

Open this question on its own page
23

What is cohesion and coupling in OOP?

Cohesion and coupling are fundamental design quality metrics that measure how well-organized your code is: Cohesion measures how strongly related and focused the responsibilities of a class are. High cohesion = a class has one, well-defined responsibility. Low cohesion = a class does too many unrelated things. High cohesion (good): class EmailValidator { public boolean isValidFormat(String email) { /* regex */ } public boolean domainExists(String email) { /* DNS lookup */ } public boolean isNotDisposable(String email) { /* check list */ } } // All methods relate to email validation -- single purpose. Low cohesion (bad): class Utility { public boolean validateEmail(String email) {} public int calculateTax(double price) {} public void sendNotification() {} public File parseCSV(String path) {} // Unrelated responsibilities! }. Coupling measures the degree of dependency between classes. Low coupling = classes are independent. High coupling = changes in one class cascade to others. Tight coupling (bad): class UserService { private MySQLDatabase db = new MySQLDatabase(); // Hardcoded dependency -- can't switch to PostgreSQL or mock! }. Loose coupling (good): class UserService { private Database db; // Depends on abstraction (interface) public UserService(Database db) { this.db = db; } // Can inject any Database implementation }. Goal: high cohesion + low coupling = modular, maintainable, testable code. Each class has one reason to change and is independent of others' implementation details.

Open this question on its own page
24

What are design patterns in OOP?

Design patterns are proven, reusable solutions to commonly occurring problems in software design. They are general templates, not specific code — they must be adapted to each situation. Documented by the "Gang of Four" (GoF) in their 1994 book. Three categories: (1) Creational patterns — deal with object creation mechanisms: Singleton: ensure only one instance; Factory Method: define interface for object creation; Abstract Factory: create families of related objects; Builder: construct complex objects step by step; Prototype: copy existing objects; (2) Structural patterns — deal with object composition: Adapter: make incompatible interfaces work together; Decorator: add behavior dynamically; Facade: simplified interface to complex subsystem; Composite: tree structure of objects; Bridge: separate abstraction from implementation; Proxy: placeholder for another object; (3) Behavioral patterns — deal with communication between objects: Observer: notify dependents of state changes; Strategy: define family of algorithms, make them interchangeable; Command: encapsulate request as object; Iterator: traverse collection without exposing structure; Template Method: define algorithm skeleton, let subclasses fill in steps; State: object changes behavior with state; Chain of Responsibility. Why use patterns? Shared vocabulary, proven solutions, faster communication ("use Observer here"), avoid reinventing the wheel, flexible extensible code.

Open this question on its own page
25

What is the Singleton pattern?

The Singleton pattern ensures that a class has only ONE instance and provides a global access point to it. Classic thread-safe implementation (Java): public class DatabaseConnection { // Volatile ensures visibility across threads: private static volatile DatabaseConnection instance; private Connection connection; // Private constructor -- prevent external instantiation: private DatabaseConnection() { this.connection = DriverManager.getConnection(DB_URL); } // Thread-safe lazy initialization (double-checked locking): public static DatabaseConnection getInstance() { if (instance == null) { // First check (no lock) synchronized (DatabaseConnection.class) { if (instance == null) { // Second check (with lock) instance = new DatabaseConnection(); } } } return instance; } public Connection getConnection() { return connection; } }. Bill Pugh (enum) implementation (best for Java): public enum Singleton { INSTANCE; private final ResourcePool pool = new ResourcePool(); public void doWork() { pool.process(); } } // Usage: Singleton.INSTANCE.doWork();. Use cases: database connection pool, configuration manager, logger, thread pool, device driver management. Problems with Singleton: hard to test (can't easily mock); tightly coupled; hides dependencies; thread-safety must be carefully managed; violates Single Responsibility Principle (manages own creation + its main job). Dependency injection alternative: create one instance, inject it everywhere — testable and decoupled, without the Singleton pattern's drawbacks.

Open this question on its own page
26

What is the Factory pattern?

The Factory pattern provides an interface for creating objects without specifying the exact class to be instantiated. It delegates object creation to subclasses or factory methods, enabling loose coupling. Simple Factory (not a GoF pattern but common): class ShapeFactory { public static Shape createShape(String type) { switch (type.toLowerCase()) { case "circle": return new Circle(); case "square": return new Square(); case "triangle": return new Triangle(); default: throw new IllegalArgumentException("Unknown shape: " + type); } } } // Client code: Shape shape = ShapeFactory.createShape("circle"); shape.draw(); // Doesn't know or care which class is instantiated. Factory Method pattern (GoF): define the interface in base class, let subclasses decide which class to instantiate: abstract class Dialog { // Factory method: public abstract Button createButton(); // Template method uses factory method: public void render() { Button btn = createButton(); // Which Button? Decided by subclass btn.onClick(() -> closeDialog()); btn.render(); } } class WindowsDialog extends Dialog { @Override public Button createButton() { return new WindowsButton(); } } class WebDialog extends Dialog { @Override public Button createButton() { return new HTMLButton(); } } // Client: Dialog dialog = new WindowsDialog(); dialog.render(); // Uses WindowsButton without knowing it. Abstract Factory: create families of related objects: interface GUIFactory { Button createButton(); Checkbox createCheckbox(); } class WindowsFactory implements GUIFactory { ... } class MacFactory implements GUIFactory { ... }. Benefits: Single Responsibility (creation in one place), Open/Closed Principle (add new products without changing existing code), loose coupling (client doesn't depend on concrete classes).

Open this question on its own page
27

What is method chaining?

Method chaining is a technique where multiple methods are called on the same object in a single statement, each method returning this (or a new object). This creates a fluent interface that reads like a sentence. Builder pattern with method chaining: class QueryBuilder { private String table; private List<String> columns = new ArrayList<>(); private String whereClause; private String orderByClause; private Integer limit; public QueryBuilder select(String... columns) { this.columns.addAll(Arrays.asList(columns)); return this; // Return this for chaining } public QueryBuilder from(String table) { this.table = table; return this; } public QueryBuilder where(String condition) { this.whereClause = condition; return this; } public QueryBuilder orderBy(String column) { this.orderByClause = column; return this; } public QueryBuilder limit(int n) { this.limit = n; return this; } public String build() { return "SELECT " + String.join(", ", columns) + " FROM " + table + (whereClause != null ? " WHERE " + whereClause : "") + (orderByClause != null ? " ORDER BY " + orderByClause : "") + (limit != null ? " LIMIT " + limit : ""); } } // Usage -- reads like SQL! String query = new QueryBuilder() .select("id", "name", "email") .from("users") .where("active = true") .orderBy("name") .limit(10) .build();. Real examples: Java Stream API (.filter().map().collect()), StringBuilder, Lombok @Builder, most SQL query builders (Hibernate Criteria API, jOOQ), HTTP client builders. Benefits: readable code, less repetition, fluent API design. Immutable chaining: each call returns a new object (functional style) — for immutable types.

Open this question on its own page
28

What is the Iterator pattern?

The Iterator pattern provides a standard way to sequentially access elements of a collection without exposing the underlying representation. It decouples traversal logic from the collection itself. Key interfaces (Java): interface Iterator<E> { boolean hasNext(); // Does another element exist? E next(); // Get next element, advance pointer } interface Iterable<E> { Iterator<E> iterator(); // Return an iterator }. Custom collection with iterator: class NumberRange implements Iterable<Integer> { private int start; private int end; public NumberRange(int start, int end) { this.start = start; this.end = end; } @Override public Iterator<Integer> iterator() { return new Iterator<Integer>() { int current = start; @Override public boolean hasNext() { return current <= end; } @Override public Integer next() { if (!hasNext()) throw new NoSuchElementException(); return current++; } }; } } // Usage with for-each (uses Iterable): for (int n : new NumberRange(1, 5)) { System.out.println(n); // 1 2 3 4 5 } // Explicit iterator: Iterator<Integer> it = range.iterator(); while (it.hasNext()) { System.out.println(it.next()); }. Real-world uses: all Java collections (List, Set, Map.entrySet()) are Iterable. Allows for-each loops on custom classes. Java streams build on iterators. External vs Internal iterator: external (client controls) vs internal (collection controls, lambda): list.forEach(item -> process(item)); // Internal. Python implements with __iter__ and __next__. C# uses IEnumerable and yield return.

Open this question on its own page
Intermediate 15 questions

Practical knowledge for developers with hands-on experience.

01

What are the SOLID principles?

SOLID is an acronym for five object-oriented design principles that promote maintainable, extensible, and understandable code: S — Single Responsibility Principle: a class should have only ONE reason to change. Each class should do one thing well. // Bad: class UserManager handles auth, email, DB, logging // Good: UserAuthenticator, UserEmailSender, UserRepository (separate classes). O — Open/Closed Principle: classes should be OPEN for extension but CLOSED for modification. Add new behavior without changing existing code — use abstraction, polymorphism: // Bad: add new payment type by modifying PaymentProcessor switch statement // Good: PaymentProcessor takes PaymentStrategy; add StripeStrategy without touching PaymentProcessor. L — Liskov Substitution Principle: derived classes must be substitutable for their base classes without altering program correctness. Subclasses must honor the contract of the base: // Bad: Square extends Rectangle but breaks setWidth() behavior (square must keep equal sides) // Good: separate Shape hierarchy, or use composition. I — Interface Segregation Principle: clients should not depend on interfaces they don't use. Prefer small, specific interfaces over fat ones: // Bad: Animal interface with fly(), swim(), walk(), bark() // Good: Flyable, Swimmable, Walkable, Barkable (separate interfaces). D — Dependency Inversion Principle: high-level modules should not depend on low-level modules — both should depend on abstractions. Inject dependencies via interfaces: // Bad: UserService creates new MySQLDatabase() // Good: UserService takes Database interface, inject MySQLDatabase or PostgreSQLDatabase. SOLID principles lead to code that is easier to change, test, and understand.

Open this question on its own page
02

What is the Observer pattern?

The Observer pattern defines a one-to-many dependency — when one object (Subject/Publisher) changes state, all dependent objects (Observers/Subscribers) are automatically notified. Interfaces: interface Observer { void update(Event event); } interface Subject { void addObserver(Observer observer); void removeObserver(Observer observer); void notifyObservers(Event event); }. Implementation: class StockMarket implements Subject { private List<Observer> observers = new ArrayList<>(); private Map<String, Double> prices = new HashMap<>(); @Override public void addObserver(Observer o) { observers.add(o); } @Override public void removeObserver(Observer o) { observers.remove(o); } @Override public void notifyObservers(Event event) { for (Observer o : observers) { o.update(event); } } public void updatePrice(String symbol, double newPrice) { prices.put(symbol, newPrice); notifyObservers(new PriceChangeEvent(symbol, newPrice)); } } class StockAlertBot implements Observer { private String targetSymbol; private double alertPrice; public StockAlertBot(String symbol, double alertPrice) { this.targetSymbol = symbol; this.alertPrice = alertPrice; } @Override public void update(Event event) { if (event instanceof PriceChangeEvent pce) { if (pce.symbol().equals(targetSymbol) && pce.price() >= alertPrice) { System.out.println("ALERT: " + targetSymbol + " hit $" + pce.price()); } } } } StockMarket market = new StockMarket(); market.addObserver(new StockAlertBot("AAPL", 200.0)); market.addObserver(new StockAlertBot("TSLA", 300.0)); market.updatePrice("AAPL", 205.0); // Triggers AAPL alert. Real-world uses: event systems (JavaScript addEventListener), MVC (Model notifies Views), reactive streams, GUI frameworks, publish-subscribe systems (Kafka, RabbitMQ conceptually).

Open this question on its own page
03

What is the Strategy pattern?

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it. Replaces complex if/else or switch statements for algorithm selection. Example — sorting strategy: interface SortStrategy<T extends Comparable<T>> { void sort(List<T> data); } class BubbleSortStrategy<T extends Comparable<T>> implements SortStrategy<T> { @Override public void sort(List<T> data) { // Bubble sort implementation } } class QuickSortStrategy<T extends Comparable<T>> implements SortStrategy<T> { @Override public void sort(List<T> data) { // Quick sort implementation } } class DataSorter<T extends Comparable<T>> { private SortStrategy<T> strategy; public DataSorter(SortStrategy<T> strategy) { this.strategy = strategy; } public void setStrategy(SortStrategy<T> strategy) { this.strategy = strategy; // Can switch at runtime! } public void sort(List<T> data) { strategy.sort(data); } } // Usage: DataSorter<Integer> sorter = new DataSorter<>(new QuickSortStrategy<>()); sorter.sort(largeData); // Use quick sort for large data sorter.setStrategy(new BubbleSortStrategy<>()); sorter.sort(smallData); // Switch to bubble sort. Another example — payment strategy: interface PaymentStrategy { void pay(double amount); } class CreditCardPayment implements PaymentStrategy { void pay(double a) { /* charge card */ } } class PayPalPayment implements PaymentStrategy { void pay(double a) { /* PayPal API */ } } class Checkout { private PaymentStrategy paymentMethod; void processPayment(double total) { paymentMethod.pay(total); } }. Benefits: Open/Closed Principle, eliminate conditionals, swap algorithms at runtime, test strategies independently.

Open this question on its own page
04

What is the Decorator pattern?

The Decorator pattern attaches additional responsibilities to an object dynamically. It provides a flexible alternative to subclassing for extending functionality. Key insight: instead of creating a class hierarchy for every combination of features, wrap objects with decorator classes: interface Coffee { double getCost(); String getDescription(); } class SimpleCoffee implements Coffee { public double getCost() { return 1.0; } public String getDescription() { return "Coffee"; } } // Abstract decorator: abstract class CoffeeDecorator implements Coffee { protected Coffee decoratedCoffee; public CoffeeDecorator(Coffee coffee) { this.decoratedCoffee = coffee; } public double getCost() { return decoratedCoffee.getCost(); } public String getDescription() { return decoratedCoffee.getDescription(); } } class MilkDecorator extends CoffeeDecorator { public MilkDecorator(Coffee coffee) { super(coffee); } @Override public double getCost() { return super.getCost() + 0.5; } @Override public String getDescription() { return super.getDescription() + ", Milk"; } } class SugarDecorator extends CoffeeDecorator { @Override public double getCost() { return super.getCost() + 0.25; } @Override public String getDescription() { return super.getDescription() + ", Sugar"; } } // Usage: Coffee myCoffee = new SimpleCoffee(); myCoffee = new MilkDecorator(myCoffee); myCoffee = new SugarDecorator(myCoffee); myCoffee = new MilkDecorator(myCoffee); // Double milk! System.out.println(myCoffee.getDescription()); // Coffee, Milk, Sugar, Milk System.out.println(myCoffee.getCost()); // 2.25. Real uses: Java I/O streams (BufferedInputStream wraps FileInputStream), Spring Security filters, middleware chains, UI component decoration. Benefits: flexible composition of behaviors, avoids subclass explosion, Open/Closed Principle.

Open this question on its own page
05

What is the Builder pattern?

The Builder pattern separates the construction of a complex object from its representation. It allows creating different representations of the same object step by step. Especially useful when an object has many optional parameters. Problem without Builder: // Telescoping constructor anti-pattern: new Pizza("large", "thin", "mozzarella", "tomato", true, false, true, false); // What does each boolean mean?! Unreadable!. Builder solution: class Pizza { private final String size; private final String crustType; private final String cheese; private final String sauce; private final boolean pepperoni; private final boolean mushrooms; private final boolean onions; private Pizza(Builder builder) { this.size = builder.size; this.crustType = builder.crustType; this.cheese = builder.cheese; this.sauce = builder.sauce; this.pepperoni = builder.pepperoni; this.mushrooms = builder.mushrooms; this.onions = builder.onions; } public static class Builder { private String size = "medium"; // Defaults private String crustType = "regular"; private String cheese = "mozzarella"; private String sauce = "tomato"; private boolean pepperoni = false; private boolean mushrooms = false; private boolean onions = false; public Builder size(String size) { this.size = size; return this; } public Builder crustType(String type) { this.crustType = type; return this; } public Builder withPepperoni() { this.pepperoni = true; return this; } public Builder withMushrooms() { this.mushrooms = true; return this; } public Pizza build() { validate(); return new Pizza(this); } private void validate() { if (size == null || size.isEmpty()) throw new IllegalStateException("Size required"); } } } // Usage: Pizza myPizza = new Pizza.Builder() .size("large") .crustType("thin") .withPepperoni() .withMushrooms() .build();. Benefits: readable construction, optional parameters with defaults, immutable final objects, validation in build(). Real examples: StringBuilder, Java Stream.Builder, Lombok @Builder, OkHttp RequestBuilder, Retrofit.

Open this question on its own page
06

What is the Adapter pattern?

The Adapter pattern allows incompatible interfaces to work together. It acts as a bridge — wrapping an existing class with a new interface so it can work with code that expects a different interface. Like a power plug adapter that lets US plugs work in European outlets. Object adapter (composition): // Existing class we can't modify: class LegacyPaymentSystem { public void chargeCard(String cardNum, String expiry, int amountInCents) { System.out.println("Legacy charging: $" + amountInCents / 100.0); } } // New interface our system expects: interface ModernPaymentGateway { void processPayment(PaymentDetails details); } // Adapter bridges the gap: class LegacyPaymentAdapter implements ModernPaymentGateway { private LegacyPaymentSystem legacy; public LegacyPaymentAdapter(LegacyPaymentSystem legacy) { this.legacy = legacy; } @Override public void processPayment(PaymentDetails details) { // Translate ModernPaymentGateway call to LegacyPaymentSystem String cardNum = details.getCardNumber(); String expiry = details.getFormattedExpiry(); int cents = (int)(details.getAmount() * 100); legacy.chargeCard(cardNum, expiry, cents); } } // Our code only knows ModernPaymentGateway: LegacyPaymentSystem legacy = new LegacyPaymentSystem(); ModernPaymentGateway gateway = new LegacyPaymentAdapter(legacy); gateway.processPayment(paymentDetails); // Works with old system!. Class adapter (inheritance — Java with interfaces): adapter inherits from adaptee and implements target interface. Real examples: Arrays.asList() adapts array to List; InputStreamReader adapts byte stream to character stream; Java's Collections.enumeration() adapts Iterator to Enumeration; third-party library integrations. When to use: integrating legacy code with new interface requirements, using third-party libraries with incompatible interfaces, creating reusable components that work with classes that don't have the right interface.

Open this question on its own page
07

What is the Template Method pattern?

The Template Method pattern defines the skeleton of an algorithm in a base class, deferring some steps to subclasses. The base class controls the overall structure (the "template") while subclasses override specific steps without changing the overall algorithm structure. Example — data processing pipeline: abstract class DataProcessor { // Template method -- defines the algorithm structure: public final void process() { readData(); validateData(); processData(); formatOutput(); saveResults(); } // Steps that subclasses must implement: protected abstract void readData(); protected abstract void processData(); // Steps with default implementation (can be overridden): protected void validateData() { System.out.println("Default validation: checking for null"); } protected void formatOutput() { System.out.println("Default formatting: plain text"); } protected void saveResults() { System.out.println("Default: saving to file"); } } class CSVDataProcessor extends DataProcessor { @Override protected void readData() { System.out.println("Reading CSV file"); } @Override protected void processData() { System.out.println("Parsing CSV columns"); } @Override protected void formatOutput() { System.out.println("Formatting as JSON"); // Override default } } class APIDataProcessor extends DataProcessor { @Override protected void readData() { System.out.println("Calling REST API"); } @Override protected void processData() { System.out.println("Deserializing JSON response"); } } // Both use the same algorithm structure: new CSVDataProcessor().process(); new APIDataProcessor().process();. Hook methods: optional override points — base class provides empty implementation: protected void postProcess() {} // Hook -- override if needed. Real examples: JUnit TestCase (setUp, test, tearDown), Java's AbstractList, Spring's JdbcTemplate. vs Strategy: Template Method uses inheritance to vary part of algorithm; Strategy uses composition to vary the whole algorithm.

Open this question on its own page
08

What is the Command pattern?

The Command pattern encapsulates a request as an object, allowing parameterization of clients with different requests, queuing of requests, logging, and supporting undoable operations. Core structure: interface Command { void execute(); void undo(); } class TextEditor { private StringBuilder text = new StringBuilder(); private Deque<Command> history = new ArrayDeque<>(); public void executeCommand(Command command) { command.execute(); history.push(command); } public void undo() { if (!history.isEmpty()) { history.pop().undo(); } } public String getText() { return text.toString(); } // Accessor for commands: public StringBuilder getBuffer() { return text; } } class InsertTextCommand implements Command { private TextEditor editor; private String textToInsert; private int position; public InsertTextCommand(TextEditor editor, String text, int pos) { this.editor = editor; this.textToInsert = text; this.position = pos; } @Override public void execute() { editor.getBuffer().insert(position, textToInsert); } @Override public void undo() { editor.getBuffer().delete(position, position + textToInsert.length()); } } // Usage: TextEditor editor = new TextEditor(); editor.executeCommand(new InsertTextCommand(editor, "Hello", 0)); editor.executeCommand(new InsertTextCommand(editor, " World", 5)); System.out.println(editor.getText()); // "Hello World" editor.undo(); System.out.println(editor.getText()); // "Hello". Command queue (task scheduling): store commands in a queue, execute asynchronously. Macro recording: record a list of commands, replay them. Transaction log: log commands to disk, replay after crash. Real uses: GUI button actions, undo/redo in editors, job queues, database transaction logs, game input recording.

Open this question on its own page
09

What is the Facade pattern?

The Facade pattern provides a simplified, unified interface to a complex subsystem — hiding the complexity and dependencies within. Like a hotel concierge — you make one request and they coordinate the complex details. Complex subsystem: class AudioSystem { public void turnOn() { } public void setVolume(int level) { } public void selectSource(String source) { } public void turnOff() { } } class VideoSystem { public void turnOn() { } public void setResolution(String res) { } public void selectInput(String input) { } public void turnOff() { } } class LightingSystem { public void dimLights(int percent) { } public void setColor(String color) { } public void brightLights() { } } class Projector { public void deploy() { } public void setAspectRatio(String ratio) { } public void retract() { } } // Facade -- simple interface to the complex home theater: class HomeTheaterFacade { private AudioSystem audio; private VideoSystem video; private LightingSystem lights; private Projector projector; public HomeTheaterFacade(AudioSystem audio, VideoSystem video, LightingSystem lights, Projector projector) { this.audio = audio; this.video = video; this.lights = lights; this.projector = projector; } // Simple operations: public void watchMovie(String movie) { lights.dimLights(20); lights.setColor("warm"); projector.deploy(); projector.setAspectRatio("16:9"); video.turnOn(); video.setResolution("4K"); video.selectInput("HDMI-1"); audio.turnOn(); audio.setVolume(70); audio.selectSource("HDMI"); System.out.println("Movie ready: " + movie); } public void endMovie() { audio.turnOff(); video.turnOff(); projector.retract(); lights.brightLights(); } } // Client -- simple interface: HomeTheaterFacade theater = new HomeTheaterFacade(audio, video, lights, projector); theater.watchMovie("Inception"); theater.endMovie();. Benefits: simplifies complex APIs, reduces coupling between client and subsystem, provides a simple entry point. Used heavily in frameworks and APIs.

Open this question on its own page
10

What is the Proxy pattern?

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. Types: Virtual Proxy (lazy initialization), Protection Proxy (access control), Remote Proxy (remote object), Logging Proxy, Caching Proxy. Caching proxy example: interface ImageLoader { BufferedImage loadImage(String url); } class RealImageLoader implements ImageLoader { @Override public BufferedImage loadImage(String url) { System.out.println("Loading from network: " + url); return downloadFromNetwork(url); // Slow! } } class CachingImageProxy implements ImageLoader { private Map<String, BufferedImage> cache = new HashMap<>(); private ImageLoader realLoader = new RealImageLoader(); @Override public BufferedImage loadImage(String url) { if (cache.containsKey(url)) { System.out.println("Cache hit: " + url); return cache.get(url); } System.out.println("Cache miss, loading: " + url); BufferedImage image = realLoader.loadImage(url); cache.put(url, image); return image; } }. Protection proxy (access control): class SecureDocumentProxy implements Document { private Document realDocument; private User currentUser; @Override public String getContent() { if (!currentUser.hasPermission("READ")) { throw new SecurityException("Access denied"); } return realDocument.getContent(); } @Override public void setContent(String content) { if (!currentUser.hasPermission("WRITE")) { throw new SecurityException("Access denied"); } realDocument.setContent(content); } }. Virtual proxy (lazy init): class LazyInitProxy implements HeavyService { private HeavyService instance = null; @Override public void doWork() { if (instance == null) { instance = new ExpensiveHeavyService(); // Create only when needed } instance.doWork(); } }. Real examples: Java dynamic proxies, Spring AOP proxies (for @Transactional, @Cacheable), Hibernate lazy loading proxies for entities.

Open this question on its own page
11

What is a mixin?

A mixin is a class or interface that provides reusable functionality that can be "mixed into" other classes without requiring inheritance in the traditional sense. Mixins add behavior without establishing a hierarchical relationship. Problem they solve: in single-inheritance languages, you can't inherit from multiple classes. Mixins allow reusing code across class hierarchies. Java implementation via interfaces with default methods: interface Loggable { default Logger getLogger() { return LoggerFactory.getLogger(getClass()); } default void logInfo(String message) { getLogger().info(message); } default void logError(String message, Throwable t) { getLogger().error(message, t); } } interface Cacheable { Map<String, Object> getCache(); default <T> T getFromCache(String key, Supplier<T> loader) { Map cache = getCache(); if (!cache.containsKey(key)) { cache.put(key, loader.get()); } return (T) cache.get(key); } } class UserService implements Loggable, Cacheable { private Map<String, Object> cache = new HashMap<>(); @Override public Map<String, Object> getCache() { return cache; } public User getUser(int id) { logInfo("Fetching user: " + id); // From Loggable return getFromCache("user:" + id, () -> fetchFromDB(id)); // From Cacheable } }. Ruby mixins (most true form): module Serializable def to_json JSON.generate(instance_variables.map { |var| [var.to_s, instance_variable_get(var)] }.to_h) end end class User include Serializable attr_accessor :name, :email end. Python: multiple inheritance used for mixins. Benefits: code reuse without inheritance hierarchy, compose behaviors independently, avoids diamond problem (if designed carefully).

Open this question on its own page
12

What is dependency inversion principle (DIP)?

The Dependency Inversion Principle (DIP) states: High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions. Violation of DIP: class UserController { // High-level private MySQLDatabase db = new MySQLDatabase(); // Depends on concrete low-level! private EmailService emailService = new SendGridEmailService(); // Concrete! public void registerUser(String email) { db.save(new User(email)); // Tightly coupled emailService.send(email, "Welcome!"); // Tightly coupled } } // Problems: can't test without MySQL and SendGrid, can't switch to PostgreSQL, // testing requires real infrastructure, hard to change implementations. DIP applied: // Abstractions (interfaces): interface UserRepository { void save(User user); } interface EmailNotifier { void sendWelcome(String email); } // High-level module depends on abstractions: class UserController { private final UserRepository repository; // Abstraction private final EmailNotifier notifier; // Abstraction public UserController(UserRepository repository, EmailNotifier notifier) { this.repository = repository; this.notifier = notifier; } public void registerUser(String email) { repository.save(new User(email)); notifier.sendWelcome(email); } } // Low-level modules implement abstractions: class MySQLUserRepository implements UserRepository { public void save(User user) { /* MySQL */ } } class SendGridEmailNotifier implements EmailNotifier { public void sendWelcome(String email) { /* SendGrid */ } } class MockUserRepository implements UserRepository { public void save(User user) { /* in-memory for testing */ } } // Injection: UserController ctrl = new UserController(new MySQLUserRepository(), new SendGridEmailNotifier()); // Testing: UserController testCtrl = new UserController(new MockUserRepository(), new MockNotifier());. DIP enables testability, flexibility, and loose coupling. It's the "D" in SOLID.

Open this question on its own page
13

What is the difference between early and late binding?

Binding is the process of associating a method call with the method's definition (code). Timing of this association determines whether binding is early or late: Early binding (static binding, compile-time binding): the method to be called is determined at COMPILE time, based on the declared type of the reference. Occurs with: overloaded methods, static methods, private methods, final methods: class Animal { public static void staticMethod() { System.out.println("Animal static"); } public final void finalMethod() { System.out.println("Animal final"); } } class Dog extends Animal { public static void staticMethod() { System.out.println("Dog static"); } } // Static method -- early binding: Animal a = new Dog(); a.staticMethod(); // "Animal static" -- decided at compile time based on declared type!. Late binding (dynamic binding, runtime binding): the method to be called is determined at RUNTIME based on the actual type of the object (not the declared reference type). Occurs with virtual/overridden methods: class Animal { public void speak() { System.out.println("Some sound"); } } class Dog extends Animal { @Override public void speak() { System.out.println("Woof!"); } } Animal a = new Dog(); a.speak(); // "Woof!" -- determined at RUNTIME because Dog object is there!. How late binding works: Java uses vtable (virtual method table) — each class has a table of method pointers. When speak() is called on a reference, the runtime looks up the actual class's vtable and calls the right method. Performance: early binding is faster (direct call); late binding adds one indirection. In Java: all instance methods are late-bound by default (virtual). Use final/static/private for early binding (and performance when needed).

Open this question on its own page
14

What is the Open/Closed Principle?

The Open/Closed Principle (OCP) states that software entities (classes, modules, functions) should be OPEN for extension but CLOSED for modification. You should be able to add new behavior without changing existing, tested code. Violation: class AreaCalculator { public double calculateArea(Object shape) { if (shape instanceof Circle circle) { return Math.PI * circle.radius * circle.radius; } else if (shape instanceof Rectangle rectangle) { return rectangle.width * rectangle.height; } // Adding Triangle requires modifying this method! else if (shape instanceof Triangle triangle) { return 0.5 * triangle.base * triangle.height; } return 0; } }. OCP applied — open for extension via abstraction: // Abstraction (closed for modification): interface Shape { double calculateArea(); } // Concrete implementations (extensions don't change existing code): class Circle implements Shape { private double radius; @Override public double calculateArea() { return Math.PI * radius * radius; } } class Rectangle implements Shape { private double width, height; @Override public double calculateArea() { return width * height; } } // Adding new shape -- NO modification to AreaCalculator: class Triangle implements Shape { private double base, height; @Override public double calculateArea() { return 0.5 * base * height; } } class AreaCalculator { public double calculateArea(Shape shape) { return shape.calculateArea(); // Closed -- never changes } public double totalArea(List<Shape> shapes) { return shapes.stream().mapToDouble(Shape::calculateArea).sum(); } } // Add Pentagon, Hexagon etc. without touching AreaCalculator!. OCP in practice: use interfaces/abstract classes for points of variation; use composition over conditional logic; plug-in architecture; Strategy pattern; Decorator pattern. OCP doesn't mean never modifying code — first write the code, then protect it from likely future changes.

Open this question on its own page
15

What is the Liskov Substitution Principle?

The Liskov Substitution Principle (LSP) states: If class B is a subclass of A, then objects of type B should be substitutable for objects of type A without altering the correctness of the program. Subclasses must honor the behavioral contract of the base class. Classic LSP violation — Square extends Rectangle: class Rectangle { protected double width, height; public void setWidth(double w) { this.width = w; } public void setHeight(double h) { this.height = h; } public double area() { return width * height; } } class Square extends Rectangle { @Override public void setWidth(double w) { this.width = w; this.height = w; // Square must keep equal sides! } @Override public void setHeight(double h) { this.width = h; this.height = h; } } // LSP test: void testArea(Rectangle r) { r.setWidth(5); r.setHeight(4); assert r.area() == 20 : "Area should be 20"; // Fails for Square! (25, not 20) } Rectangle rect = new Square(); testArea(rect); // Breaks! Square violates Rectangle's contract. Fix — separate hierarchy: interface Shape { double area(); } class Rectangle implements Shape { /* ... */ } class Square implements Shape { /* ... */ } // Neither extends the other -- no LSP violation possible. LSP rules for subclasses: don't strengthen preconditions (don't require more than base); don't weaken postconditions (don't provide less than base); preserve invariants of base class; don't throw new checked exceptions not in base interface; override methods must have same or weaker preconditions, same or stronger postconditions. LSP enables: true substitutability, reliable polymorphism, correct inheritance hierarchies. Ask: "Is Square really a Rectangle in behavior?" — if no, don't inherit.

Open this question on its own page
Advanced 8 questions

Deep expertise questions for senior and lead roles.

01

What is the Interface Segregation Principle?

The Interface Segregation Principle (ISP) states: Clients should not be forced to depend on methods they do not use. Prefer many small, specific interfaces over one large, general interface ("fat interface"). ISP violation (fat interface): interface Worker { void work(); void eat(); void sleep(); void attendMeeting(); void writeReport(); } class HumanWorker implements Worker { public void work() { /* ... */ } public void eat() { /* ... */ } public void sleep() { /* ... */ } public void attendMeeting() { /* ... */ } public void writeReport() { /* ... */ } } // Problem -- Robot worker must implement methods that don't make sense: class RobotWorker implements Worker { public void work() { /* ... */ } public void eat() { throw new UnsupportedOperationException("Robots don't eat!"); } public void sleep() { throw new UnsupportedOperationException("Robots don't sleep!"); } public void attendMeeting() { /* maybe */ } public void writeReport() { /* maybe */ } }. ISP applied — segregated interfaces: interface Workable { void work(); } interface Eatable { void eat(); } interface Sleepable { void sleep(); } interface Reportable { void writeReport(); } interface Meetable { void attendMeeting(); } class HumanWorker implements Workable, Eatable, Sleepable, Reportable, Meetable { /* Implements all, makes sense */ } class RobotWorker implements Workable, Reportable, Meetable { /* Only relevant interfaces */ }. ISP in practice: // Bad: interface UserRepository with 15 methods // Good: interface UserReader { User findById(int id); List<User> findAll(); } interface UserWriter { void save(User user); void delete(int id); } interface UserSearcher { List<User> search(String criteria); }. Design cohesive interfaces. Clients get only what they need. Changes to one interface don't affect unrelated clients. ISP prevents "implementing interface = implementing everything even unneeded methods."

Open this question on its own page
02

What are GRASP principles?

GRASP (General Responsibility Assignment Software Patterns) are nine principles for assigning responsibilities to classes and objects, defined by Craig Larman: (1) Information Expert: assign responsibility to the class that has the information needed to fulfill it. Order should calculateTotal() because it knows its items; (2) Creator: assign class B to create instances of class A if: B contains A, B records A, B closely uses A, or B has initialization data for A; (3) Controller: assign responsibility for receiving and handling system events to a non-UI class — the "controller" in MVC/MVP; (4) Low Coupling: assign responsibilities to minimize dependencies between classes; (5) High Cohesion: assign responsibilities to keep classes focused and manageable; (6) Polymorphism: use polymorphism when behavior varies by type — avoid type-checking conditions; (7) Pure Fabrication: create a class with no direct real-world counterpart to achieve low coupling/high cohesion (e.g., DatabaseAccessObject, Service, Repository); (8) Indirection: add an intermediate object to mediate between components to reduce coupling (e.g., Controller, Adapter, Facade); (9) Protected Variations: identify points of likely variation, create stable interfaces around them. Most important: Low Coupling + High Cohesion + Information Expert. These guide day-to-day decisions about where to put code — they're more practical than GoF patterns for everyday design.

Open this question on its own page
03

What is cohesion vs coupling trade-offs in practice?

Achieving high cohesion and low coupling simultaneously requires constant design trade-offs. The tension: communication between classes creates coupling, but putting all related code together increases cohesion — you can't eliminate all coupling. Types of coupling (from worst to best): (1) Content coupling — class modifies internals of another (worst); (2) Common coupling — share global state; (3) Control coupling — passing control flags; (4) Stamp coupling — passing complex data structures; (5) Data coupling — passing only necessary data (best); (6) Message coupling — passing messages via interfaces (ideal). Measuring cohesion — types (worst to best): Coincidental (random, unrelated); Logical (related by category but not functionally); Temporal (things done at same time); Procedural (follow execution order); Communicational (operate on same data); Sequential (output of one is input of next); Functional (everything contributes to single, well-defined task — best). Practical trade-offs: // Option A: High cohesion, higher coupling: class OrderService { private final OrderRepository orderRepo; private final PaymentService paymentService; private final EmailService emailService; private final InventoryService inventoryService; // All order operations here -- cohesive but tightly coupled to 4 dependencies } // Option B: Lower cohesion, lower coupling (too many classes): // OrderValidator, OrderPlacer, OrderConfirmer, OrderNotifier // Each tiny, loosely coupled, but hard to understand the flow. Sweet spot: classes that are functionally cohesive (do one clear thing) and coupled only through stable abstractions (interfaces, events). Too much decoupling → over-engineering. Too much coupling → unmaintainable spaghetti. Use coupling metrics in SonarQube or similar tools to guide refactoring.

Open this question on its own page
04

What is the Composite pattern?

The Composite pattern composes objects into tree structures to represent part-whole hierarchies. Clients treat individual objects and compositions of objects uniformly — through the same interface. File system example: interface FileSystemComponent { String getName(); long getSize(); void display(String indent); } class File implements FileSystemComponent { private String name; private long size; public File(String name, long size) { this.name = name; this.size = size; } @Override public String getName() { return name; } @Override public long getSize() { return size; } @Override public void display(String indent) { System.out.println(indent + name + " (" + size + " bytes)"); } } class Directory implements FileSystemComponent { private String name; private List<FileSystemComponent> children = new ArrayList<>(); public Directory(String name) { this.name = name; } public void add(FileSystemComponent component) { children.add(component); } public void remove(FileSystemComponent component) { children.remove(component); } @Override public String getName() { return name; } @Override public long getSize() { return children.stream().mapToLong(FileSystemComponent::getSize).sum(); } @Override public void display(String indent) { System.out.println(indent + name + "/"); for (FileSystemComponent child : children) { child.display(indent + " "); } } } // Usage: Directory root = new Directory("root"); root.add(new File("readme.txt", 1024)); Directory src = new Directory("src"); src.add(new File("Main.java", 2048)); src.add(new File("Utils.java", 1024)); root.add(src); root.display(""); root.getSize(); // Recursive total // Client treats File and Directory uniformly!. Real uses: UI component trees (HTML DOM), menus with sub-menus, org charts, expression trees, XML/JSON parsing.

Open this question on its own page
05

What is the Chain of Responsibility pattern?

The Chain of Responsibility pattern passes requests along a chain of handlers. Each handler decides to either process the request or pass it to the next handler. HTTP middleware example: abstract class MiddlewareHandler { private MiddlewareHandler next; public MiddlewareHandler setNext(MiddlewareHandler next) { this.next = next; return next; // Enables fluent chaining } protected boolean passToNext(HttpRequest request) { if (next != null) { return next.handle(request); } return false; } public abstract boolean handle(HttpRequest request); } class AuthenticationMiddleware extends MiddlewareHandler { @Override public boolean handle(HttpRequest request) { String token = request.getHeader("Authorization"); if (token == null || !isValid(token)) { System.out.println("AUTH FAILED: 401 Unauthorized"); return false; } System.out.println("AUTH: Authenticated user"); return passToNext(request); } } class RateLimitMiddleware extends MiddlewareHandler { private Map<String, Integer> requestCounts = new HashMap<>(); @Override public boolean handle(HttpRequest request) { String ip = request.getClientIp(); int count = requestCounts.getOrDefault(ip, 0); if (count >= 100) { System.out.println("RATE LIMIT: 429 Too Many Requests"); return false; } requestCounts.put(ip, count + 1); return passToNext(request); } } class LoggingMiddleware extends MiddlewareHandler { @Override public boolean handle(HttpRequest request) { System.out.println("LOG: " + request.getMethod() + " " + request.getPath()); return passToNext(request); } } // Setup chain: MiddlewareHandler auth = new AuthenticationMiddleware(); MiddlewareHandler rateLimit = new RateLimitMiddleware(); MiddlewareHandler logging = new LoggingMiddleware(); auth.setNext(rateLimit).setNext(logging); auth.handle(request); // Auth → RateLimit → Logging. Real examples: Java exception handling, Spring Security filter chain, Node.js Express middleware, Servlet Filters, GUI event bubbling.

Open this question on its own page
06

What is the State pattern?

The State pattern allows an object to alter its behavior when its internal state changes. The object appears to change its class. Instead of large switch/if-else based on state, each state is its own class. Vending machine example: interface VendingMachineState { void insertCoin(VendingMachine machine); void selectProduct(VendingMachine machine, String product); void dispense(VendingMachine machine); } class IdleState implements VendingMachineState { public void insertCoin(VendingMachine machine) { machine.setBalance(machine.getBalance() + 1); System.out.println("Coin accepted. Balance: $" + machine.getBalance()); machine.setState(new CoinInsertedState()); } public void selectProduct(VendingMachine machine, String product) { System.out.println("Please insert a coin first."); } public void dispense(VendingMachine machine) { System.out.println("Please insert a coin first."); } } class CoinInsertedState implements VendingMachineState { public void insertCoin(VendingMachine machine) { System.out.println("Coin already inserted. Balance: $" + machine.getBalance()); } public void selectProduct(VendingMachine machine, String product) { if (machine.hasProduct(product)) { machine.setSelectedProduct(product); machine.setState(new ProductSelectedState()); System.out.println("Selected: " + product); } else { System.out.println("Product out of stock."); } } public void dispense(VendingMachine machine) { System.out.println("Please select a product first."); } } class VendingMachine { private VendingMachineState currentState = new IdleState(); private double balance = 0; private String selectedProduct; // ... state + getters/setters public void setState(VendingMachineState state) { this.currentState = state; } public void insertCoin() { currentState.insertCoin(this); } public void selectProduct(String product) { currentState.selectProduct(this, product); } }. Benefits: state-specific behavior in state classes (no large conditionals), easy to add new states, cleaner code. Real uses: traffic lights, UI (enabled/disabled/loading), order status, network connections, game characters.

Open this question on its own page
07

What is meta-programming and reflection in OOP?

Reflection allows a program to inspect and modify its own structure and behavior at runtime — examine classes, methods, fields, and call methods dynamically without knowing them at compile time. Java Reflection: // Inspect a class: Class<?> clazz = String.class; System.out.println(clazz.getName()); // "java.lang.String" for (Method method : clazz.getDeclaredMethods()) { System.out.println(method.getName()); } for (Field field : clazz.getDeclaredFields()) { System.out.println(field.getName()); } // Instantiate without knowing the class at compile time: Class<?> cls = Class.forName("com.example.UserService"); Object instance = cls.getDeclaredConstructor().newInstance(); // Call a method dynamically: Method method = cls.getMethod("processUser", String.class); method.invoke(instance, "Alice"); // Accessing private fields (dangerous!): Field field = clazz.getDeclaredField("secretField"); field.setAccessible(true); Object value = field.get(instance);. Annotations (metadata): @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Benchmark {} class MyService { @Benchmark public void performWork() { /* ... */ } } // Read at runtime: for (Method m : service.getClass().getDeclaredMethods()) { if (m.isAnnotationPresent(Benchmark.class)) { long start = System.nanoTime(); m.invoke(service); long duration = System.nanoTime() - start; System.out.println(m.getName() + " took " + duration + "ns"); } }. Uses of reflection: frameworks (Spring IoC, JUnit test discovery), serialization (Jackson), ORM (Hibernate), dependency injection, RPC frameworks. Costs: slow (bypasses JIT optimization), breaks encapsulation (private access), compile-time safety lost. Use with care — prefer compile-time alternatives when possible.

Open this question on its own page
08

What are immutable objects and why are they important?

An immutable object is one whose state cannot be changed after construction. Its fields are set during construction and never modified. Java's String, Integer, and all wrapper classes are immutable. Creating immutable class (Java): public final class ImmutablePerson { private final String firstName; private final String lastName; private final LocalDate birthDate; private final List<String> hobbies; // Defensive copy for mutable fields! public ImmutablePerson(String firstName, String lastName, LocalDate birthDate, List<String> hobbies) { this.firstName = firstName; this.lastName = lastName; this.birthDate = birthDate; this.hobbies = Collections.unmodifiableList(new ArrayList<>(hobbies)); // Defensive copy } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public LocalDate getBirthDate() { return birthDate; } public List<String> getHobbies() { return hobbies; // Already unmodifiable } // "Wither" methods -- return new instance with changed field: public ImmutablePerson withFirstName(String newFirstName) { return new ImmutablePerson(newFirstName, this.lastName, this.birthDate, this.hobbies); } }. Benefits of immutability: (1) Thread-safety: can safely share across threads without synchronization — immutable = inherently thread-safe; (2) No defensive copying needed for consumers: no risk that they modify your object; (3) Safe as Map keys/Set elements: hashCode won't change; (4) Simple reasoning: state never changes — easier to debug and test; (5) Caching: can safely cache and reuse; (6) Failure atomicity: if creation fails, no partial state exists. Java records (Java 14+): record Point(int x, int y) {} // Automatically immutable!. Kotlin data classes with val: data class Point(val x: Int, val y: Int). Value Objects in DDD are immutable. The more immutable your data, the fewer bugs from accidental mutation.

Open this question on its own page
Back to All Topics 51 questions total