OOPs is the #1 topic where freshers lose technical interviews. It is deceptively easy to claim and genuinely hard to demonstrate. The winning formula: every answer needs (1) a one-line definition, (2) a real-world analogy, (3) a short code snippet, and (4) a practical reason it exists.
🔷 Why OOPs Questions Are Fresher Killers
Almost every fresher can define the 4 pillars. Almost none can demonstrate them in code. The winning move: every answer needs (1) one-line definition, (2) real-world analogy, (3) short code snippet, (4) practical reason it exists. Without all four, the answer sounds memorized, not understood.
"Object-oriented programming forces you to think about the world as interacting, encapsulated entities with clear responsibilities. That mental model is the real skill — not the syntax."— Edsger W. Dijkstra · Turing Award Winner · Pioneer of structured programming
🔷 The 4 Pillars — Quick Reference
Encapsulation
Bundle data + methods. Hide internals via access modifiers. Expose only what is needed.
Inheritance
Child acquires properties of parent. Code reuse and IS-A relationships.
Polymorphism
Same interface, different behavior. One method name, many implementations.
Abstraction
Hide complexity. Expose only essential features. Users interact with interface, not internals.
| Pillar | Real-World Analogy | Achieved Via | Without It |
|---|---|---|---|
| Encapsulation | Car engine — you use accelerator, not fuel injector | private fields + public getters/setters | Any code can corrupt any object's state |
| Inheritance | Dog IS-A Animal — gets eat()/sleep(), adds bark() | extends keyword | Copy-paste code everywhere |
| Polymorphism | draw() on circle vs square — same call, different result | Method overriding + interfaces | Giant if-else chains for every object type |
| Abstraction | ATM — press "Withdraw", don't see the DB calls | abstract class, interface | Complexity leaks everywhere |
🔒 Encapsulation
Without encapsulation, any code can directly modify any object's internal state. A bank account where anyone writes directly to the balance field vs one where all changes go through validated deposit()/withdraw() methods — that difference is encapsulation in action.
Definition: Bundling data (fields) and the methods that operate on them into a single unit (class), restricting direct external access using access modifiers.
Analogy: A bank account — you cannot write directly to the balance field. All changes go through deposit() or withdraw() with validation.
public class BankAccount {
private double balance; // PRIVATE - no direct access
private String owner;
public BankAccount(String owner, double initial) {
this.owner = owner; this.balance = initial;
}
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Invalid");
balance += amount; // validated before state change
}
public boolean withdraw(double amount) {
if (amount > balance) return false;
balance -= amount; return true;
}
public double getBalance() { return balance; } // read-only, no setter
}
account.balance = -99999; // Without encapsulation: state corrupted
account.withdraw(99999); // With encapsulation: fails gracefully
- Data protection: invalid states prevented at the source
- Maintainability: internal changes do not break external code
- Controlled access: read-only, write-only, or computed fields
| Modifier | Same Class | Same Package | Subclass (diff pkg) | Everywhere |
|---|---|---|---|---|
private | ✓ | ✗ | ✗ | ✗ |
| default | ✓ | ✓ | ✗ | ✗ |
protected | ✓ | ✓ | ✓ | ✗ |
public | ✓ | ✓ | ✓ | ✓ |
public class Employee {
private String ssn; // most restrictive - this class only
int id; // default - this package only
protected String name; // package + subclasses
public String dept; // everywhere
}
🧬 Inheritance
Only use inheritance for genuine IS-A relationships. Java's own Stack extends Vector — exposing 43 extra methods that break the Stack contract. This mistake is cited in every OOP textbook as a cautionary tale.
Definition: A mechanism where a child (subclass) acquires the properties and behaviors of a parent (superclass), enabling code reuse and IS-A relationships.
class Animal {
String name;
void eat() { System.out.println(name + " is eating"); }
void sleep(){ System.out.println(name + " is sleeping"); }
}
class Dog extends Animal { // Dog IS-A Animal
void bark(){ System.out.println(name + " says Woof!"); }
}
Dog d = new Dog(); d.name = "Rex";
d.eat(); // inherited from Animal
d.bark(); // Dog's own method
Types: Single (A→B), Multilevel (A→B→C), Hierarchical (A→B and A→C) — all in Java. Multiple class inheritance: NOT supported (Diamond Problem). Multiple interface: YES.
IS-A vs HAS-A: Dog IS-A Animal → extends. Car HAS-A Engine → use a field. Prefer composition when no genuine IS-A relationship exists.
When class D inherits from B and C, both of which override the same method from A — the compiler cannot resolve which version D should use.
// Hypothetical - NOT valid Java:
class A { void greet() { print("A"); } }
class B extends A { void greet() { print("B"); } }
class C extends A { void greet() { print("C"); } }
class D extends B, C { } // Which greet() does D use? Ambiguous!
// Java solution: disallow multiple class inheritance.
// Allow multiple INTERFACE implementation with explicit resolution:
interface B { default void greet() { print("B"); } }
interface C { default void greet() { print("C"); } }
class D implements B, C {
public void greet() { B.super.greet(); } // must explicitly choose
}
class Animal {
String name; int age;
Animal(String name, int age){ this.name=name; this.age=age; }
void describe(){ System.out.println("Animal: " + name); }
}
class Dog extends Animal {
String breed;
// USE 1: super() calls parent constructor - MUST be first line
Dog(String name, int age, String breed){
super(name, age); // initialises Animal fields
this.breed = breed;
}
// USE 2: super.method() calls parent's overridden method
void describe(){
super.describe(); // "Animal: Rex"
System.out.println("Breed: " + breed);
}
// USE 3: super.field accesses parent field when shadowed
String getParentName(){ return super.name; }
}
🔀 Polymorphism
Without polymorphism, you need a giant if-else every time you process different object types. With it, code written against an interface works for every type that implements it — including types that did not exist when you wrote the code. This is the foundation of every plugin system and framework hook in existence.
Compile-time (Method Overloading): Same method name, different parameter signatures in the same class. Resolved at compile time — static binding.
class Calculator {
int add(int a, int b) { return a+b; }
double add(double a, double b) { return a+b; }
int add(int a, int b, int c) { return a+b+c; }
}
calc.add(1,2); // compile time: add(int,int)
calc.add(1.5,2.5); // compile time: add(double,double)
Runtime (Method Overriding): Child redefines parent's method. Resolved at runtime based on actual object type — dynamic dispatch.
class Shape { void draw(){ System.out.println("Shape"); } }
class Circle extends Shape { void draw(){ System.out.println("Drawing a circle"); } }
class Rectangle extends Shape { void draw(){ System.out.println("Drawing a rectangle"); } }
class Triangle extends Shape { void draw(){ System.out.println("Drawing a triangle"); } }
Shape[] shapes = { new Circle(), new Rectangle(), new Triangle() };
for(Shape s : shapes) s.draw();
// One loop handles ALL shape types, including future ones.
// Add Pentagon? Just extend Shape. ZERO changes to this loop.
| Property | Overloading | Overriding |
|---|---|---|
| Definition | Same name, different params — SAME class | Child redefines parent method — SAME signature |
| Binding | Compile-time (static) | Runtime (dynamic) |
| Signature | MUST differ | MUST be identical |
| Return type | Can differ | Same or covariant |
| Access modifier | Anything | Cannot reduce visibility |
| Static methods? | ✓ Can overload | ✗ Static methods are HIDDEN, not overridden |
class Parent {
public void show() { System.out.println("Parent show"); }
public void show(String msg) { System.out.println("Parent: " + msg); }
}
class Child extends Parent {
@Override
public void show() { System.out.println("Child show"); } // overriding
public void show(int n) { System.out.println("Child: " + n); } // overloading
}
Parent p = new Child();
p.show(); // "Child show" - runtime dispatch (overriding)
p.show("hi"); // "Parent: hi" - compile-time (overloading)
No. Static methods belong to the class, not instances. They are resolved at compile time by reference type. When a child defines a static method with the same signature, it is called method hiding.
class Parent {
static void staticMethod() { System.out.println("Parent static"); }
void instanceMethod(){ System.out.println("Parent instance"); }
}
class Child extends Parent {
static void staticMethod() { System.out.println("Child static"); } // HIDING
void instanceMethod(){ System.out.println("Child instance"); } // OVERRIDING
}
Parent ref = new Child();
ref.staticMethod(); // "Parent static" - REFERENCE TYPE decides (compile time)
ref.instanceMethod(); // "Child instance" - ACTUAL OBJECT decides (runtime)
🎭 Abstraction
Abstraction is why you can use HashMap without knowing its hash function, or use JDBC without knowing your database protocol. It allows systems to grow — no single engineer needs to understand the entire stack to contribute.
| Property | Abstract Class | Interface |
|---|---|---|
| Methods | Abstract AND concrete | Abstract by default; default/static (Java 8+) |
| Fields | Any visibility, instance variables | public static final constants only |
| Constructor | ✓ Yes | ✗ No |
| Multiple inheritance | ✗ Extend only ONE | ✓ Implement MANY |
| Use when | Partial impl + shared state + IS-A | Contract/capability + multiple inheritance of type |
// Abstract class: partial implementation + shared state
abstract class Vehicle {
private String color;
public Vehicle(String color){ this.color = color; }
abstract void refuel(); // subclasses MUST implement
public void start(){ System.out.println(color + " engine started"); }
}
class Car extends Vehicle {
public Car(String c){ super(c); }
public void refuel(){ System.out.println("Filling petrol"); }
}
// Interface: capability contract
interface Flyable { void fly(); }
interface Swimmable { void swim(); }
class Duck extends Animal implements Flyable, Swimmable {
public void fly() { System.out.println("Duck flies"); }
public void swim(){ System.out.println("Duck swims"); }
}
// Duck IS-A Animal, and is BOTH Flyable and Swimmable
| Aspect | Abstraction | Encapsulation |
|---|---|---|
| Hides | Complexity — the HOW | Data — internal WHAT |
| Question | "What does this DO?" | "How does it PROTECT its data?" |
| Level | Design/architecture | Implementation/coding |
| Achieved via | Interfaces, abstract classes | private fields + public methods |
Analogy: ATM machine — Abstraction: you see "Withdraw" button, not the DB calls (complexity hidden). Encapsulation: the cash tray — you cannot reach in directly; all interactions go through the process (data protected).
🏗 Classes, Objects & Constructors
Constructor chaining, this() vs super() ordering rules, private constructors for Singleton, and copy constructors appear regularly in Infosys technical rounds. These are daily Java mechanics every developer uses.
Class: Blueprint defining attributes and behaviors. No memory allocated for instance data at definition time. Object: A specific instance. Memory allocated on the heap when new is called.
class Car { String brand; int year; void start(){ System.out.println(brand + " started"); } }
Car car1 = new Car(); car1.brand = "Toyota"; // heap memory for car1
Car car2 = new Car(); car2.brand = "Honda"; // separate heap memory
car1.start(); // "Toyota started" - same method, different data
car2.start(); // "Honda started"
Memory layout: Stack → reference variable. Heap → actual object data (GC-managed). Method Area → class metadata and bytecode shared across all instances.
class Student {
String name; int age; String major;
// 1. DEFAULT: chains to parameterized via this()
Student(){ this("Unknown", 18, "Undecided"); }
// 2. PARAMETERIZED: initialises all fields
Student(String name, int age, String major){
this.name = name; this.age = age; this.major = major;
}
// 3. COPY: creates new object from existing one
Student(Student other){ this(other.name, other.age, other.major); }
}
// Rules:
// this(...) - calls another constructor in SAME class, must be FIRST line
// super(...) - calls parent constructor, must be FIRST line
// Cannot call BOTH this() AND super() in the same constructor
Yes. A private constructor prevents external code from calling new ClassName() directly.
// Singleton - double-checked locking
public class AppConfig {
private static volatile AppConfig instance;
private AppConfig(){ } // private - no external instantiation
public static AppConfig getInstance(){
if(instance == null){
synchronized(AppConfig.class){
if(instance == null) instance = new AppConfig();
}
}
return instance;
}
}
// Enum Singleton (BEST - JVM-guaranteed thread-safe + serialization-safe)
public enum Config { INSTANCE; public String getDb(){ return "jdbc:..."; } }
Config.INSTANCE.getDb(); // guaranteed single instance
// Static factory methods - named, expressive constructors
public class Color {
private int r, g, b;
private Color(int r, int g, int b){ this.r=r; this.g=g; this.b=b; }
public static Color red() { return new Color(255, 0, 0); }
public static Color fromHex(String h){ /* parse */ return new Color(r, g, b); }
}
public class Person {
private String name; private int age;
// USE 1: disambiguate instance variable from parameter
public Person(String name, int age){ this.name = name; this.age = age; }
// USE 2: call another constructor in same class (FIRST line)
public Person(String name){ this(name, 0); }
// USE 3: pass current object as argument
public void register(Registry r){ r.add(this); }
// USE 4: return current object - enables fluent builder chaining
public Person setName(String n){ this.name = n; return this; }
public Person setAge(int a) { this.age = a; return this; }
}
// Builder chaining:
Person p = new Person("Alice").setName("Alice Chen").setAge(25);
| Relationship | Ownership | Lifecycle | Example |
|---|---|---|---|
| Association | None | Independent | Student has a Teacher |
| Aggregation | Weak — parts independent | Parts survive the whole | Library HAS Books (books survive if library closes) |
| Composition | Strong — owns exclusively | Parts destroyed with whole | Human HAS Heart (heart cannot exist without Human) |
// AGGREGATION: Engine passed in from outside
class Car { private Engine engine; Car(Engine e){ this.engine = e; } }
// COMPOSITION: Rooms created inside, destroyed with House
class House {
private final Room[] rooms;
House(int n){ rooms = new Room[n]; for(int i=0;i<n;i++) rooms[i] = new Room(); }
}
🔑 Keywords & Modifiers
These appear in every Infosys question set as "What happens when..." scenarios. Understanding what each prevents and the edge cases separates solid Java knowledge from surface-level familiarity.
class Counter {
private static int count = 0; // STATIC FIELD: shared by ALL instances
private int id; // instance field: each object has its own
static { // STATIC BLOCK: runs ONCE when class loads
count = 100;
System.out.println("Counter class loaded");
}
public Counter(){ this.id = ++count; }
// STATIC METHOD: no 'this', cannot access instance fields
public static int getCount(){ return count; }
// public static int getId(){ return this.id; } // compile error - no 'this'
}
// STATIC NESTED CLASS: no outer instance needed
class Outer {
private int x = 10;
static class Nested { void show(){ System.out.println("nested"); } }
class Inner { void show(){ System.out.println(x); } } // CAN access x
}
// 1. FINAL VARIABLE: cannot be reassigned after initialization
final int MAX = 100;
MAX = 200; // compile error
final StringBuilder sb = new StringBuilder("hello");
sb.append(" world"); // OK - mutating the OBJECT is fine
sb = new StringBuilder(); // compile error - reassigning the REFERENCE is not
// CRUCIAL: final prevents reference reassignment, NOT object mutation!
// 2. FINAL METHOD: cannot be overridden
class Base { final void critical(){ } }
class Child extends Base { void critical(){ } } // compile error
// 3. FINAL CLASS: cannot be subclassed
// Java's String is final:
// 1. Security: cannot override hashCode/equals used as HashMap keys
// 2. Thread safety: immutable = no synchronization needed
// 3. String pool: JVM can safely reuse interned literals
| Property | Checked | Unchecked |
|---|---|---|
| Detected at | Compile time — enforced | Runtime — optional to handle |
| Must handle? | ✓ Yes — try-catch or throws | ✗ No |
| Indicates | Recoverable external conditions | Programming errors (bugs) |
| Examples | IOException, SQLException, ClassNotFoundException | NullPointerException, IllegalArgumentException, ArrayIndexOutOfBoundsException |
// CHECKED: predictable external failure - caller MUST handle
void readFile(String path) throws IOException {
new BufferedReader(new FileReader(path));
}
// UNCHECKED: programmer error - fix it, do not catch it
void setAmount(double amt){
if(amt < 0) throw new IllegalArgumentException("Amount negative: " + amt);
}
// Exception hierarchy:
// Throwable
// / // Exception Error (Error: JVM issues, do not catch)
// / // Checked RuntimeException (Unchecked)
☕ Java-Specific Internals
Infosys primarily uses Java. String immutability, == vs .equals(), StringBuilder vs StringBuffer, and garbage collection are the most common Java-specific OOP questions. These reveal whether you actually write Java or just know its syntax.
| Property | String | StringBuffer | StringBuilder |
|---|---|---|---|
| Mutable? | ✗ Immutable | ✓ Mutable | ✓ Mutable |
| Thread-safe? | ✓ Yes | ✓ Yes (synchronized) | ✗ No |
| Use when | Simple strings, HashMap keys | Multi-threaded string building | Single-threaded string building (99% of cases) |
// BAD: creates 10,000 String objects in heap - O(n squared)
String result = "";
for(int i = 0; i < 10000; i++) result += "item" + i; // each += creates new object
// GOOD: one mutable object, amortized O(1) per append
StringBuilder sb = new StringBuilder();
for(int i = 0; i < 10000; i++) sb.append("item").append(i);
String result = sb.toString();
// Why String is immutable:
// 1. String Pool: JVM reuses identical literals safely
// 2. Thread safety: immutable = no synchronization needed
// 3. HashMap key safety: hashCode() is stable after insertion
// 4. Security: prevents class-loading attacks
Java's GC automatically reclaims heap memory occupied by objects no longer reachable from any live root reference.
Generational GC: Young Generation (Eden + Survivor) → Minor GC (frequent, cheap, most objects die here). Old Generation → Full GC (infrequent, expensive). Metaspace (Java 8+) → class metadata, not heap.
Object obj = new Object();
obj = null; // unreachable - eligible for GC
System.gc(); // hint only - JVM decides WHEN, NOT guaranteed
// finalize(): deprecated Java 9+, removed Java 18.
// Problems: no timing guarantee, silent exception swallowing, can resurrect objects.
// CORRECT approach: AutoCloseable + try-with-resources (deterministic cleanup)
class DBConn implements AutoCloseable {
public void close(){ System.out.println("Connection closed"); }
}
try(DBConn c = new DBConn()){ /* use c */ } // close() called automatically
📐 SOLID Principles
SOLID reveals whether you think about code quality and maintainability, not just functionality. One sentence per principle plus a concrete before/after example is what impresses Infosys interviewers.
Single Responsibility
A class should have only ONE reason to change — one job, done well.
Open / Closed
Open for extension (new behavior via new classes). Closed for modification (do not change working code).
Liskov Substitution
Subtype objects must be substitutable for supertype objects without breaking the program.
Interface Segregation
Many small focused interfaces beat one large bloated one. Clients should not depend on methods they do not use.
Dependency Inversion
Depend on abstractions (interfaces), not concrete implementations.
// VIOLATES SRP: UserManager has 3 reasons to change
class UserManager {
void createUser(String name) { /* DB insert */ }
void sendEmail(String email) { /* SMTP logic */ }
void genReport(String id) { /* PDF gen */ }
}
// Change email provider -> modify UserManager
// Change PDF library -> modify UserManager
// Change DB schema -> modify UserManager
// FOLLOWS SRP: each class has ONE responsibility
class UserRepository { void save(User u){ } }
class EmailService { void send(User u){ } }
class ReportGenerator { void gen(User u) { } }
// Change email provider -> only EmailService changes. Others untouched.
// VIOLATES OCP: every new discount type requires modifying this method
double discount(String type, double price){
if(type.equals("Regular")) return price * 0.05;
if(type.equals("Premium")) return price * 0.10;
// Adding "Corporate"? Must MODIFY this method - risky!
return 0;
}
// FOLLOWS OCP: new types extend the interface - zero existing code changed
interface DiscountStrategy { double apply(double p); }
class RegularDiscount implements DiscountStrategy { public double apply(double p){ return p*0.05; } }
class PremiumDiscount implements DiscountStrategy { public double apply(double p){ return p*0.10; } }
class CorporateDiscount implements DiscountStrategy { public double apply(double p){ return p*0.25; } }
// Add "Enterprise"? Just add a new class. Zero changes to existing code.
// VIOLATES LSP: Square is not a proper behavioral subtype of Rectangle
class Rectangle {
protected int width, height;
void setWidth(int w) { this.width = w; }
void setHeight(int h) { this.height = h; }
int area() { return width * height; }
}
class Square extends Rectangle {
void setWidth(int w) { this.width = w; this.height = w; } // side effect!
void setHeight(int h) { this.width = h; this.height = h; }
}
void test(Rectangle r){
r.setWidth(5); r.setHeight(10);
assert r.area() == 50; // PASSES for Rectangle, FAILS for Square (area=100)
}
// Fix: do not inherit just because of geometric "is-a".
// Mathematical is-a does not equal behavioral is-a in OOP.
// Both should independently implement a Shape interface.
// VIOLATES ISP: fat interface forces animals to implement irrelevant methods
interface Animal {
void eat(); void sleep();
void fly(); // Penguins cannot fly - forced to throw UnsupportedOperationException
void swim(); // Eagles do not swim - forced empty implementation
}
// FOLLOWS ISP: small focused interfaces
interface Eatable { void eat(); }
interface Sleepable{ void sleep(); }
interface Flyable { void fly(); }
interface Swimmable{ void swim(); }
class Eagle implements Eatable, Sleepable, Flyable { }
class Penguin implements Eatable, Sleepable, Swimmable { }
class Duck implements Eatable, Sleepable, Flyable, Swimmable { }
// VIOLATES DIP: OrderService directly coupled to MySQLDatabase
class OrderService {
private MySQLDatabase db = new MySQLDatabase(); // tightly coupled!
void placeOrder(String o){ db.save(o); }
}
// Switching to MongoDB requires changing OrderService - violation!
// FOLLOWS DIP: depends on ABSTRACTION
interface Database { void save(String data); }
class MySQLDatabase implements Database { public void save(String d){ System.out.println("MySQL: " + d); } }
class MongoDatabase implements Database { public void save(String d){ System.out.println("Mongo: " + d); } }
class OrderService {
private final Database db;
OrderService(Database db){ this.db = db; } // INJECTED - loose coupling
void placeOrder(String o){ db.save(o); }
}
// Switch to MongoDB? Just inject MongoDatabase. OrderService unchanged.
OrderService svc = new OrderService(new MongoDatabase());
Cohesion: How strongly related are the responsibilities within a single class? High cohesion = does one thing well. Low cohesion = does many unrelated things.
Coupling: How dependent are modules on each other? Low coupling = change independently. High coupling = changing one forces changes in others.
| Combination | Outcome |
|---|---|
| High cohesion + Low coupling | ✓ Ideal — maintainable, testable, extensible |
| Low cohesion + High coupling | ✗ Worst — spaghetti code |
// LOW COHESION: God class doing unrelated things
class GodClass { void parseJSON(){} void sendEmail(){} void generatePDF(){} }
// HIGH COHESION: each class focused on one responsibility
class JsonParser { Object parse(String json){ return null; } }
class EmailSender { void send(String to, String body){ } }
class PdfGenerator { byte[] generate(Object data){ return null; } }
🧩 Design Patterns
Singleton, Factory, Observer, and Strategy are standard fresher questions at Infosys for DSE and SP roles. You need to explain the pattern, write its code, and explain the real problem it solves. Definitions alone will not pass.
Problem it solves: Ensure only one instance of a class exists throughout the application (logger, config, DB connection pool).
// VERSION 1: Not thread-safe (avoid)
class Logger {
private static Logger instance;
private Logger(){ }
public static Logger getInstance(){
if(instance == null) instance = new Logger(); // race condition!
return instance;
}
}
// VERSION 2: Double-checked locking (thread-safe + efficient)
class Logger {
private static volatile Logger instance; // volatile prevents reordering
private Logger(){ }
public static Logger getInstance(){
if(instance == null){
synchronized(Logger.class){
if(instance == null) instance = new Logger();
}
}
return instance;
}
}
// VERSION 3: Enum Singleton (BEST - JVM-guaranteed, serialization-safe)
public enum AppConfig {
INSTANCE;
public String getDbUrl(){ return "jdbc:mysql://..."; }
}
AppConfig.INSTANCE.getDbUrl(); // guaranteed single instance
Problem it solves: The exact type of object to create is determined at runtime. Using new ConcreteClass() directly couples code to specific implementations.
interface Document { void open(); void save(); }
class PdfDocument implements Document { public void open(){ System.out.println("PDF open"); } public void save(){ } }
class WordDocument implements Document { public void open(){ System.out.println("Word open"); } public void save(){ } }
// Factory: one centralized place for all creation logic
class DocumentFactory {
public static Document create(String type){
return switch(type.toLowerCase()){
case "pdf" -> new PdfDocument();
case "word" -> new WordDocument();
default -> throw new IllegalArgumentException("Unknown: " + type);
};
}
}
// Client does not know WHICH concrete class it gets
Document doc = DocumentFactory.create("pdf");
doc.open(); // works for any Document type
// Add Excel support? New class + one case in factory. Client code unchanged.
Problem it solves: One object (publisher) needs to notify many observers when its state changes — without knowing who those observers are.
interface StockObserver { void update(String stock, double price); }
class StockMarket { // Publisher
private List<StockObserver> list = new ArrayList<>();
public void subscribe(StockObserver o) { list.add(o); }
public void unsubscribe(StockObserver o) { list.remove(o); }
public void setPrice(String s, double p) {
list.forEach(o -> o.update(s, p)); // notify ALL observers
}
}
class EmailAlert implements StockObserver { public void update(String s,double p){ System.out.println("Email: " + s + "=" + p); } }
class MobilePush implements StockObserver { public void update(String s,double p){ System.out.println("Push: " + s + "=" + p); } }
class TradingBot implements StockObserver { public void update(String s,double p){ System.out.println("Bot trading: " + s); } }
StockMarket mkt = new StockMarket();
mkt.subscribe(new EmailAlert()); mkt.subscribe(new MobilePush()); mkt.subscribe(new TradingBot());
mkt.setPrice("INFY", 1750.50); // all 3 notified automatically
Problem it solves: Multiple algorithms for the same task — switch between them at runtime without if-else chains.
interface SortStrategy { void sort(int[] arr); }
class BubbleSort implements SortStrategy { public void sort(int[] a){ System.out.println("Bubble"); } }
class MergeSort implements SortStrategy { public void sort(int[] a){ System.out.println("Merge"); } }
class QuickSort implements SortStrategy { public void sort(int[] a){ System.out.println("Quick"); } }
class Sorter {
private SortStrategy strategy;
Sorter(SortStrategy s){ this.strategy = s; }
void setStrategy(SortStrategy s){ this.strategy = s; }
void sort(int[] arr){ strategy.sort(arr); } // delegates to current strategy
}
Sorter sorter = new Sorter(new QuickSort());
sorter.sort(data); // uses QuickSort
sorter.setStrategy(new MergeSort()); // swap at runtime
sorter.sort(data); // now uses MergeSort
// Java's Comparator IS a strategy: Collections.sort(list, comparator)
⚡ Advanced OOP Concepts
Declare variables, parameters, and return types using interfaces (or abstract types), not concrete classes. Let the concrete implementation be determined at runtime or by injection.
// Tightly coupled to ArrayList
ArrayList<String> list = new ArrayList<>();
// Switching to LinkedList requires finding and changing every usage.
// Loosely coupled - programmed to interface
List<String> list = new ArrayList<>(); // declare as List (interface)
// Switch to LinkedList? ONE change, nothing else breaks:
// List<String> list = new LinkedList<>();
// Powerful real-world example: payment gateway abstraction
interface PaymentProcessor { boolean process(double amount); }
class PaymentService {
private final PaymentProcessor processor; // depends on INTERFACE
PaymentService(PaymentProcessor p){ this.processor = p; }
boolean pay(double amt){ return processor.process(amt); }
}
PaymentService live = new PaymentService(new RazorpayProcessor());
PaymentService test = new PaymentService(new MockPaymentProcessor()); // for unit tests
// == compares REFERENCES (same object in heap?)
// .equals() compares CONTENT (same logical value?)
String s1 = "hello"; // from String pool
String s2 = "hello"; // same pool object
String s3 = new String("hello"); // new heap object, NOT from pool
System.out.println(s1 == s2); // TRUE - same pool object
System.out.println(s1 == s3); // FALSE - s3 is a different heap object
System.out.println(s1.equals(s3)); // TRUE - same content
// Integer cache trap (values -128 to 127):
Integer a = 127; Integer b = 127; System.out.println(a == b); // TRUE (cached)
Integer c = 128; Integer d = 128; System.out.println(c == d); // FALSE (not cached)
System.out.println(c.equals(d)); // TRUE
The String Pool: Java maintains a pool of String literals in JVM non-heap memory. When you write "hello" as a literal, the JVM checks if that value already exists. If yes, the existing reference is reused (memory saving). new String("hello") always creates a new heap object, bypassing the pool.
// Explicitly intern a String back into the pool:
String s4 = new String("hello").intern(); // now references the pool object
System.out.println(s1 == s4); // TRUE
🗀 OOPs Quick-Review Cheatsheet
"Programs must be written for people to read, and only incidentally for machines to execute. OOP, done right, achieves exactly that."— Harold Abelson · MIT · Co-author of "Structure and Interpretation of Computer Programs"
Design Patterns Quick Reference
| Pattern | Category | Problem Solved | Key Structure |
|---|---|---|---|
| Singleton | Creational | Exactly one global instance | Private constructor + volatile static + getInstance() |
| Factory | Creational | Object type decided at runtime | Interface + concrete classes + static factory method |
| Builder | Creational | Complex object with many optional params | Fluent setters returning this + build() |
| Observer | Behavioral | One-to-many state change notification | Subject + Observer interface + subscribe/notify list |
| Strategy | Behavioral | Swappable algorithms at runtime | Strategy interface + concrete strategies + context |
| Decorator | Structural | Add behavior without subclassing | Component interface + concrete + wrapper |
| Adapter | Structural | Incompatible interfaces need to work together | Target interface + Adaptee + Adapter wrapping Adaptee |
Up Next: Phase 4 — DBMS
32 DBMS questions — beyond SQL into how databases work internally: transactions, B-tree indexing, deadlocks, MVCC, connection pools, and modern database architecture.