OOP & Collections Deep Dive
"First, learn the rules. Then break them intelligently. OOP gives you the rules for building scalable systems."
Object-Oriented Programming (OOP) is how engineers manage complexity. A single FAANG backend service can have millions of lines of code written by hundreds of engineers. Without OOP principles, that codebase collapses under its own weight. Encapsulation, Inheritance, Polymorphism, and Abstraction are the four tools that keep large systems maintainable — and they appear directly in interview questions like "Design a Parking Lot" or "Design an LRU Cache."
📖 Before We Start — The Big Picture
In the real world, we manage complexity by grouping related things. A class is a blueprint (the architect's drawing of a house). An object is the actual thing built from that blueprint (a real house). Every Toyota car on the road is a different object, but they all share the same class (blueprint). OOP is just this idea — applied to code.
You already used objects in Lecture 2 when you read about the
String class, ArrayList, and the JVM's heap.
Every single one of those is a class. This lecture teaches you how to
design your own classes and how to make them work together
elegantly.
new up goes on the heap. This lecture
teaches you what those objects are. Lecture 4 (Java 8+) extends this
with lambdas and functional interfaces, which are just anonymous
object implementations behind the scenes.
🏗 The Four Pillars of OOP
| Pillar | What it means | DSA Example | Key mechanism |
|---|---|---|---|
| Encapsulation | Bundle data + methods; hide internals via access modifiers | LinkedList: private head, public add/remove | private fields + public getters/setters |
| Abstraction | Expose what to do; hide how | PriorityQueue: you call offer/poll, not heapifyUp | abstract classes, interfaces |
| Inheritance | Subclass reuses + extends superclass | TreeNode extends Node; DFS methods inherited | extends keyword, super() |
| Polymorphism | Same interface, different behaviour per type | Comparator: sort by salary vs name without changing sort logic | @Override, method overloading |
1. Encapsulation
class BankAccount { private double balance; // hidden — cannot access directly BankAccount(double init) { balance = init; } public void deposit(double amt) { if (amt <= 0) throw new IllegalArgumentException("Amount must be positive"); balance += amt; } public double getBalance() { return balance; } // read-only access } // Cannot do: account.balance = -1000; (compile error!) // Must go through deposit/withdraw which enforce business rules
2. Inheritance
abstract class Shape { abstract double area(); void print() { System.out.println("Area: " + area()); } // inherited } class Circle extends Shape { double r; Circle(double r) { this.r = r; } @Override double area() { return Math.PI * r * r; } } class Rectangle extends Shape { double w, h; Rectangle(double w, double h) { this.w=w; this.h=h; } @Override double area() { return w * h; } } // Polymorphism in action: Shape[] shapes = {new Circle(5), new Rectangle(3,4)}; for (Shape s : shapes) s.print(); // calls correct area() per type
3. Polymorphism — Runtime vs Compile-time
| Type | When resolved | Mechanism | Example |
|---|---|---|---|
| Runtime (Dynamic) | At runtime | @Override — subclass overrides parent method | Shape.area() calls Circle.area() or Rectangle.area() |
| Compile-time (Static) | At compile time | Method overloading — same name, different params | print(int) vs print(String) vs print(double) |
4. Abstract Class vs Interface — The Critical Comparison
| Dimension | Abstract Class | Interface |
|---|---|---|
| Instantiation | Cannot instantiate | Cannot instantiate |
| Constructor | ✅ Can have | ❌ Cannot have |
| Fields | Any access modifier | Only public static final (constants) |
| Methods | Abstract + concrete |
Abstract + default + static (Java
8+)
|
| Multiple inheritance | ❌ One parent only | ✅ A class can implement many |
| When to use | "Is-a" with shared state/code (Template Method pattern) | "Can-do" (Comparable, Runnable, Serializable) |
| DSA Example | AbstractList providing default iterator | Comparable for natural ordering in TreeSet/PQ |
📊 Comparable vs Comparator
class Employee implements Comparable<Employee> { String name; int salary; @Override public int compareTo(Employee o) { return Integer.compare(this.salary, o.salary); // natural order = by salary } } // Comparable: used when ONE natural ordering makes sense // TreeSet<Employee> automatically uses compareTo() // Comparator: used when you need MULTIPLE orderings or can't modify class Comparator<Employee> byName = Comparator.comparing(e -> e.name); Comparator<Employee> bySalaryDesc = (a,b) -> b.salary - a.salary; // Chaining comparators (sort by salary desc, then by name asc) Comparator<Employee> combined = Comparator.comparingInt((Employee e) -> e.salary).reversed() .thenComparing(e -> e.name); list.sort(combined); Arrays.sort(arr, combined); new PriorityQueue<>(combined); // custom-ordered heap
🧬 Generics Deep Dive
// Generic method — works for any Comparable type public static <T extends Comparable<T>> T findMax(List<T> list) { T max = list.get(0); for (T item : list) if (item.compareTo(max) > 0) max = item; return max; } // Bounded wildcards // ? extends T — can READ (covariant): upper bound double sumOfList(List<? extends Number> list) { double sum = 0; for (Number n : list) sum += n.doubleValue(); return sum; } // works for List<Integer>, List<Double>, List<Long> // ? super T — can WRITE (contravariant): lower bound void addNumbers(List<? super Integer> list) { for (int i = 1; i <= 10; i++) list.add(i); } // works for List<Integer>, List<Number>, List<Object> // PECS: Producer Extends, Consumer Super
📋 ArrayList Internals
new ArrayList<>() it creates an
Object[10] internally. When full, it allocates a NEW array of size
1.5× and copies everything over. This copy is O(n) but happens
rarely, so add() is O(1) amortised.
// Pseudocode approximation of ArrayList internals class SimpleArrayList<T> { Object[] data = new Object[10]; // default capacity int size = 0; void add(T item) { if (size == data.length) { // capacity hit int newCap = (size * 3) / 2 + 1; // ~1.5× growth data = Arrays.copyOf(data, newCap); // O(n) copy! } data[size++] = item; // O(1) } void remove(int idx) { // O(n) — shifts elements left System.arraycopy(data, idx+1, data, idx, size-idx-1); data[--size] = null; // prevent memory leak } } // Tip: new ArrayList<>(expectedSize) avoids all resizing overhead
💪 Practice Problems
📝 Assignment
20+ company-tagged OOP & collections problems including LRU Cache, Design HashMap, Group Anagrams, and inheritance design exercises.
📄 Open Assignment →
✅ Lecture Completion Checklist
You've nailed OOP — inheritance, polymorphism, and Java Collections. Java 8 brought the biggest syntax revolution in the language: lambdas, streams, and Optional. Master these and your code becomes concise and interview-ready.