Java Integer Caching — The $128 Comparison Bug
Balances over $128 silently corrupt due to Integer caching and == comparison.
- Core Java questions probe the 'why' behind APIs, not just syntax
- JVM internals (heap vs stack, GC, classloaders) are high-probability topics
- Collections: know HashMap internals (buckets, treeification, load factor)
- Concurrency: race conditions, deadlock prevention, and thread pools separate senior from junior
- Java 8+ features (Streams, Optional, lambdas) are table stakes, not bonuses
- Production failures (Integer caching, ConcurrentModificationException) prove deeper understanding
Java has powered enterprise software, Android apps, and backend systems for nearly three decades. That staying power means one thing for developers: Java interview questions are everywhere, and they're getting sharper. Interviewers at companies like Google, Amazon, and mid-size startups all use Java questions to separate candidates who genuinely understand the platform from those who memorised a cheat sheet the night before.
The real problem with most interview prep resources is they give you the answer without the insight. They tell you 'HashMap is not thread-safe' but never explain what actually happens when two threads collide inside one — or why you'd ever choose ConcurrentHashMap over Collections.synchronizedMap(). That gap is exactly what trips people up in real interviews, where follow-up questions are how interviewers find your ceiling.
I've conducted over 200 Java interviews at two companies — a fintech processing 50,000 TPS and a SaaS platform serving 10 million users. The candidates who get offers aren't the ones who memorise definitions. They're the ones who can say 'I've seen this fail in production, and here's how I fixed it.' Every question in this article has been asked in a real interview I've conducted or been asked in. Every answer includes the production context that separates a senior answer from a junior one.
By the end of this article you'll be able to answer all 50 questions confidently, explain the reasoning behind each answer, spot the traps interviewers set, and connect abstract concepts to real production code. We've grouped the questions into logical themes so each section builds on the last — by the end, the pieces snap together into a coherent mental model of how Java actually works.
Core Java & JVM Internals (Questions 1-8)
A senior Java developer must understand how the JVM manages memory. One of the most common questions involves the difference between the Stack and the Heap. The Stack is used for static memory allocation and execution of a thread, while the Heap is used for dynamic memory allocation of Java objects. Understanding the Garbage Collection (GC) roots and how the 'Stop-the-World' phase affects application latency is crucial for production-grade backend engineering.
Question 1: What is the difference between Stack and Heap memory?
Stack: per-thread, stores local variables, method parameters, return addresses. Fast access (pointer arithmetic). Auto-managed (popped when method returns). Fixed size per thread (-Xss flag).
Heap: shared across all threads, stores all Java objects and arrays. Slower access (requires pointer dereference). Managed by garbage collector. Sized by -Xms/-Xmx.
Production insight: I once debugged a service that was OOM-killed in Kubernetes not because of heap, but because 2000 threads × 1 MB stacks = 2 GB of stack memory on a 4 GB container. The heap was only using 1.5 GB. Always account for thread count × stack size when sizing containers.
Question 2: What is the difference between == and .equals()?
== compares references (memory addresses). .equals() compares content (values). For primitives, == compares values directly. For objects, == checks if both references point to the same object in memory.
The trap: Integer caching. Integer.valueOf(127) == Integer.valueOf(127) returns true (cached). Integer.valueOf(128) == Integer.valueOf(128) returns false (not cached). This bites developers who use == to compare Integer objects instead of .equals().
Question 3: Why is String immutable in Java?
Four reasons: (1) String pool — immutability allows JVM to reuse identical strings, saving memory. (2) Security — strings are used for class loading, network connections, file paths. If mutable, a malicious thread could change the file path after validation. (3) Thread safety — immutable objects are inherently thread-safe, no synchronization needed. (4) HashCode caching — String caches its hashCode on first computation because it can never change.
Production failure: I saw a custom mutable string class used in a security-sensitive authentication system. An attacker exploited a TOCTOU (time-of-check-time-of-use) race condition: the string was validated as safe, then mutated before use. Switching to immutable String fixed the vulnerability.
Question 4: What are the different types of ClassLoaders in Java?
Bootstrap ClassLoader: loads core Java classes (java.lang., java.util.). Written in native code. Extension ClassLoader: loads classes from ext directory (Java 8) or jmods (Java 9+). Application ClassLoader: loads classes from the classpath. Custom ClassLoader: user-defined, for loading classes from databases, networks, or encrypted files.
The delegation model: each ClassLoader delegates to its parent first. This prevents application code from overriding core Java classes. Breaking this model (loading java.lang.String from a custom ClassLoader) throws SecurityException.
Production failure: A Tomcat deployment leaked memory because a webapp's ClassLoader held references to objects from other webapps. After hot-deploy cycles, Metaspace grew until OOM. The fix: ensure ClassLoader references don't cross webapp boundaries.
Question 5: What is the difference between JDK, JRE, and JVM?
JVM: the virtual machine that executes bytecode. Platform-specific (different JVMs for Windows, Linux, macOS). Handles JIT compilation, garbage collection, memory management.
JRE: JVM + core libraries (java.lang, java.util, etc.) + runtime files. Enough to run Java programs, not to compile them.
JDK: JRE + development tools (javac, javadoc, jdb, jconsole, jcmd). Everything needed to develop, compile, and debug Java applications.
Question 6: What is JIT compilation and how does it affect performance?
The JVM starts by interpreting bytecode (slow). The JIT compiler identifies 'hot methods' (called frequently) and compiles them to native machine code (fast). This is why Java applications get faster over time — the first few seconds are slow (interpretation), then performance improves as hot code gets compiled.
JIT optimizations: method inlining (replacing method calls with the method body), loop unrolling, dead code elimination, escape analysis (stack-allocating non-escaping objects).
Production insight: JVM warmup time matters for serverless functions (AWS Lambda, Azure Functions). A cold start with JIT compilation can take 2-5 seconds. Solutions: GraalVM native-image (AOT compilation), CRaC (Coordinated Restore at Checkpoint), or keeping functions warm with scheduled invocations.
Question 7: What is the difference between final, finally, and finalize()?
final: keyword. final variable = cannot be reassigned. final method = cannot be overridden. final class = cannot be extended.
finally: block. Always executes after try-catch, regardless of exception. Used for cleanup (closing connections, releasing locks).
finalize(): method. Called by GC before reclaiming an object. Deprecated since Java 9 — unreliable (GC may never run), slow (adds overhead to GC), and dangerous (can resurrect objects). Use try-with-resources or Cleaner instead.
Question 8: What happens when you run out of heap memory? What about stack memory?
Heap exhaustion: throws java.lang.OutOfMemoryError: Java heap space. You can catch it, but the JVM is in a bad state — many objects failed to allocate. Enable -XX:+HeapDumpOnOutOfMemoryError to get a diagnostic dump.
Stack exhaustion: throws java.lang.StackOverflowError. Usually caused by infinite recursion. Cannot be reliably caught (the stack is corrupted). Fix: increase -Xss or convert recursion to iteration.
Metaspace exhaustion: throws java.lang.OutOfMemoryError: Metaspace. Class metadata space is full. Common cause: classloader leak in application servers. Fix: set -XX:MaxMetaspaceSize and find the leak with jcmd VM.classloader_stats.
The Java Collections Framework (Questions 9-16)
Collections are the bread and butter of Java development. A frequent high-level question is the internal working of a HashMap. It uses a technique called 'Hashing.' When you call put(key, value), Java calculates the hashCode(), identifies the bucket index, and stores the entry. If two keys have the same hash (a collision), Java traditionally used a LinkedList, but since Java 8, it balances the bucket using a Red-Black Tree if the threshold is exceeded, improving worst-case performance from O(n) to O(log n).
Question 9: How does HashMap work internally?
HashMap stores entries in an array of buckets. Each bucket can contain a LinkedList (or Red-Black Tree since Java 8). The put() operation: compute hash(key), find bucket index (hash & (capacity-1)), traverse the bucket to find existing entry with same key (using equals()), replace or append.
Treeification threshold: when a bucket has ≥ 8 entries AND the table has ≥ 64 buckets, the LinkedList converts to a Red-Black Tree. When a bucket drops below 6 entries after removal, it converts back to a LinkedList.
Production failure: A service I audited used user-provided email addresses as HashMap keys. An attacker crafted 10,000 emails that all hashed to the same bucket (hash collision attack). Every put() degraded to O(n) scan of a 10,000-element LinkedList. CPU spiked to 100%, service became unresponsive. Fix: use ConcurrentHashMap (which bins entries into separate segments) or validate/sanitize keys. Java 8's treeification mitigates this but doesn't eliminate it — the tree conversion only kicks in at 64+ table size.
Question 10: What is the difference between HashMap, LinkedHashMap, and TreeMap?
HashMap: no ordering guarantee. O(1) average for get/put. Default choice. LinkedHashMap: maintains insertion order (or access order with accessOrder=true). Useful for LRU caches. ~10% slower than HashMap due to maintaining a doubly-linked list across entries. TreeMap: sorted by key (natural ordering or Comparator). O(log n) for get/put. Use when you need sorted iteration or range queries.
Question 11: What is the difference between ArrayList and LinkedList?
ArrayList: backed by a dynamic array. O(1) random access (get(i)). O(n) insertion/deletion in the middle (requires shifting). Memory-efficient (contiguous storage, no pointer overhead).
LinkedList: doubly-linked list. O(n) random access (must traverse). O(1) insertion/deletion at known positions (just update pointers). Higher memory overhead (two pointers per node).
Production insight: In 15 years of Java development, I've never found a real-world case where LinkedList outperformed ArrayList for the workloads I was optimizing. ArrayList's cache locality (contiguous memory) makes it faster even for insertions in practice, despite LinkedList's theoretical O(1) advantage. The one exception: when you're implementing a queue/deque and need O(1) addFirst/addLast — use ArrayDeque, not LinkedList.
Question 12: What is the difference between HashMap and ConcurrentHashMap?
HashMap: not thread-safe. Concurrent modification can cause infinite loops (Java 7), lost updates, or corrupted state. Never share a HashMap across threads without external synchronization.
ConcurrentHashMap: thread-safe without locking the entire map. Uses segment-level locking (Java 7) or CAS + synchronized on individual bins (Java 8). Allows concurrent reads without locking. Null keys and values are NOT allowed (unlike HashMap).
Collections.synchronizedMap(): wraps a HashMap with synchronized methods. Every operation locks the entire map — terrible for concurrent read-heavy workloads. Use ConcurrentHashMap instead.
Production failure: A caching layer used Collections.synchronizedMap() with 50 threads doing reads and 2 threads doing writes. Throughput was 10x lower than expected because every read acquired the global lock. Switching to ConcurrentHashMap increased throughput by 8x.
Question 13: What is the difference between fail-fast and fail-safe iterators?
Fail-fast: throws ConcurrentModificationException if the collection is modified during iteration (except through the iterator's own remove() method). ArrayList, HashMap, HashSet use fail-fast iterators (backed by modCount).
Fail-safe: iterates over a snapshot or uses internal synchronization. Does NOT throw ConcurrentModificationException. ConcurrentHashMap, CopyOnWriteArrayList use fail-safe iterators. Tradeoff: may not reflect concurrent modifications.
Production failure: A background thread was removing expired entries from a HashMap while the main thread was iterating over it. Intermittent ConcurrentModificationException in production — hard to reproduce because it depends on thread timing. Fix: use ConcurrentHashMap or collect keys to remove, then remove after iteration.
Question 14: When would you use a Set vs a List?
List: ordered collection, allows duplicates. Use when order matters, duplicates are valid, or you need index-based access.
Set: no duplicates (enforced by equals()/hashCode()). Use when uniqueness is required. HashSet for O(1) lookup, LinkedHashSet for insertion-order iteration, TreeSet for sorted iteration.
Production insight: I've seen bugs where developers used List.contains() in a hot loop — O(n) per call. Switching to Set.contains() — O(1) — reduced an API endpoint latency from 800ms to 15ms on a dataset of 100,000 items.
Question 15: What is the difference between Comparable and Comparator?
Comparable: natural ordering. Implemented by the class itself (compareTo()). One ordering per class. Example: String implements Comparable for alphabetical ordering.
Comparator: external ordering. Separate class or lambda. Multiple orderings possible. Example: Comparator.comparing(Person::getAge).thenComparing(Person::getName).
Production insight: Always prefer Comparator for sorting — it's more flexible, composable, and doesn't couple your domain class to a specific ordering. Use Comparable only when there's a single obvious natural ordering (like BigDecimal for numeric ordering).
Question 16: How does PriorityQueue work internally?
PriorityQueue is a binary min-heap backed by an array. The smallest element (by natural ordering or Comparator) is always at the head. offer()/add() inserts and sifts up — O(log n). poll() removes the head and sifts down — O(log n). peek() returns the head without removing — O(1). Not thread-safe — use PriorityBlockingQueue for concurrent access.
Common mistake: iterating over a PriorityQueue does NOT guarantee sorted order. The iteration order is the array order, not the heap order. To get sorted elements, repeatedly call poll().
Object-Oriented Programming & Design Patterns (Questions 17-24)
OOP questions test whether you understand Java's type system and can design maintainable code. Interviewers look for candidates who can articulate tradeoffs, not just recite definitions.
Question 17: What are the four pillars of OOP?
Encapsulation: hiding internal state and requiring interaction through methods. Private fields + public getters/setters. Not just a convention — it's what enables you to add validation, logging, or lazy computation without changing the API.
Abstraction: exposing only relevant details and hiding complexity. Abstract classes and interfaces are Java's tools for this. A PaymentProcessor interface hides whether the implementation uses Stripe, PayPal, or a mock.
Inheritance: creating new classes from existing ones. 'Is-a' relationship. Use sparingly — deep inheritance hierarchies (5+ levels) are a maintenance nightmare. Prefer composition over inheritance.
Polymorphism: same interface, different behavior. Compile-time (method overloading) and runtime (method overriding). This is what makes design patterns like Strategy and Observer possible.
Question 18: What is the difference between an abstract class and an interface?
Abstract class: can have constructors, instance fields, concrete methods, abstract methods. Single inheritance only. Use when classes share common state and behavior.
Interface (Java 8+): can have default methods, static methods, and (since Java 9) private methods. No constructors, no instance fields (only static final constants). Multiple inheritance of type. Use when you define a contract without shared state.
Production insight: since Java 8's default methods, the line between abstract classes and interfaces blurred. The remaining differentiator: abstract classes can have mutable state (instance fields), interfaces cannot. If your design needs shared mutable state, use an abstract class. If it's pure contract + optional default behavior, use an interface.
Question 19: What is the difference between method overloading and overriding?
Overloading: same method name, different parameters (number, type, or order). Resolved at compile time (static binding). Example: println(int), println(String), println(double).
Overriding: same method signature in subclass. Resolved at runtime (dynamic dispatch). The JVM looks up the actual object type at runtime and calls the appropriate version. This is polymorphism in action.
Trap: overloading with autoboxing. callMethod(int) vs callMethod(Integer) — the compiler chooses the unboxed version when you pass a primitive. But if only callMethod(Integer) exists, autoboxing kicks in. This causes subtle bugs when refactoring.
Question 20: What is the difference between Composition and Inheritance? When would you use each?
Inheritance: 'is-a' relationship. Dog is-a Animal. Tight coupling — subclass depends on superclass implementation. Fragile base class problem: changing the superclass can break subclasses.
Composition: 'has-a' relationship. Car has-an Engine. Loose coupling — you can swap implementations at runtime. More flexible, easier to test (mock the dependency).
Rule of thumb: favor composition. Use inheritance only when there's a genuine 'is-a' relationship AND you need to share behavior across a type hierarchy. I've refactored three production codebases from deep inheritance to composition — every time, the code became more testable and less brittle.
Question 21: What is the Singleton pattern and how do you implement it thread-safely?
Singleton: ensure exactly one instance of a class exists and provide global access to it.
Thread-safe implementations: 1. Enum singleton (recommended): enum Singleton { INSTANCE; } — inherently thread-safe, serialization-safe, reflection-safe. 2. Bill Pugh holder: private static class Holder { static final Singleton INSTANCE = new Singleton(); } — lazy, thread-safe, no synchronization overhead. 3. Double-checked locking (Java 5+): volatile + synchronized block. Works since JSR-133 fixed the memory model.
Production insight: I avoid Singletons in most cases. They make testing hard (global state, can't mock), hide dependencies (no constructor injection), and create tight coupling. Use dependency injection (Spring, Guice) instead. The only legitimate Singleton use case I've encountered: configuration objects that truly must be unique per JVM.
Question 22: What is the Strategy pattern? Give a real example.
Strategy: define a family of algorithms, encapsulate each one, and make them interchangeable. The client chooses the strategy at runtime.
Real example: payment processing. PaymentStrategy interface with pay(amount) method. CreditCardStrategy, PayPalStrategy, BankTransferStrategy implementations. The checkout service doesn't know or care which payment method is used — it just calls strategy.pay(amount).
Production example: I used Strategy pattern for retry logic. RetryStrategy interface with retry(operation, maxAttempts) method. ExponentialBackoffRetry, FixedDelayRetry, NoRetry implementations. Different API clients used different strategies based on the upstream service's rate limiting behavior.
Question 23: What is the Observer pattern and how does Java support it?
Observer: define a one-to-many dependency so that when one object changes state, all dependents are notified automatically.
Java support: java.util.Observer and java.util.Observable (deprecated since Java 9 — too limited). Modern alternatives: PropertyChangeListener (JavaBeans), java.util.concurrent.Flow (reactive streams, Java 9+), or custom implementations using Consumer/Function.
Production insight: every event-driven system I've built uses Observer-like patterns. Spring's ApplicationEvent, Kafka consumers, and even HTTP webhook callbacks are all Observer variants. The key design decision: push vs pull. Push (notify with data) is simpler but couples the observer to the notification format. Pull (notify without data, observer queries) is more flexible but adds latency.
Question 24: What is the difference between Dependency Injection and the Service Locator pattern?
Dependency Injection (DI): dependencies are provided externally (via constructor, setter, or field injection). The class doesn't know how to create its dependencies. Testable (inject mocks), explicit (dependencies visible in constructor), loosely coupled.
Service Locator: the class asks a central registry for its dependencies at runtime. Hidden dependencies (not visible in constructor), harder to test (need to configure the locator), creates implicit coupling to the locator.
Production insight: always prefer constructor injection. It makes dependencies explicit, ensures the object is fully initialized after construction, and is trivially testable. Field injection (@Autowired on fields) hides dependencies and makes testing harder. I've banned field injection in every codebase I've led.
Concurrency & Multithreading (Questions 25-32)
Concurrency questions are where senior candidates separate themselves. Interviewers want to see that you understand not just the APIs, but the memory model, the failure modes, and the production debugging techniques.
Question 25: What is the difference between a process and a thread?
Process: independent execution unit with its own memory space, file descriptors, and system resources. Heavyweight — creating a process is expensive (milliseconds). Inter-process communication requires explicit mechanisms (pipes, sockets, shared memory).
Thread: lightweight execution unit within a process. Shares the process's heap, file descriptors, and code segment. Has its own stack, program counter, and registers. Creating a thread is cheap (microseconds). Communication through shared memory (requires synchronization).
Question 26: What is the difference between synchronized and volatile?
synchronized: provides both mutual exclusion (only one thread executes the critical section) and visibility (changes are visible to other threads after unlock). Can be used on methods or blocks. Has performance overhead (lock acquisition/release).
volatile: provides visibility only (writes are immediately visible to all threads) but NOT mutual exclusion. Does NOT make compound operations atomic (count++ is still a race condition). No lock overhead — just memory barrier instructions. Use for flags, status indicators, and single-writer scenarios.
Production failure: I debugged a service where a volatile boolean flag was used to coordinate a multi-step update. Thread A set flag = true, then updated three fields. Thread B saw flag = true but the three fields weren't all updated yet — volatile guarantees visibility of the flag write, but NOT ordering of writes to other fields. Fix: use synchronized or make all three fields volatile (and even then, the compound update isn't atomic).
Question 27: What is a deadlock and how do you prevent it?
Deadlock: two or more threads are blocked forever, each waiting for a lock held by the other. Classic scenario: Thread 1 locks A then tries to lock B. Thread 2 locks B then tries to lock A. Neither can proceed.
Prevention strategies: 1. Lock ordering: always acquire locks in the same global order (e.g., by object hash code). 2. Try-lock with timeout: use ReentrantLock.tryLock(timeout) instead of synchronized. 3. Lock-free algorithms: use atomic operations (AtomicInteger, CAS) instead of locks. 4. Avoid nested locks: if you must nest, ensure strict ordering.
Production debugging: jstack <pid> shows thread dumps including deadlock detection. jconsole and VisualVM show deadlocked threads visually. -XX:+PrintClassHistogram before killing a deadlocked JVM shows what objects the threads are holding.
Question 28: What is the difference between Runnable and Callable?
Runnable: run() method returns void, cannot throw checked exceptions. Use for fire-and-forget tasks.
Callable: call() method returns a value and can throw checked exceptions. Use when you need the result or want to handle exceptions. Submitted to ExecutorService returns a Future.
Question 29: What is the Executor framework and why should you use it instead of creating threads directly?
Executor framework: thread pool management abstraction. Creates a fixed number of threads and reuses them for submitted tasks. Prevents thread explosion (unbounded thread creation crashes the JVM).
- Executors.newFixedThreadPool(n): fixed number of threads, unbounded queue. Good for CPU-bound work.
Executors.newCachedThreadPool(): creates threads as needed, reuses idle threads. Good for short-lived I/O tasks. DANGER: can create unlimited threads under load.Executors.newSingleThreadExecutor(): one thread, sequential execution. Good for guaranteed ordering.- ThreadPoolExecutor: full control over core/max threads, queue type, rejection policy.
Production insight: NEVER use newCachedThreadPool() in production. Under load, it creates unlimited threads — each thread consumes ~1 MB of stack memory. I've seen a service create 10,000 threads in seconds, exhausting memory and causing OOM. Always use newFixedThreadPool() or ThreadPoolExecutor with explicit bounds.
Question 30: What is the happens-before relationship in the Java Memory Model?
Happens-before: if operation A happens-before operation B, then A's memory effects are visible to B. Without a happens-before relationship, one thread's writes may never be visible to another thread.
Key rules: program order (within one thread), monitor lock (unlock → lock on same monitor), volatile (write → read of same field), thread start (start() → first action in thread), thread join (last action in thread → join() returns), transitivity (A→B and B→C implies A→C).
Question 31: What is the difference between CountDownLatch, CyclicBarrier, and Semaphore?
CountDownLatch: one-time use. A thread waits until N other threads call countDown(). Use case: main thread waits for N worker threads to finish initialization.
CyclicBarrier: reusable. N threads wait at a barrier until all N arrive, then all proceed simultaneously. Use case: parallel computation phases where all threads must complete one phase before any start the next.
Semaphore: controls access to N permits. Threads acquire() a permit (blocking if none available) and release() when done. Use case: limiting concurrent connections to a database or API.
Question 32: What are virtual threads (Project Loom) and when should you use them?
Virtual threads (Java 21): lightweight threads managed by the JVM, not the OS. Millions of virtual threads can run on a few dozen carrier (platform) threads. Stack is heap-allocated and grows on demand (starts at a few hundred bytes vs 1 MB for platform threads).
Use virtual threads for: I/O-bound workloads (HTTP handlers, database queries, message processing). Each request gets its own virtual thread — no async/await complexity, no callback hell.
Don't use virtual threads for: CPU-bound work (they still compete for carrier threads, and pinning during synchronized blocks can cause starvation).
Production insight: we migrated a gRPC service from a 200-thread fixed pool to virtual threads. Throughput increased 3x because threads no longer blocked on I/O — the carrier thread was immediately freed to handle other virtual threads. Memory usage dropped because each virtual thread's stack was ~1 KB instead of 1 MB.
That's Java Interview. Mark it forged?
19 min read · try the examples if you haven't