Skip to content

Programming Deep Dive — Interview-Grade Topics

The "defend your knowledge under probing" companion to 12_Programming_Basics.

Every topic has: What it isWhy interviewers askCode with explanationFollow-up they'll askWrong answer that gets you rejected.


PART 1: JAVA DEEP DIVE

1.1 — equals() and hashCode() contract

What

When you override equals() in a class, you MUST also override hashCode(). They have a binding contract.

Why interviewers ask

This is the #1 Java bug in production code. Misusing it breaks HashMap, HashSet, and anything that relies on hashing.

The contract (memorize these 3 rules)

  1. If a.equals(b) is true → a.hashCode() == b.hashCode() MUST be true
  2. If a.equals(b) is false → hashCodes MAY still be equal (collision)
  3. hashCode() must return same value across multiple calls (unless object state changes)

Code

public class User {
    private String email;
    private String name;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User u = (User) o;
        return Objects.equals(email, u.email);   // identity = email only
    }

    @Override
    public int hashCode() {
        return Objects.hash(email);   // MUST use same field as equals
    }
}

What breaks if you don't override both

Set<User> users = new HashSet<>();
users.add(new User("a@x.com"));
users.contains(new User("a@x.com"));  // FALSE — different hashCode!

Follow-up they'll ask

  • "What if email is null?"Objects.hash() handles nulls safely
  • "Why use instanceof and not getClass()?"instanceof allows subclasses to be equal; getClass() is stricter. Choice depends on inheritance design.
  • "What's the third equals rule (after reflexive, symmetric)?" → Transitive: if a=b and b=c, then a=c

Wrong answer that gets you rejected

"I override equals to check fields and that's enough." Why wrong: Without hashCode() override, hash-based collections silently break.


1.2 — HashMap internals (Java 8+)

What

Hash table implementation. Key → bucket via hash function. Collisions handled with linked list, upgraded to red-black tree if too many collisions.

Why interviewers ask

Tests if you understand performance characteristics and why hashCode matters.

How it works step by step

  1. put(key, value) calls hash(key) — uses key's hashCode()
  2. Index = hash & (n - 1) where n = bucket array size (always power of 2)
  3. If bucket empty → store
  4. If bucket has entry → check equals() to see if it's update or collision
  5. If collision → append to linked list at that bucket
  6. If list grows to 8+ nodes → convert to red-black tree (O(log n) lookup)
  7. If load factor > 0.75 → resize array (doubles size, rehashes)

Code reading map internals

HashMap<String, Integer> map = new HashMap<>();
// Default capacity: 16 buckets
// Default load factor: 0.75
// Resize triggers at: 16 * 0.75 = 12 entries

map.put("hello", 1);
// 1. hash = hash("hello".hashCode())
// 2. index = hash & 15  (for 16 buckets)
// 3. bucket[index] = new Node("hello", 1)

Follow-up they'll ask

  • "What's the time complexity of get()?" → O(1) average, O(log n) worst (with treeification), O(n) worst-worst (terrible hashCode)
  • "What's load factor?" → Ratio of entries to capacity. Default 0.75 balances memory vs collision rate.
  • "What happens during resize?" → New array (2x size), every entry rehashed. Expensive — pre-size if you know count.
  • "Why bucket size = power of 2?" → Allows hash & (n-1) instead of slow modulo. Bit math is faster.
  • "HashMap vs Hashtable vs ConcurrentHashMap?" → Hashtable is old, synchronized everything (slow). ConcurrentHashMap uses segment locking for thread-safe high performance.

Wrong answer that gets you rejected

"HashMap is O(1) always." Why wrong: Only on average. With bad hashCode, all entries collide → O(n).


1.3 — ArrayList vs LinkedList (real performance)

What

Both implement List but with very different underlying structures.

Why interviewers ask

Common trick question. The "obvious" answer (LinkedList is faster for inserts) is often wrong in practice.

Comparison

Operation ArrayList LinkedList
get(index) O(1) O(n) — must traverse
add(element) (to end) O(1) amortized O(1)
add(0, element) (to start) O(n) — shifts all O(1)
add(middle, element) O(n) — shift O(n) — traverse to find spot
remove(0) O(n) O(1)
Memory per element low (array slot) high (node + prev + next pointers)
Cache locality excellent terrible

The truth (most candidates miss this)

ArrayList wins in practice almost always. Reasons: 1. CPU cache loves contiguous memory (ArrayList) — even O(n) operations on ArrayList beat O(1) on LinkedList because of cache misses 2. LinkedList traversal pollutes the cache 3. Modern CPUs are wildly faster at sequential memory access

Code — when LinkedList actually wins

// LinkedList is fast as a Deque (queue from both ends)
Deque<String> queue = new LinkedList<>();
queue.addFirst("a");
queue.addLast("b");
queue.removeFirst();
// But ArrayDeque is usually even better!

Follow-up they'll ask

  • "When would you actually use LinkedList?" → Honestly, rarely. As a Deque, prefer ArrayDeque.
  • "Why is ArrayList.add() O(1) amortized?" → Most adds are O(1). When full, it doubles capacity (O(n) rare event). Averaged over many calls: O(1).
  • "How does ArrayList grow?" → Default: 50% growth (was 100% in older Java). When full, new array allocated, contents copied.

Wrong answer that gets you rejected

"LinkedList is better for frequent insertions." Why wrong: Theory says yes, real-world CPU cache says no — except at the very ends.


1.4 — Why String is immutable

What

You cannot modify a String. All "modifications" create new Strings.

Why immutable?

  1. String pool — JVM caches string literals to save memory. Mutability would break sharing.
  2. Security — strings used in file paths, URLs, class names. Immutability prevents tampering after validation.
  3. HashingString.hashCode() cached after first call. Mutability would invalidate.
  4. Thread safety — immutable objects are inherently thread-safe.

Why interviewers ask

Tests if you understand JVM design philosophy.

The String pool

String a = "hello";      // goes into String pool
String b = "hello";      // reuses the SAME object from pool
String c = new String("hello");  // forces new object on HEAP

a == b;            // true — same reference
a == c;            // false — different objects
a.equals(c);       // true — content match
c.intern() == a;   // true — intern() returns pool reference

Follow-up they'll ask

  • "How many String objects: String s = new String("hi");" → Two! "hi" in pool, plus new on heap.
  • "What is StringBuilder vs StringBuffer?" → Both mutable. StringBuffer is synchronized (slower), StringBuilder is not (faster, single-thread). Use StringBuilder unless you need thread safety.
  • "Why does s += "x" in a loop kill performance?" → Each iteration creates new String + new char array. Use StringBuilder.

Wrong answer that gets you rejected

"Strings are immutable so they can't be changed." Why wrong: That's the WHAT. The interviewer wants the WHY (4 reasons above).


1.5 — Checked vs Unchecked exceptions

What

  • Checked: Must be declared in throws or caught. Compiler enforces. Extends Exception.
  • Unchecked: No compile-time enforcement. Extends RuntimeException.

Why interviewers ask

Tests understanding of Java's design philosophy and error handling.

Hierarchy

Throwable
├── Error (system, unrecoverable — OutOfMemoryError, StackOverflowError)
└── Exception
    ├── RuntimeException (unchecked — NullPointerException, IllegalArgumentException)
    └── Other (checked — IOException, SQLException, ClassNotFoundException)

Code

// Checked — compiler forces you to handle
public void readFile() throws IOException {       // must declare
    FileReader f = new FileReader("a.txt");
}

// Or catch
public void readFile() {
    try {
        FileReader f = new FileReader("a.txt");
    } catch (IOException e) {
        // handle
    }
}

// Unchecked — no compiler enforcement
public void divide(int a, int b) {
    int x = a / b;          // throws ArithmeticException if b=0, no declaration needed
}

When to use which

  • Checked for recoverable problems (file missing → user can retry)
  • Unchecked for programming errors (null reference, illegal argument)

Follow-up they'll ask

  • "Why do many devs hate checked exceptions?" → Forces verbose throws chains. Breaks lambdas (lambda can't throw checked). Modern libraries (Spring, Jakarta) lean unchecked.
  • "What does try-with-resources do?" → Auto-calls close() on resources, even if exception thrown. Requires AutoCloseable.
  • "What's exception chaining?"throw new MyException("wrapper", originalException) preserves cause.

Wrong answer that gets you rejected

"Errors and Exceptions are the same." Why wrong: Error = JVM-level, don't catch. Exception = app-level, can handle.


1.6 — Stream API (Java 8+)

What

Functional-style operations on collections. Lazy, chainable, supports parallel.

Why interviewers ask

Modern Java code uses streams heavily. Confirms you're not stuck in Java 6.

Core pattern

List<String> names = List.of("Rohan", "Asha", "Bob", "Carol");

List<String> result = names.stream()
    .filter(n -> n.length() > 3)        // intermediate (lazy)
    .map(String::toUpperCase)           // intermediate (lazy)
    .sorted()                            // intermediate (lazy)
    .collect(Collectors.toList());      // terminal (triggers)
// [ASHA, CAROL, ROHAN]

Operations

Intermediate (return Stream, lazy): - filter(predicate) — keep matching - map(function) — transform each - flatMap(function) — flatten nested - sorted(), distinct(), limit(n), skip(n)

Terminal (trigger execution): - collect(collector) — to List, Set, Map - forEach(action) — side effect - count(), sum(), min(), max() - reduce(identity, accumulator) - anyMatch(), allMatch(), noneMatch() - findFirst(), findAny() — returns Optional

Real example

// Total age of adult users
int totalAge = users.stream()
    .filter(u -> u.getAge() >= 18)
    .mapToInt(User::getAge)
    .sum();

// Group by city
Map<String, List<User>> byCity = users.stream()
    .collect(Collectors.groupingBy(User::getCity));

// Average response time
double avg = responses.stream()
    .mapToLong(Response::getTimeMs)
    .average()
    .orElse(0);

Method references

Syntax Meaning
String::toUpperCase s -> s.toUpperCase()
System.out::println x -> System.out.println(x)
User::new () -> new User()
User::getName u -> u.getName()

Follow-up they'll ask

  • "Stream vs Collection?" → Collection holds data. Stream processes data. Streams are lazy and single-use.
  • "What's parallelStream?" → Splits into chunks, processes on ForkJoinPool. Use only for CPU-bound, large datasets. Beware of side effects.
  • "What does Collector do?" → Defines how to accumulate stream elements. Collectors.toList(), toMap(), groupingBy(), joining(",").

Wrong answer that gets you rejected

"I always use streams instead of for loops." Why wrong: Streams have overhead. For simple iteration, a for-loop is faster and clearer.


1.7 — Functional Interfaces

What

An interface with exactly one abstract method (SAM — Single Abstract Method). Can be used as lambda target.

Why interviewers ask

Foundation of lambdas and Stream API.

The Big 4

// 1. Function<T, R> — take T, return R
Function<String, Integer> length = s -> s.length();
length.apply("hello");   // 5

// 2. Predicate<T> — take T, return boolean
Predicate<Integer> isEven = n -> n % 2 == 0;
isEven.test(4);   // true

// 3. Consumer<T> — take T, return void
Consumer<String> printer = s -> System.out.println(s);
printer.accept("hi");

// 4. Supplier<T> — take nothing, return T
Supplier<LocalDate> today = () -> LocalDate.now();
today.get();

Custom functional interface

@FunctionalInterface           // optional but recommended (compiler check)
interface Calculator {
    int calculate(int a, int b);
}

Calculator add = (a, b) -> a + b;
add.calculate(2, 3);   // 5

Follow-up they'll ask

  • "What's BiFunction?" → Like Function but takes 2 args: (a, b) -> result
  • "UnaryOperator?" → Function — same input/output type
  • "Why @FunctionalInterface annotation?" → Compiler error if interface has more than one abstract method

1.8 — Generics (bounded types, wildcards, type erasure)

What

Type parameters that let you write classes/methods working with any type while preserving type safety.

Why interviewers ask

Common source of bugs and confusion. Tests deep Java knowledge.

Basic generic class

public class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

Box<String> b = new Box<>();
b.set("hello");
// b.set(42);  // compile error — type safety!

Bounded types

// T must be Number or subclass
public class NumberBox<T extends Number> {
    public double doubled() { return value.doubleValue() * 2; }
}

NumberBox<Integer> ok = new NumberBox<>();
// NumberBox<String> bad = new NumberBox<>();  // compile error

Wildcards

// ? extends — covariance (read-only from list's perspective)
public double sum(List<? extends Number> list) {
    double total = 0;
    for (Number n : list) total += n.doubleValue();
    return total;
    // list.add(...)  // ERROR — can't add to ? extends
}

// ? super — contravariance (write-only)
public void addNumbers(List<? super Integer> list) {
    list.add(1);    // OK
    list.add(2);
}

// PECS mnemonic: Producer Extends, Consumer Super
// If you READ from collection — use extends
// If you WRITE to collection — use super

Type erasure (tricky!)

At runtime, generic types are erased to Object. The compiler enforces types at compile time, then strips them.

List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
a.getClass() == b.getClass();   // true — both are ArrayList at runtime!

// Cannot do:
public <T> T create() {
    return new T();           // ERROR — T erased
}
public boolean check(Object o) {
    return o instanceof List<String>;   // ERROR
}

Follow-up they'll ask

  • "Why type erasure?" → Backward compatibility with pre-generics Java
  • "What's a raw type?"List (no <>) — bypasses type checking, dangerous
  • "Diff between List<Object> and List<?>?"List<Object> only accepts List. List<?> accepts ANY parameterized List.

    Wrong answer that gets you rejected

    "Generics give runtime type safety." Why wrong: Only compile-time. At runtime, List<String> and List<Integer> are identical.


    1.9 — Comparable vs Comparator

    What

    Both for sorting. Comparable = natural ordering on the class itself. Comparator = external custom ordering.

    Code

    // Comparable — sort User by name naturally
    public class User implements Comparable<User> {
        String name;
        @Override
        public int compareTo(User other) {
            return this.name.compareTo(other.name);
        }
    }
    Collections.sort(userList);   // uses User's natural order
    
    // Comparator — custom ordering, no class change needed
    Comparator<User> byAge = (a, b) -> Integer.compare(a.age, b.age);
    Collections.sort(userList, byAge);
    
    // Modern syntax
    userList.sort(Comparator.comparing(User::getAge));
    userList.sort(Comparator.comparing(User::getAge).reversed());
    userList.sort(Comparator.comparing(User::getCity)
                            .thenComparing(User::getAge));
    

    Contract: compareTo / compare

    • Negative if a < b
    • Zero if a == b
    • Positive if a > b

    Follow-up they'll ask

    • "When to use which?" → Comparable for the one "natural" ordering. Comparator for multiple custom orderings.
    • "Must compareTo be consistent with equals?" → Strongly recommended. Inconsistency breaks TreeMap/TreeSet.

    1.10 — Concurrency basics

    What

    Java has built-in multithreading. Tests touch threads, locks, atomic operations.

    Thread creation

    // Old way
    Thread t = new Thread(() -> System.out.println("Hi"));
    t.start();
    
    // Modern — ExecutorService
    ExecutorService pool = Executors.newFixedThreadPool(4);
    pool.submit(() -> doWork());
    Future<String> f = pool.submit(() -> "result");
    String result = f.get();   // blocks
    pool.shutdown();
    

    synchronized

    // Method-level lock
    public synchronized void increment() {
        count++;
    }
    
    // Block-level lock
    public void increment() {
        synchronized (this) {
            count++;
        }
    }
    

    volatile

    Ensures visibility across threads (writes are immediately visible to other threads). Does NOT ensure atomicity!

    private volatile boolean stopFlag = false;
    // Thread A sets stopFlag = true
    // Thread B sees the change immediately
    

    Atomic classes

    AtomicInteger count = new AtomicInteger(0);
    count.incrementAndGet();   // thread-safe, no lock needed
    count.compareAndSet(5, 10);   // CAS operation
    

    Follow-up they'll ask

    • "synchronized vs volatile?" → synchronized = mutex + visibility. volatile = visibility only.
    • "What's a race condition?" → Two threads access shared state, at least one writes, no synchronization → unpredictable result
    • "Deadlock?" → Thread A holds lock X waiting for Y; Thread B holds Y waiting for X. Both stuck forever.
    • "ConcurrentHashMap vs synchronized HashMap?" → ConcurrentHashMap uses bucket-level locking → way faster under contention.

    Wrong answer that gets you rejected

    "volatile makes operations atomic." Why wrong: volatile = visibility only. count++ on volatile is still NOT atomic (read-modify-write).


    1.11 — final keyword (3 uses)

    // 1. final variable — cannot reassign
    final int MAX = 100;
    final List<String> list = new ArrayList<>();
    list.add("x");           // OK — modifying contents
    // list = new ArrayList<>(); // ERROR — reassignment
    
    // 2. final method — cannot override in subclass
    public class Animal {
        public final void breathe() { ... }
    }
    
    // 3. final class — cannot be extended
    public final class String { ... }    // hence we can't subclass String
    

    1.12 — Singleton pattern (interview classic)

    // Lazy + thread-safe (double-checked locking)
    public class Singleton {
        private static volatile Singleton instance;
    
        private Singleton() {}    // private constructor
    
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    
    // Modern — enum (Joshua Bloch's recommended approach)
    public enum Singleton {
        INSTANCE;
        public void doWork() { ... }
    }
    

    PART 2: PYTHON DEEP DIVE

    2.1 — List comprehensions vs map/filter

    What

    Concise way to build lists/dicts/sets from iterables. Pythonic alternative to map/filter.

    Why interviewers ask

    Differentiates Python beginners from those who write idiomatic code.

    List comprehension

    # Basic
    squares = [n ** 2 for n in range(10)]
    # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    
    # With filter
    evens = [n for n in range(10) if n % 2 == 0]
    # [0, 2, 4, 6, 8]
    
    # Nested
    pairs = [(x, y) for x in [1, 2] for y in [3, 4]]
    # [(1,3), (1,4), (2,3), (2,4)]
    
    # vs map/filter (less Pythonic)
    squares = list(map(lambda n: n ** 2, range(10)))
    evens = list(filter(lambda n: n % 2 == 0, range(10)))
    

    Dict and set comprehensions

    # Dict
    sq_map = {n: n ** 2 for n in range(5)}
    # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
    
    # Set
    uniques = {n % 3 for n in range(10)}
    # {0, 1, 2}
    

    Generator expression (key difference!)

    # List comp — materializes ALL values immediately
    nums = [n ** 2 for n in range(10**6)]   # big memory
    
    # Generator — lazy, one at a time
    nums = (n ** 2 for n in range(10**6))   # parens! memory efficient
    sum(nums)                                # iterates lazily
    

    Follow-up they'll ask

    • "When to use comprehension vs map?" → Comprehension is Pythonic for transform + filter. Map for existing functions: map(int, str_list).
    • "What's the memory impact?" → List comp = O(n). Generator = O(1). Use generator for large data.

    Wrong answer that gets you rejected

    "Comprehensions and generators are the same." Why wrong: Comprehension creates the full list. Generator is lazy.


    2.2 — Generators and yield

    What

    A function that produces values lazily, one at a time, instead of returning a full list.

    Why interviewers ask

    Critical for memory-efficient processing of large data streams.

    Basic generator

    def counter(n):
        i = 0
        while i < n:
            yield i        # pauses here, resumes on next()
            i += 1
    
    g = counter(3)
    next(g)   # 0
    next(g)   # 1
    next(g)   # 2
    next(g)   # StopIteration
    
    # Or just loop
    for n in counter(3):
        print(n)
    

    Real use case — process a huge file

    def read_lines(file_path):
        with open(file_path) as f:
            for line in f:           # file objects are themselves generators
                yield line.strip()
    
    # Process 10 GB file without loading all into memory
    for line in read_lines("huge.log"):
        if "ERROR" in line:
            print(line)
    

    Generators as state machines

    def running_total():
        total = 0
        while True:
            x = yield total       # receives via .send()
            if x is None:
                break
            total += x
    
    g = running_total()
    next(g)            # 0 (initial yield)
    g.send(5)          # 5
    g.send(3)          # 8
    g.send(10)         # 18
    

    Follow-up they'll ask

    • "yield vs return?" → return ends function, gives single value. yield pauses function, can resume.
    • "Generator vs iterator?" → Generator is a special, simpler kind of iterator. All generators are iterators; not vice versa.
    • "Why use generators?" → Memory efficiency. Lazy evaluation. Pipelines of transformations.

    2.3 — Decorators (must-know)

    What

    A function that wraps another function to extend its behavior, without modifying it.

    Why interviewers ask

    Decorators are everywhere in Python — Flask routes, pytest fixtures, caching, timing, logging.

    Basic decorator

    def my_decorator(func):
        def wrapper(*args, **kwargs):
            print(f"Calling {func.__name__}")
            result = func(*args, **kwargs)
            print(f"Done {func.__name__}")
            return result
        return wrapper
    
    @my_decorator
    def greet(name):
        return f"Hello, {name}"
    
    greet("Rohan")
    # Calling greet
    # Done greet
    

    What @ syntax actually does

    # These are equivalent:
    @my_decorator
    def greet(name): ...
    
    # Same as:
    def greet(name): ...
    greet = my_decorator(greet)
    

    Real-world: caching/memoization

    def cache(fn):
        memo = {}
        def wrapper(x):
            if x not in memo:
                memo[x] = fn(x)
            return memo[x]
        return wrapper
    
    @cache
    def fib(n):
        if n < 2: return n
        return fib(n - 1) + fib(n - 2)
    
    fib(100)   # fast, no decorator → would take forever
    

    Decorator with arguments

    def repeat(times):
        def decorator(fn):
            def wrapper(*args, **kwargs):
                for _ in range(times):
                    fn(*args, **kwargs)
            return wrapper
        return decorator
    
    @repeat(3)
    def say_hi():
        print("Hi")
    
    say_hi()   # prints Hi 3 times
    

    Preserve metadata with functools.wraps

    from functools import wraps
    
    def my_decorator(fn):
        @wraps(fn)           # preserves fn's __name__, __doc__
        def wrapper(*args, **kwargs):
            return fn(*args, **kwargs)
        return wrapper
    

    Follow-up they'll ask

    • "Why @wraps?" → Without it, the decorated function loses its original name and docstring → breaks introspection, debugging.
    • "Decorator vs middleware?" → Same concept. Middleware is the web framework word for it.
    • "Can you chain decorators?" → Yes. @a @b def f(): ... is f = a(b(f)) — applied bottom-up.

    2.4 — Context managers (with statement)

    What

    Handles setup + teardown automatically. Most famous: file handling.

    Why interviewers ask

    Shows you write clean, leak-free code.

    Built-in usage

    # Without context manager — must remember to close
    f = open("file.txt")
    data = f.read()
    f.close()              # easy to forget; doesn't run if exception
    
    # With context manager — auto close
    with open("file.txt") as f:
        data = f.read()
    # f.close() called automatically, even on exception
    

    Write your own

    class Timer:
        def __enter__(self):
            self.start = time.time()
            return self
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            elapsed = time.time() - self.start
            print(f"Elapsed: {elapsed:.2f}s")
            return False    # don't suppress exceptions
    
    with Timer():
        expensive_operation()
    

    Easier with @contextmanager

    from contextlib import contextmanager
    
    @contextmanager
    def timer():
        start = time.time()
        try:
            yield
        finally:
            print(f"Elapsed: {time.time() - start:.2f}s")
    
    with timer():
        expensive_operation()
    

    Real test usage

    # Pytest temporary directories
    with tempfile.TemporaryDirectory() as tmp:
        write_file(tmp)
        # Folder auto-deleted
    
    # Mocking
    with mock.patch("requests.get") as mock_get:
        mock_get.return_value.json.return_value = {"id": 1}
        result = my_function()
    

    Follow-up they'll ask

    • "What's enter and exit?" → The protocol that makes with work. Any class implementing both can be used in with.
    • "What if exit returns True?" → Suppresses exception. Use carefully — often a bad idea.

    2.5 — GIL (Global Interpreter Lock)

    What

    A mutex in CPython that allows only ONE thread to execute Python bytecode at a time.

    Why interviewers ask

    Common source of confusion. Affects how you do concurrency in Python.

    Why GIL exists

    • Simplifies CPython's reference counting (memory management)
    • Makes single-threaded code faster
    • Removing it would break tons of C extensions

    What it means in practice

    import threading
    
    def cpu_work():
        n = 0
        for i in range(10**8):
            n += i
    
    # Spinning up 4 threads for CPU work does NOT speed up
    threads = [threading.Thread(target=cpu_work) for _ in range(4)]
    # Same time as running serially — GIL prevents parallelism
    

    When GIL doesn't hurt (I/O bound)

    # Threads ARE useful for I/O — GIL is released during I/O wait
    def fetch_url(url):
        return requests.get(url).text    # GIL released during network wait
    
    threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]
    # Much faster than serial — threads run concurrently during I/O
    

    CPU-bound? Use multiprocessing

    from multiprocessing import Pool
    
    with Pool(4) as p:
        results = p.map(cpu_work, items)
    # Separate processes = separate GILs = true parallelism
    

    Follow-up they'll ask

    • "Why is multiprocessing 'true' parallel?" → Each process has its own Python interpreter and GIL.
    • "Is GIL going away?" → PEP 703 (no-GIL Python) is in progress — experimental in Python 3.13+.
    • "What about asyncio?" → Single-threaded but uses cooperative multitasking — great for I/O concurrency.

    Wrong answer that gets you rejected

    "Threading doesn't work in Python." Why wrong: It works great for I/O. Just not for CPU-bound work.


    2.6 — args / *kwargs deep dive

    What

    • *args — collects positional arguments into a tuple
    • **kwargs — collects keyword arguments into a dict

    Code

    def example(*args, **kwargs):
        print(args)      # tuple
        print(kwargs)    # dict
    
    example(1, 2, 3, name="Rohan", age=28)
    # (1, 2, 3)
    # {'name': 'Rohan', 'age': 28}
    

    Unpacking (opposite direction)

    nums = [1, 2, 3]
    print(*nums)        # print(1, 2, 3)
    
    config = {"a": 1, "b": 2}
    some_func(**config)  # some_func(a=1, b=2)
    

    Forwarding to wrapped function

    def wrapper(*args, **kwargs):
        print("calling")
        return original(*args, **kwargs)
    

    Follow-up they'll ask

    • "Order of args in signature?"def f(pos, *args, kw_only, **kwargs) — pos, then args, then keyword-only, then *kwargs
    • "What's a keyword-only argument?" → After * in signature: def f(a, *, b) — b MUST be passed as b=...

    2.7 — Magic methods (dunder methods)

    What

    Special methods with double underscores. Define how objects behave with built-in operations.

    Why interviewers ask

    Tests if you can write Pythonic, idiomatic classes.

    Most important magic methods

    class User:
        def __init__(self, name, age):           # constructor
            self.name = name
            self.age = age
    
        def __str__(self):                       # human-readable
            return f"{self.name} ({self.age})"
    
        def __repr__(self):                      # developer-readable, unambiguous
            return f"User(name='{self.name}', age={self.age})"
    
        def __eq__(self, other):                 # ==
            if not isinstance(other, User):
                return False
            return self.name == other.name
    
        def __hash__(self):                      # for use in sets/dicts
            return hash(self.name)
    
        def __lt__(self, other):                 # <  (also __le__, __gt__, __ge__)
            return self.age < other.age
    
        def __len__(self):                       # len(user)
            return len(self.name)
    
        def __getitem__(self, key):              # user[key]
            return self.__dict__[key]
    
        def __call__(self, *args):               # user() — makes instance callable
            print(f"Called with {args}")
    

    str vs repr (common Q)

    • __str__ — for end users (print(obj) calls this)
    • __repr__ — for developers (debugger, repr(obj), REPL)
    • If only one, define __repr__ — it's the fallback for __str__

    Follow-up they'll ask

    • "Why both eq and hash?" → Same contract as Java. Equal objects must have equal hashes.
    • "What's new vs init?"__new__ creates the object. __init__ initializes it. You rarely override new.

    2.8 — Mutable default argument trap (classic gotcha)

    The bug

    def add_item(item, lst=[]):     # BUG! Default list is shared!
        lst.append(item)
        return lst
    
    add_item(1)    # [1]
    add_item(2)    # [1, 2]  — same list reused!
    add_item(3)    # [1, 2, 3]
    

    Why it happens

    Default values are evaluated ONCE at function definition. The empty list is created at definition, shared across all calls without a fresh list passed in.

    The fix

    def add_item(item, lst=None):
        if lst is None:
            lst = []
        lst.append(item)
        return lst
    

    Why interviewers ask

    This catches even experienced Python devs. Tests deep understanding of evaluation timing.


    2.9 — Shallow vs Deep copy

    What

    • Shallow copy: New container, but inner objects are shared references
    • Deep copy: Recursively copies everything

    Code

    import copy
    
    original = [[1, 2], [3, 4]]
    
    shallow = copy.copy(original)
    # or: shallow = original[:]
    # or: shallow = list(original)
    
    shallow[0].append(99)
    print(original)   # [[1, 2, 99], [3, 4]]  — inner list modified!
    
    deep = copy.deepcopy(original)
    deep[0].append(100)
    print(original)   # unchanged
    

    When to use

    • Shallow: when contents are immutable (numbers, strings)
    • Deep: when contents are mutable and you need full independence

    2.10 — is vs ==

    Difference

    • == — equality (calls __eq__)
    • is — identity (same object in memory)
    a = [1, 2]
    b = [1, 2]
    c = a
    
    a == b      # True (content equal)
    a is b      # False (different objects)
    a is c      # True (same object)
    
    # Use 'is' for None
    x is None       # CORRECT
    x == None       # works but bad style
    

    Gotcha: small integer caching

    a = 100
    b = 100
    a is b      # True — CPython caches small ints (-5 to 256)
    
    a = 1000
    b = 1000
    a is b      # False — outside cache range
    

    2.11 — Iterator vs Iterable vs Generator

    Definitions

    • Iterable: Has __iter__ returning an iterator. Examples: list, dict, set, str.
    • Iterator: Has __next__ returning next value. Created by iter(iterable).
    • Generator: Special iterator created with yield.

    Code

    lst = [1, 2, 3]          # iterable
    it = iter(lst)           # iterator
    next(it)                 # 1
    next(it)                 # 2
    next(it)                 # 3
    next(it)                 # StopIteration
    
    # All these work with for-loop because they're iterable:
    for x in [1, 2, 3]: ...        # list
    for x in "hello": ...          # string
    for x in {"a": 1}: ...         # dict (iterates keys)
    for x in (n*2 for n in [1,2]): # generator
    

    2.12 — Multiple inheritance & MRO

    What

    Python supports multiple inheritance. MRO = Method Resolution Order — what order to look up methods.

    Code

    class A:
        def greet(self): print("A")
    
    class B(A):
        def greet(self): print("B")
    
    class C(A):
        def greet(self): print("C")
    
    class D(B, C):
        pass
    
    d = D()
    d.greet()        # B
    print(D.__mro__)
    # (D, B, C, A, object)
    

    MRO uses C3 linearization — left-to-right, depth-first, no duplicates.


    PART 3: JAVASCRIPT DEEP DIVE

    3.1 — Event loop (THE most important JS concept)

    What

    JavaScript is single-threaded but handles async via the event loop. Tasks queue up and the engine processes them one at a time.

    Why interviewers ask

    The #1 differentiator between junior and mid-level JS devs.

    Components

    1. Call Stack — currently executing code
    2. Web APIs / Node APIs — browser/Node features (setTimeout, fetch, fs)
    3. Microtask Queue — Promise callbacks, queueMicrotask
    4. Macrotask Queue — setTimeout, setInterval, I/O, UI rendering

    The rule

    After every macrotask, process ALL microtasks before the next macrotask.

    Classic interview trick

    console.log(1);
    setTimeout(() => console.log(2), 0);
    Promise.resolve().then(() => console.log(3));
    console.log(4);
    
    // Output: 1, 4, 3, 2
    

    Why this order: 1. console.log(1) — synchronous, runs immediately → 1 2. setTimeout — registers callback to macrotask queue 3. Promise.then — registers callback to microtask queue 4. console.log(4) — synchronous → 4 5. Stack empty → drain microtasks → 3 6. Then macrotasks → 2

    Follow-up they'll ask

    • "Why microtasks before macrotasks?" → Promise resolutions should happen ASAP for responsive UI
    • "What's queueMicrotask?" → Schedules a microtask explicitly (rarely needed in app code)
    • "setTimeout(fn, 0) — does it run immediately?" → No! It's queued as macrotask, runs after current sync code + all microtasks

    3.2 — Hoisting (var vs let vs const vs function)

    What

    JS "moves" declarations to the top of the scope at compile time. Behavior differs by declaration type.

    var is hoisted as undefined

    console.log(x);    // undefined (NOT error)
    var x = 5;
    
    // JS engine sees this as:
    // var x;
    // console.log(x);
    // x = 5;
    

    let and const are hoisted but in "Temporal Dead Zone"

    console.log(y);    // ReferenceError
    let y = 5;
    

    Function declarations fully hoisted

    hello();           // works!
    function hello() { console.log("hi"); }
    

    Function expressions NOT hoisted

    hello();           // TypeError: hello is not a function
    var hello = function() { console.log("hi"); };
    

    Why interviewers ask

    Catches devs who don't understand JS scoping.


    3.3 — Closures

    What

    A function that "remembers" variables from its outer scope, even after the outer function returns.

    Why interviewers ask

    The most important JS concept after the event loop. Used everywhere in real code.

    Basic

    function makeCounter() {
        let count = 0;
        return function() {
            count++;
            return count;
        };
    }
    
    const counter = makeCounter();
    counter();  // 1
    counter();  // 2
    counter();  // 3
    // count is "trapped" inside the closure
    

    Real-world: data privacy

    function createBankAccount(initial) {
        let balance = initial;       // private
    
        return {
            deposit(amount) { balance += amount; },
            withdraw(amount) {
                if (amount > balance) throw "Insufficient";
                balance -= amount;
            },
            getBalance() { return balance; }
        };
    }
    
    const account = createBankAccount(100);
    account.deposit(50);
    account.getBalance();   // 150
    account.balance;        // undefined — truly private
    

    Real-world: debounce (commonly asked!)

    function debounce(fn, ms) {
        let timeoutId;
        return function(...args) {
            clearTimeout(timeoutId);
            timeoutId = setTimeout(() => fn(...args), ms);
        };
    }
    
    const onSearch = debounce(searchAPI, 300);
    // Calling onSearch repeatedly only fires the last call after 300ms idle
    

    Real-world: throttle

    function throttle(fn, ms) {
        let lastCalled = 0;
        return function(...args) {
            const now = Date.now();
            if (now - lastCalled >= ms) {
                lastCalled = now;
                fn(...args);
            }
        };
    }
    

    Closure gotcha in loops

    // BUG with var
    for (var i = 0; i < 3; i++) {
        setTimeout(() => console.log(i), 0);
    }
    // Prints: 3, 3, 3 — all closures share the same i
    
    // FIX with let
    for (let i = 0; i < 3; i++) {
        setTimeout(() => console.log(i), 0);
    }
    // Prints: 0, 1, 2 — let creates new binding per iteration
    

    Follow-up they'll ask

    • "Memory implications of closures?" → They keep referenced vars alive. Forgotten closures can cause memory leaks.
    • "Implement once()" → Function that can only be called one time
      function once(fn) {
          let called = false;
          return function(...args) {
              if (called) return;
              called = true;
              return fn(...args);
          };
      }
      

    3.4 — Prototypes & Prototypal Inheritance

    What

    JS uses prototypes, not classes (under the hood). Every object has an internal [[Prototype]] link to another object.

    Why interviewers ask

    class syntax (ES6) hides the truth. Senior devs must know the underlying model.

    How it works

    const animal = {
        breathe() { console.log("Breathing"); }
    };
    
    const dog = Object.create(animal);   // dog's prototype = animal
    dog.bark = function() { console.log("Woof"); };
    
    dog.bark();      // Woof — found on dog
    dog.breathe();   // Breathing — not on dog, looks up prototype chain
    
    // Chain: dog → animal → Object.prototype → null
    

    Class is syntactic sugar

    class Animal {
        breathe() { console.log("Breathing"); }
    }
    class Dog extends Animal {
        bark() { console.log("Woof"); }
    }
    
    // Equivalent to:
    function Animal() {}
    Animal.prototype.breathe = function() { console.log("Breathing"); };
    
    function Dog() {}
    Dog.prototype = Object.create(Animal.prototype);
    Dog.prototype.bark = function() { console.log("Woof"); };
    

    Follow-up they'll ask

    • "What's proto?" → Direct access to the prototype link (use Object.getPrototypeOf instead in modern code)
    • "prototype vs proto?"prototype is a property on constructor functions. __proto__ is on instances.
    • "How does method lookup work?" → JS walks the prototype chain. Stops at first match or returns undefined.

    3.5 — this binding (call, apply, bind)

    The 4 rules of this

    // 1. Default — `this` = global (or undefined in strict mode)
    function f() { console.log(this); }
    f();   // window/global (or undefined)
    
    // 2. Implicit — `this` = object before the dot
    const obj = { name: "Rohan", greet() { console.log(this.name); } };
    obj.greet();   // "Rohan"
    
    // 3. Explicit — call, apply, bind
    function greet(greeting) { console.log(`${greeting}, ${this.name}`); }
    const user = { name: "Rohan" };
    
    greet.call(user, "Hi");           // Hi, Rohan
    greet.apply(user, ["Hello"]);     // Hello, Rohan
    const bound = greet.bind(user);   // returns new function
    bound("Hey");                      // Hey, Rohan
    
    // 4. new keyword
    function User(name) { this.name = name; }
    const u = new User("Rohan");
    

    Arrow function exception

    Arrow functions don't have their own this. They inherit from surrounding scope.

    const obj = {
        name: "Rohan",
        greet: function() {
            setTimeout(function() {
                console.log(this.name);   // undefined — `this` lost
            }, 100);
    
            setTimeout(() => {
                console.log(this.name);   // "Rohan" — arrow inherits
            }, 100);
        }
    };
    

    Follow-up they'll ask

    • "When should you NOT use arrow functions?" → As object methods, constructors, or when you need dynamic this
    • "call vs apply vs bind?" → call/apply invoke immediately; bind returns a new function. call takes args separately, apply takes array.

    3.6 — Promises in depth

    Promise states

    • Pending — initial state
    • Fulfilled — operation succeeded (.then() runs)
    • Rejected — operation failed (.catch() runs)
    • Settled — either fulfilled or rejected

    Creating promises

    const p = new Promise((resolve, reject) => {
        setTimeout(() => {
            if (Math.random() > 0.5) resolve("success");
            else reject("fail");
        }, 1000);
    });
    
    p.then(data => console.log(data))
     .catch(err => console.error(err))
     .finally(() => console.log("always"));
    

    Promise.all vs allSettled vs race vs any

    // Promise.all — fails fast on first rejection
    const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
    
    // Promise.allSettled — waits for all, returns results array
    const results = await Promise.allSettled([fetchA(), fetchB()]);
    // [{status: "fulfilled", value: ...}, {status: "rejected", reason: ...}]
    
    // Promise.race — first to settle (resolve or reject) wins
    const fastest = await Promise.race([fetchA(), fetchB()]);
    
    // Promise.any — first to RESOLVE wins (ignores rejections until all fail)
    const firstSuccess = await Promise.any([fetchA(), fetchB()]);
    

    Common interview problem: implement Promise.all

    function promiseAll(promises) {
        return new Promise((resolve, reject) => {
            const results = [];
            let completed = 0;
            promises.forEach((p, i) => {
                Promise.resolve(p)
                    .then(value => {
                        results[i] = value;
                        completed++;
                        if (completed === promises.length) resolve(results);
                    })
                    .catch(reject);
            });
        });
    }
    

    async/await is syntax over Promises

    // These are equivalent:
    async function getUser() {
        const res = await fetch("/api/user");
        return res.json();
    }
    
    // Same as:
    function getUser() {
        return fetch("/api/user").then(res => res.json());
    }
    

    Common gotcha — forgetting await

    async function bad() {
        const data = fetch("/api/data");   // BUG — no await
        console.log(data);                 // Promise object, not data
    }
    

    Follow-up they'll ask

    • "What if Promise.all rejects? Do the other promises stop?" → No — they run to completion. Promise.all just rejects with first error.
    • "Difference between Promise.race and Promise.any?" → race = first to settle (success or fail). any = first to SUCCEED.

    3.7 — Destructuring patterns

    Array destructuring

    const [a, b, c] = [1, 2, 3];
    const [first, ...rest] = [1, 2, 3, 4];   // first=1, rest=[2,3,4]
    const [a, , c] = [1, 2, 3];               // skip middle
    const [a = 10] = [];                       // default value
    

    Object destructuring

    const { name, age } = { name: "Rohan", age: 28 };
    const { name: userName } = obj;             // rename
    const { city = "Bangalore" } = obj;         // default
    
    // Nested
    const { user: { address: { city } } } = data;
    
    // Function parameter destructuring (super common!)
    function greet({ name, greeting = "Hi" } = {}) {
        return `${greeting}, ${name}`;
    }
    greet({ name: "Rohan" });
    

    Swapping variables

    let a = 1, b = 2;
    [a, b] = [b, a];   // a=2, b=1
    

    3.8 — Spread vs Rest operators

    Both use ... but in different contexts.

    Spread — expands

    const arr1 = [1, 2, 3];
    const arr2 = [...arr1, 4, 5];          // [1,2,3,4,5]
    
    const obj1 = { a: 1 };
    const obj2 = { ...obj1, b: 2 };        // {a:1, b:2}
    
    function add(a, b, c) { return a + b + c; }
    add(...[1, 2, 3]);                     // spread as args
    

    Rest — collects

    function sum(...nums) {                // rest params
        return nums.reduce((a, b) => a + b, 0);
    }
    sum(1, 2, 3, 4);                       // nums=[1,2,3,4]
    
    const [first, ...rest] = [1, 2, 3];   // rest in destructuring
    const { a, ...others } = { a: 1, b: 2, c: 3 };
    

    Memory hook

    • Spread: ... on the right side (expanding)
    • Rest: ... on the left side (collecting)

    3.9 — Module systems (CommonJS vs ESM)

    CommonJS (Node.js traditional)

    // math.js
    function add(a, b) { return a + b; }
    module.exports = { add };
    
    // or
    exports.add = add;
    
    // main.js
    const { add } = require("./math");
    

    ES Modules (modern standard)

    // math.js
    export function add(a, b) { return a + b; }
    export default function multiply(a, b) { return a * b; }
    
    // main.js
    import multiply, { add } from "./math.js";
    import * as math from "./math.js";
    

    Key differences

    CommonJS ESM
    Syntax require/module.exports import/export
    Loading Synchronous Asynchronous
    When evaluated Runtime Parse time
    Tree shaking No Yes (smaller bundles)
    File extension .js .mjs or "type": "module" in package.json

    3.10 — Equality (== vs === vs Object.is)

    "5" == 5         // true (type coercion)
    "5" === 5        // false (strict)
    
    null == undefined    // true
    null === undefined   // false
    
    NaN === NaN          // false (!)
    Object.is(NaN, NaN)  // true
    
    0 === -0             // true
    Object.is(0, -0)     // false
    

    Rule: Always use === unless you have a specific reason.


    3.11 — Common gotchas / interview traps

    typeof null

    typeof null;          // "object" — bug from 1995, never fixed
    typeof undefined;     // "undefined"
    typeof [];            // "object"
    typeof function(){};  // "function"
    

    Array.isArray

    Array.isArray([]);          // true — use this
    typeof [] === "object";     // true (not helpful)
    

    Object key order

    const obj = { z: 1, a: 2, 1: 3, 0: 4 };
    Object.keys(obj);    // ["0", "1", "z", "a"]
    // Integer keys first (sorted), then strings (insertion order)
    

    Hoisting + function expression bug

    console.log(typeof foo);   // "undefined"
    console.log(typeof bar);   // "function"
    var foo = function() {};
    function bar() {}
    

    3.12 — Object.freeze, Object.assign, structuredClone

    // Object.freeze — shallow immutability
    const obj = Object.freeze({ a: 1, b: { c: 2 } });
    obj.a = 99;       // silently ignored (strict mode: error)
    obj.b.c = 99;     // WORKS — nested object not frozen
    
    // Object.assign — shallow copy/merge
    const merged = Object.assign({}, obj1, obj2);   // or {...obj1, ...obj2}
    
    // structuredClone — deep copy (modern)
    const deepCopy = structuredClone(original);     // works on arrays, dates, maps
    // Old way: JSON.parse(JSON.stringify(obj)) — loses functions, Dates, undefined
    

    QUICK INTERVIEW DRILL — 15 Questions

    Try answering these from memory. Each is asked frequently.

    Java

    1. What's the equals/hashCode contract?
    2. What's the difference between ArrayList and LinkedList — when is each faster?
    3. Why is String immutable?
    4. What does volatile guarantee that synchronized doesn't?
    5. What is PECS (in generics)?

    Python

    1. What's the difference between a list comprehension and a generator expression?
    2. What does @property decorator do?
    3. What is the GIL and when does it not matter?
    4. Why is def f(x=[]) a bug?
    5. What's is vs ==?

    JavaScript

    1. What does setTimeout(fn, 0) actually do?
    2. Write a debounce function
    3. Why does this print 3, 3, 3?
      for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);
      
    4. Promise.all vs Promise.allSettled?
    5. What's this in an arrow function?

    STUDY PLAN

    Day Topics
    1 Java: equals/hashCode, HashMap internals, ArrayList vs LinkedList
    2 Java: Stream API, Generics, Concurrency basics
    3 Python: Generators, Decorators, Context managers
    4 Python: GIL, Magic methods, Mutable defaults
    5 JS: Event loop, Hoisting, Closures
    6 JS: Prototypes, this, Promises
    7 Drill — answer the 15 questions out loud

    Pro tip: For each topic, write a 3-sentence explanation and a 5-line code example. If you can do both from memory, you've internalized it.