Java remains one of the most interviewed languages in the industry, powering backend systems at companies from startups to Fortune 500 enterprises. Java interviews are distinctive because they test not just language syntax, but your understanding of the JVM, memory model, concurrency primitives, and the Spring ecosystem — topics that separate developers who write Java from engineers who build reliable Java systems. The questions below are organized from fundamentals through advanced topics, covering the full range of what you will encounter in Java-focused technical interviews. For each section, the questions progress in depth. If you can answer the earlier questions confidently, you are at a solid junior/mid level. If you can articulate the nuances in the later questions with trade-off analysis, you are demonstrating senior-level understanding.Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
1. Java Fundamentals
What is Java?
What is Java?
- Java is statically typed, class-based, and compiled to an intermediate bytecode format (
.classfiles) rather than native machine code. This bytecode runs on the JVM, which provides platform independence, automatic memory management via garbage collection, and a security sandbox. - The “platform independence” story has nuance: your code is portable, but your JVM is not. Each OS needs its own JVM implementation (HotSpot, OpenJ9, GraalVM). In practice, you still hit platform-specific issues — file path separators, line endings, native library loading via JNI, and even thread scheduling behavior differs across OS implementations.
- Java’s real staying power comes from its ecosystem maturity: the Collections Framework,
java.util.concurrent, Spring, Hibernate, and build tools like Maven/Gradle. Companies like Netflix, LinkedIn, and Uber run massive backend services on Java because of its predictable performance profile at scale and battle-tested libraries. - Since Java 9, the language has adopted a six-month release cadence. Modern Java (17+) includes records, sealed classes, pattern matching, text blocks, and virtual threads (Project Loom in Java 21) — it is a fundamentally different language than Java 8.
- “What makes Java ‘platform independent’ and where does that abstraction break down in production?”
- “How does Java compare to Kotlin or Go for backend services, and when would you pick one over the other?”
- “What Java version features have you used in production, and which ones actually changed how you write code?”
Explain JVM, JRE, and JDK
Explain JVM, JRE, and JDK
- JVM (Java Virtual Machine): Executes Java bytecode. It is the runtime engine that provides memory management, garbage collection, JIT compilation, and a security model.
- JRE (Java Runtime Environment): Contains the JVM plus core class libraries (
java.lang,java.util, etc.) needed to run Java applications. No development tools included. - JDK (Java Development Kit): Includes the JRE plus development tools —
javac(compiler),jdb(debugger),javap(disassembler),jconsole,jvisualvm, and more.
- The JVM is not a single monolithic thing — it has subsystems: the Class Loader (loads
.classfiles using bootstrap, extension, and application classloaders), the Execution Engine (interpreter + JIT compiler), and the Runtime Data Areas (heap, stack, method area, PC registers, native method stack). - JIT (Just-In-Time) compilation is where Java gets its performance. The JVM starts by interpreting bytecode, then identifies “hot” methods (called frequently) and compiles them to native machine code at runtime. HotSpot JVM uses C1 (client) and C2 (server) compilers. This is why Java apps have a “warm-up” period — first few thousand requests are slower.
- Since Java 9, the JRE is no longer distributed separately. The
jlinktool lets you create custom runtime images containing only the modules your app needs, reducing deployment footprint from ~200MB to sometimes under 30MB. - In containerized deployments (Docker/K8s), understanding JVM ergonomics matters: older JVMs did not respect container memory limits (
-XX:+UseContainerSupportwas added in Java 10). This caused OOM kills in production for many teams running Java 8 in Docker.
- “Walk me through what happens from when you type
java MyAppto whenmain()starts executing.” - “What is JIT compilation, and why do Java apps have a warm-up period?”
- “How do you size JVM memory in a Docker container, and what goes wrong if you get it wrong?”
What are access modifiers in Java?
What are access modifiers in Java?
public, protected, default (package-private), and private — they control the visibility of classes, methods, and variables.What a strong candidate explains:| Modifier | Same Class | Same Package | Subclass (diff pkg) | Everywhere |
|---|---|---|---|---|
private | Yes | No | No | No |
default | Yes | Yes | No | No |
protected | Yes | Yes | Yes | No |
public | Yes | Yes | Yes | Yes |
privateis your first choice for fields — it enforces encapsulation. Expose behavior through methods, not raw state. In practice, this is the most important modifier for API design.default(package-private) is underused but powerful. It is the right choice for implementation classes that should not be part of your public API. For example, if you have an internalCacheEvictionStrategythat callers should never directly reference, package-private keeps it hidden without extra ceremony.protectedis often misunderstood — it grants access to subclasses and everything in the same package. This is a wider scope than most developers expect. It couples your class hierarchy, so use it sparingly.publicis a commitment. Once a method is public, removing or changing it is a breaking change. In library design, you should default to the most restrictive modifier and only widen when necessary. Joshua Bloch’s Effective Java calls this “minimize accessibility.”- Top-level classes can only be
publicordefault— notprivateorprotected. Inner classes can use all four.
default means package-private (not public).Follow-up:- “If you are designing a library that other teams will depend on, how do you decide which classes and methods to make public?”
- “What is the relationship between access modifiers and Java’s module system (JPMS) introduced in Java 9?”
- “Can you override a method with a more restrictive access modifier? Why or why not?”
Difference between stack and heap memory?
Difference between stack and heap memory?
new. Shared across all threads. Managed by the garbage collector.What a strong candidate adds:- Stack memory is LIFO (last-in, first-out), automatically allocated and deallocated as methods are called and return. It is extremely fast because allocation is just moving a pointer. Stack size is typically 512KB-1MB per thread (configurable via
-Xss). If you have 500 threads, that is 500MB just in stack memory. - Heap memory is where all object instances live. It is divided into generations: Young Generation (Eden + Survivor spaces) for short-lived objects, and Old Generation (Tenured) for long-lived objects. Since Java 8, the Metaspace (formerly PermGen) stores class metadata and lives in native memory, not the heap.
- Stack overflow (
StackOverflowError) happens with deep or infinite recursion — each method call adds a frame. Out of memory (OutOfMemoryError) happens when the heap is exhausted and GC cannot free enough space. - Escape analysis is a JVM optimization where the JIT compiler determines that an object does not “escape” a method scope and can be allocated on the stack instead of the heap. This eliminates GC pressure for short-lived objects. For example, an
Iteratorcreated inside a loop that never leaves the method may be stack-allocated. - Practical impact: In a high-throughput system processing 50K requests/second, excessive object allocation on the heap triggers frequent GC pauses. Tools like
jstat,jmap, and async-profiler help identify allocation hotspots. Companies like Twitter and Netflix have teams dedicated to JVM tuning.
- “What happens if you set
-Xmsand-Xmxto the same value? Why would you do that?” - “Explain Young Generation vs Old Generation. How does an object move between them?”
- “How would you diagnose a Java application that is spending 30% of its time in GC pauses?”
What are wrapper classes?
What are wrapper classes?
int to Integer, double to Double, boolean to Boolean, etc. They are necessary because Java generics and collections only work with objects, not primitives.What a strong candidate explains:- Autoboxing/unboxing (Java 5+) handles conversion automatically:
Integer x = 5;boxes theint, andint y = x;unboxes theInteger. This is syntactic sugar — the compiler insertsInteger.valueOf(5)andx.intValue()calls. - The
Integercache gotcha:Integer.valueOf()caches values from -128 to 127. SoInteger a = 127; Integer b = 127; a == breturnstrue(same cached object), butInteger a = 128; Integer b = 128; a == breturnsfalse(different objects). This trips up developers in production and is a classic interview trap. Always use.equals()for wrapper comparisons. - Performance impact: Autoboxing creates objects on the heap. In a tight loop processing millions of values,
List<Integer>creates millions ofIntegerobjects with ~16 bytes overhead each, versus a primitiveint[]with zero overhead. This is why libraries like Eclipse Collections, HPPC, and Trove provide primitive-specialized collections. Project Valhalla (value types) aims to fix this at the language level. - Null danger: Wrappers can be
null, primitives cannot. Unboxing anullIntegerthrowsNullPointerException. This is a common production bug:Map<String, Integer> map = ...; int value = map.get("missing");throws NPE becauseget()returnsnull, and unboxingnullblows up.
- “Why does
new Integer(5) == new Integer(5)returnfalsebutInteger.valueOf(5) == Integer.valueOf(5)returntrue?” - “In a hot loop processing 10 million records, what is the performance difference between
List<Integer>andint[]?” - “How does Project Valhalla aim to address the primitive/object divide?“
2. Object-Oriented Programming
What are the main OOP principles?
What are the main OOP principles?
- Encapsulation: Bundling data and methods together and restricting direct access to internal state. It is not just “use getters and setters” — it is about hiding invariants. For example, a
BankAccountclass should not exposebalancedirectly because external code could set it to a negative number, violating business rules. The getter/setter pattern is a tool, not the goal — the goal is protecting invariants. - Inheritance: Creating new classes based on existing ones to promote code reuse. But experienced engineers know inheritance is the most abused OOP principle. The “Favor composition over inheritance” guideline (Effective Java, Item 18) exists because inheritance creates tight coupling. For example, extending
HashMapto add logging seems easy untilHashMapchanges its internal implementation and your subclass breaks. Use composition: wrap aHashMapin your class and delegate. - Polymorphism: The ability to treat objects of different classes through the same interface. Compile-time (overloading: same name, different parameters) vs runtime (overriding: subclass redefines parent method, resolved via vtable dispatch). Runtime polymorphism is the foundation of the Strategy pattern, plugin architectures, and dependency injection frameworks like Spring.
- Abstraction: Exposing essential features while hiding implementation complexity. Abstract classes and interfaces are the mechanisms. A
PaymentProcessorinterface with acharge()method lets your code work with Stripe, PayPal, or Square without knowing the implementation. This is the Dependency Inversion Principle in action.
- “Give me a real example where you chose composition over inheritance. What would have gone wrong with inheritance?”
- “How does runtime polymorphism actually work at the JVM level? What is a vtable?”
- “Which SOLID principle is most related to each OOP pillar?”
Difference between abstraction and encapsulation?
Difference between abstraction and encapsulation?
- Think of abstraction as the “outside view” and encapsulation as the “inside lock.” When you use
List<String> list = new ArrayList<>(), you are programming to theListabstraction — you do not care thatArrayListuses a resizable array internally. That is abstraction. - Encapsulation is the enforcement mechanism. Making fields
privateand providing controlled access ensures that the internal representation can change without breaking callers. For example, aTemperatureclass might store degrees internally in Celsius but exposegetFahrenheit()— encapsulation lets you change the internal storage to Kelvin later without any caller knowing. - The subtle difference: Abstraction is about design (choosing the right interfaces and what to expose). Encapsulation is about implementation (how you protect the internals). You can have abstraction without encapsulation (a well-designed interface with public fields) and encapsulation without abstraction (private fields but no meaningful interface hierarchy).
- In Spring,
@Serviceand@Repositoryare abstraction — they define roles. Theprivatefields inside those beans are encapsulation. Together they produce a system where you can swap aJpaUserRepositoryfor aMongoUserRepositorywithout changing service layer code.
- “Can you have abstraction without encapsulation? Give an example.”
- “How do Java interfaces and abstract classes serve as abstraction mechanisms differently?”
What is method overloading vs overriding?
What is method overloading vs overriding?
- Overloading: Same method name, different parameter lists. Resolved at compile time (static dispatch).
- Overriding: Subclass provides a specific implementation of a parent method. Resolved at runtime (dynamic dispatch).
- Overloading rules: Methods must differ in parameter count or types. Return type alone is not sufficient to overload. The compiler selects the most specific matching method. This can cause surprising behavior:
print(null)with overloadsprint(String s)andprint(Object o)— the compiler picksStringbecause it is more specific. But addprint(Integer i)and it becomes ambiguous, causing a compile error. - Overriding rules: The method signature must match exactly. The return type can be covariant (a subtype of the parent return type, allowed since Java 5). Access cannot be more restrictive. The
@Overrideannotation is not required but is strongly recommended — it catches typos and signature mismatches at compile time. Forgetting@Overridemeans you accidentally overload instead of override, and your code silently breaks. - Runtime dispatch: When you call
animal.speak()on aDogobject referenced asAnimal, the JVM uses the virtual method table (vtable) to find the actualDog.speak()implementation. This lookup has a small overhead, but HotSpot JIT can inline monomorphic call sites (where only one implementation is ever seen) for zero overhead. - The
staticandprivateexception: Static methods cannot be overridden — they are bound at compile time. If a subclass defines a static method with the same signature, it hides (not overrides) the parent method. This is a subtle and commonly tested distinction.
- “What happens if you overload a method where one parameter is
intand another isInteger? How does autoboxing interact with overload resolution?” - “Can you override a static method? What happens if you try?”
- “What is the performance cost of virtual method dispatch, and how does the JIT optimize it?”
Can you explain 'this' and 'super' keywords?
Can you explain 'this' and 'super' keywords?
thisrefers to the current class instance.superrefers to the parent class members or constructor.
thisuses: (1) Disambiguate field vs parameter names (this.name = namein constructors), (2) Call another constructor in the same class (this(param)— must be the first statement), (3) Pass the current instance as an argument (someMethod(this)), (4) Return the current instance for fluent APIs (return this).superuses: (1) Call parent constructor (super()orsuper(args)— must be first statement in constructor; implicitly called if not specified), (2) Access parent method when overridden (super.toString()), (3) Access parent field when shadowed.- Constructor chaining subtlety:
this()andsuper()cannot both be in the same constructor because both must be the first statement. If you callthis()to chain to another constructor, that constructor is responsible for thesuper()call. The compiler ensures every constructor chain eventually calls asuper(). - Real-world fluent API pattern: Builder pattern heavily uses
return this:
new Builder().withName("foo").withAge(25).build().What interviewers are really testing: Whether you understand constructor chaining mechanics and can articulate the “first statement” constraint.Red flag answer: Just saying “this is current object, super is parent” without explaining constructor chaining or practical usage patterns.Follow-up:- “Why can’t you use
this()andsuper()in the same constructor?” - “What happens if you do not explicitly call
super()in a subclass constructor?”
What are constructors in Java?
What are constructors in Java?
- Types: Default (no-arg, auto-generated if no constructor defined), parameterized, and copy constructors (manually implemented — Java does not auto-generate these unlike C++).
- No-arg constructor gotcha: If you define any constructor, the compiler does not generate a default no-arg constructor. This breaks frameworks like Hibernate, JPA, and Jackson that require a no-arg constructor for reflection-based instantiation. This is why you see
@NoArgsConstructorfrom Lombok everywhere in Spring Boot projects. - Constructor vs Factory Method: Effective Java (Item 1) recommends static factory methods over constructors:
Optional.of(),List.of(),BigInteger.valueOf(). Advantages: they have names (clearer intent), can return cached instances, can return subtypes, and reduce verbosity with type inference. - Immutability pattern: For immutable objects, set all fields in the constructor as
final, provide no setters, and make the classfinal. Java 16+ records (record Point(int x, int y) {}) auto-generate this pattern with constructor,equals(),hashCode(), andtoString(). - Constructor execution order: Static blocks run first (once per class load), then instance initializer blocks, then the constructor body. Parent constructors always execute before child constructors. This ordering matters when you have initialization dependencies.
- “Why does Hibernate require a no-argument constructor, and what happens if you forget it?”
- “What is the execution order when a subclass with static blocks, instance initializers, and a constructor is instantiated?”
- “When would you use a static factory method instead of a constructor?“
3. Java Basics & Control Flow
Difference between == and equals()?
Difference between == and equals()?
== compares references (memory addresses) for objects and values for primitives. .equals() compares logical content — but only if the class overrides it. The default Object.equals() is the same as ==.What a strong candidate explains:- The String trap:
"hello" == "hello"returnstruebecause Java interns string literals (stores them in the String Pool). Butnew String("hello") == new String("hello")returnsfalsebecausenewforces heap allocation, bypassing the pool. This is one of the most common Java interview gotchas. - Contract:
equals()andhashCode()must be consistent. If two objects areequals(), they must have the samehashCode(). Violating this breaksHashMap,HashSet, and any hashing-based collection. Example: you overrideequals()on aPersonclass to compare by name, but forgethashCode(). TwoPerson("Alice")objects are “equal” but land in differentHashMapbuckets — somap.get(new Person("Alice"))returnsnulleven though you just put it in. equals()contract properties: Reflexive (x.equals(x)is true), symmetric (x.equals(y)impliesy.equals(x)), transitive, consistent, andx.equals(null)is always false. Implementing this correctly with inheritance is surprisingly hard — which is why Effective Java recommends favoring composition over inheritance forequals()correctness.- Modern shortcut: Java 16+ records auto-generate
equals()andhashCode()based on all fields. Lombok’s@EqualsAndHashCodedoes the same. These eliminate an entire class of bugs.
equals()/hashCode() contract and can articulate what breaks when it is violated. This separates juniors from mid-level engineers.Red flag answer: ”== checks reference, equals checks value” without mentioning the hashCode contract, String Pool behavior, or what happens when the contract is broken.Follow-up:- “What happens if you override
equals()but nothashCode()? Walk me through the HashMap failure scenario.” - “Why is implementing
equals()correctly in a class hierarchy (with inheritance) so difficult?” - “How do Java records solve the
equals/hashCodeproblem?”
What is a static keyword?
What is a static keyword?
static denotes class-level members — they belong to the class itself, not to any instance. Used for variables, methods, blocks, nested classes, and imports.What a strong candidate explains:- Static variables are shared across all instances. A counter
static int instanceCountincremented in the constructor tracks how many objects were created. But in multi-threaded environments, a non-synchronized static variable is a race condition waiting to happen. UseAtomicIntegerorsynchronizedaccess. - Static methods cannot access instance members (
thisdoes not exist). This is whymain()is static — no object exists yet when the JVM starts. Static methods are good for utility functions (Math.max(),Collections.sort()), factory methods, and pure functions with no side effects. - Static blocks execute once when the class is loaded, before any constructor. Used for complex static initialization: loading native libraries (
System.loadLibrary()), reading configuration, populating lookup tables. Order matters — multiple static blocks execute top to bottom. - Static inner classes do not hold a reference to the enclosing instance (unlike non-static inner classes). This matters for memory leaks: a non-static inner class (like an anonymous
Handlerin Android) holds an implicit reference to the outerActivity, preventing it from being garbage collected. This was one of the most common Android memory leak patterns. - Static imports (
import static java.lang.Math.PI) let you use static members without class qualification. Useful for test assertions:import static org.junit.Assert.*.
- “Why can’t you access instance variables from a static method? What would that even mean?”
- “What is the difference between a static inner class and a non-static inner class in terms of memory?”
- “How do static blocks interact with class loading? When exactly does a class get loaded?”
Difference between break and continue?
Difference between break and continue?
break exits the enclosing loop entirely. continue skips the remainder of the current iteration and moves to the next one.What a strong candidate adds:- Labeled break/continue: Java supports labels for nested loops.
break outerLoop;exits the outer loop from inside an inner loop — this is the only clean way to break out of nested loops without a flag variable or extracting a method.
breakinswitch: Missingbreakin switch cases causes fall-through — all subsequent cases execute until a break is hit. This is one of the most common Java bugs. Java 14+ switch expressions (->) eliminate this problem entirely:case "A" -> doA();does not fall through.- Clean code perspective: Heavy use of
breakandcontinueis often a code smell. In most cases, the logic is clearer when refactored using streams withfilter(),findFirst(), ortakeWhile(), or by extracting the loop body into a method with an earlyreturn.
- “How would you break out of a deeply nested loop? Compare labeled break vs extracting a method.”
- “What changed about
switchin Java 14+ that makesbreakless of a concern?”
What is final keyword used for?
What is final keyword used for?
final serves three purposes: makes variables constant (cannot be reassigned), prevents method overriding, and prevents class inheritance.What a strong candidate explains:finalvariables: The reference cannot be reassigned, but the object it points to can still be mutated.final List<String> list = new ArrayList<>(); list.add("hello");is perfectly legal. This is a critical distinction —finaldoes not mean immutable. For true immutability, you need an unmodifiable collection or a deeply immutable class design.finalmethods: Prevent subclasses from overriding. The JVM can also potentially optimize final method calls since it knows no override exists, enabling aggressive inlining. Template Method pattern often uses final methods for the fixed algorithm steps.finalclasses: Cannot be extended.String,Integer, and all wrapper classes are final. This is a security and correctness decision — if you could subclassString, you could create a “String” that changes its value after being used as aHashMapkey, breaking the entire collection.finalparameters: Commonly used in lambda expressions and anonymous classes — Java requires that captured variables be effectively final (their value does not change after initialization). Thefinalkeyword makes this explicit.finaland performance: Modern JVMs are smart enough to detect effectively final variables without the keyword. The primary benefit offinalis communicating intent to other developers: “this will not change.”
final on a reference does not mean the object is immutable — this is the most commonly misunderstood aspect.Red flag answer: “final makes things constant” — this implies immutability, which is incorrect for object references.Follow-up:- “If I have
final Map<String, String> map, can I add entries to it? How do I make it truly immutable?” - “Why is
Stringa final class? What would go wrong if you could subclass it?” - “What does ‘effectively final’ mean in the context of lambdas?”
What are varargs in Java?
What are varargs in Java?
... syntax. Example: void log(String... messages).What a strong candidate explains:- Under the hood: Varargs are syntactic sugar for an array.
log("a", "b")compiles tolog(new String[]{"a", "b"}). The method receives a regular array internally. This means there is an array allocation on every call — in a tight loop, this creates GC pressure. - Rules: Only one varargs parameter is allowed per method, and it must be the last parameter.
void process(String name, int... values)is valid;void process(int... values, String name)is not. - Overloading ambiguity: Varargs can cause confusing overload resolution. If you have
print(int... nums)andprint(int a, int b), callingprint(1, 2)matches the explicit two-parameter version (more specific). Butprint()matches varargs with zero args. Addingprint(int a)makesprint(1)ambiguous in some edge cases. - Heap pollution with generics:
@SafeVarargsannotation exists because generic varargs (T... items) can cause heap pollution. The compiler warns about this because the varargs array is reifiable (retains runtime type info) but the generic type is erased, creating a type safety hole. This is whyArrays.asList()andList.of()are annotated with@SafeVarargs.
@SafeVarargs.Follow-up:- “What is heap pollution with varargs and generics, and when does
@SafeVarargsapply?” - “What is the performance implication of using varargs in a method called millions of times per second?“
4. Collections Framework
What is the Java Collections Framework?
What is the Java Collections Framework?
Collection (with subinterfaces List, Set, Queue, Deque) and Map.What a strong candidate explains:- The hierarchy matters:
Mapdoes not extendCollection— this surprises people. It is a separate hierarchy because key-value semantics are fundamentally different from element-in-a-group semantics. But you can get collection views:map.keySet(),map.values(),map.entrySet(). - Interface-based programming: Always declare variables as the interface type:
List<String> list = new ArrayList<>(), notArrayList<String> list. This lets you swap implementations without changing client code — fundamental to clean architecture and testability. - Choosing the right implementation:
- Need fast random access?
ArrayList(O(1) get). - Need fast insertion/deletion in the middle?
LinkedList(O(1) with iterator, but O(n) to find position). - Need unique elements?
HashSet(O(1) lookup) orTreeSet(O(log n), sorted). - Need ordered keys?
TreeMap. Need insertion order?LinkedHashMap. Need thread safety?ConcurrentHashMap.
- Need fast random access?
- Immutable collections (Java 9+):
List.of(),Set.of(),Map.of()create truly unmodifiable collections. These are not the same asCollections.unmodifiableList(), which is just a wrapper — the underlying list can still be mutated through the original reference. - Fail-fast iterators: Most collections throw
ConcurrentModificationExceptionif modified during iteration (even in a single thread). This is detected via an internalmodCount. Concurrent collections likeCopyOnWriteArrayListuse fail-safe iterators that work on a snapshot.
- “Walk me through how you would choose between ArrayList, LinkedList, HashSet, and TreeSet for a specific use case.”
- “What is the difference between
List.of()andCollections.unmodifiableList()?” - “What is a fail-fast iterator and when does
ConcurrentModificationExceptionhappen?”
Difference between ArrayList and LinkedList?
Difference between ArrayList and LinkedList?
- ArrayList: Backed by a dynamic array. O(1) random access, amortized O(1) append, O(n) insertion/deletion in the middle.
- LinkedList: Doubly-linked list. O(n) random access, O(1) insertion/deletion at known positions, implements both
ListandDeque.
- In practice, ArrayList wins almost every time. Despite LinkedList’s theoretical O(1) insertion, you first need O(n) to find the position. ArrayList’s contiguous memory layout gives it massive CPU cache advantages — sequential access is 10-100x faster due to cache line prefetching. This is why Java’s own documentation and Josh Bloch (who wrote the Collections Framework) say to prefer ArrayList.
- ArrayList resizing: Default initial capacity is 10. When full, it grows by 50% (
newCapacity = oldCapacity + (oldCapacity >> 1)). This meansArrays.copyOf()is called, copying the entire array. If you know the size upfront,new ArrayList<>(expectedSize)avoids repeated resizing. For a list that will hold 1 million elements, not pre-sizing means ~20 unnecessary array copies. - LinkedList memory overhead: Each element is a
Nodeobject with pointers toprev,next, and theitem— about 48 bytes of overhead per element on a 64-bit JVM. An ArrayList element costs ~4-8 bytes (just the reference). For 1 million strings, that is ~48MB of overhead in LinkedList vs ~8MB in ArrayList. - When LinkedList wins: When used as a
QueueorDeque(add/remove from head and tail are O(1)). But even then,ArrayDequeis usually faster because of cache locality. LinkedList’s only real advantage is O(1) removal during iteration withListIterator.
- “Why is ArrayList typically faster than LinkedList even for operations where LinkedList has better Big-O?”
- “What is
ArrayDequeand when would you use it instead ofLinkedList?” - “How does ArrayList resize, and what is the performance impact of not pre-sizing?”
Difference between HashMap and Hashtable?
Difference between HashMap and Hashtable?
- HashMap: Not synchronized, allows one
nullkey and multiplenullvalues. Part of Java 1.2 Collections Framework. - Hashtable: Synchronized (thread-safe), does not allow
nullkeys or values. Legacy class from Java 1.0.
- Hashtable is legacy — never use it in new code. Use
ConcurrentHashMapfor thread safety orCollections.synchronizedMap()for a synchronized wrapper. Hashtable synchronizes every operation, which is a performance bottleneck: everyget()andput()acquires a lock, even when there is no contention. - HashMap internals (this is what senior interviews focus on): HashMap uses an array of “buckets.” A key’s
hashCode()is hashed again (bit manipulation to spread hash values), then masked to get a bucket index. Collisions are handled by chaining — a linked list in each bucket. Since Java 8: when a bucket has more than 8 entries (and the table has at least 64 buckets), the linked list converts to a red-black tree, improving worst-case lookup from O(n) to O(log n). This was added to mitigate hash collision DoS attacks. - Load factor and resizing: Default initial capacity is 16, load factor is 0.75. When 75% full, the map resizes (doubles capacity) and rehashes all entries — this is expensive. For a map that will hold 10,000 entries, initialize with
new HashMap<>(14000)(account for load factor: 10000 / 0.75 = ~13334). - Key immutability requirement: If you use a mutable object as a HashMap key and then mutate it, the
hashCode()changes but the object is still in the old bucket.get()will never find it — the entry is effectively lost. This is whyStringis the most common key type (it is immutable and caches its hashCode).
- “Walk me through what happens internally when you call
map.put(key, value)on a HashMap.” - “Why did Java 8 add treeification to HashMap buckets? What attack does it mitigate?”
- “What happens if you use a mutable object as a HashMap key and then change it?”
Explain Set vs List vs Map
Explain Set vs List vs Map
- Set: No duplicates, no guaranteed order (except
TreeSetandLinkedHashSet). Backed byMapinternally (HashSetuses aHashMap). - List: Ordered by index, allows duplicates, supports positional access.
- Map: Key-value pairs, no duplicate keys, each key maps to exactly one value.
- Set is secretly a Map.
HashSetis backed by aHashMapwhere the set elements are keys and all values are a dummy constantPRESENTobject.TreeSetis backed by aTreeMap. Understanding this implementation detail explains the O(1) add/contains for HashSet and O(log n) for TreeSet. - Choosing correctly matters at scale:
- Need to check “is this element in the collection?” frequently? HashSet — O(1) lookup.
- Need elements sorted? TreeSet (red-black tree, O(log n) operations) or PriorityQueue (heap, different use case).
- Need insertion order preserved? LinkedHashSet or LinkedHashMap.
- Need positional access (get element at index 5)? ArrayList.
- Need to deduplicate while preserving order?
new LinkedHashSet<>(list)then back tonew ArrayList<>(set).
- EnumSet and EnumMap: If your keys are enum constants, always use these specialized implementations.
EnumSetuses a bit vector internally — it is orders of magnitude faster thanHashSet<MyEnum>and uses almost no memory. A set of up to 64 enum values fits in a singlelong.
- “How is
HashSetimplemented internally? Why does that matter?” - “When would you use
EnumSetorEnumMap? What makes them special?” - “If you need a collection that is both sorted and allows fast lookup, what do you use?”
What are Concurrent Collections?
What are Concurrent Collections?
ConcurrentHashMap, CopyOnWriteArrayList, ConcurrentLinkedQueue, BlockingQueue implementations.What a strong candidate explains:ConcurrentHashMap(most important to know): Does not lock the entire map. In Java 7, it used segment-based locking (16 segments by default). In Java 8+, it uses CAS operations (Compare-And-Swap) andsynchronizedblocks on individual bins (tree or list nodes). This means multiple threads can read and write simultaneously to different bins with no contention. Throughput is dramatically better thanHashtableorCollections.synchronizedMap()— benchmarks show 5-10x improvement under high concurrency.CopyOnWriteArrayList: Every write creates a new copy of the underlying array. Reads are lock-free and very fast. Writes are expensive (O(n) copy). Perfect for read-heavy scenarios: configuration lists, listener registries, and observer patterns where writes are rare. Used heavily in Spring’s event system.BlockingQueuefamily:ArrayBlockingQueue(bounded, array-backed),LinkedBlockingQueue(optionally bounded, linked-node),PriorityBlockingQueue(unbounded, priority-ordered). These are the backbone of producer-consumer patterns. Thread pools internally useBlockingQueuefor task queuing.- Common mistake: Wrapping operations in
synchronizedwhen usingConcurrentHashMapdefeats the purpose. The check-then-act pattern (if (!map.containsKey(k)) map.put(k, v)) is still a race condition — useputIfAbsent()orcomputeIfAbsent()instead.
ConcurrentHashMap is superior to Hashtable/synchronizedMap, and whether you know the producer-consumer pattern with BlockingQueue.Red flag answer: “ConcurrentHashMap is thread-safe” without explaining how it achieves better performance than locking the whole map.Follow-up:- “How does
ConcurrentHashMapachieve thread safety without locking the entire map?” - “When would you use
CopyOnWriteArrayListvssynchronizedList?” - “Implement a simple producer-consumer using
BlockingQueue— what happens when the queue is full or empty?“
5. Exception Handling
What is exception handling?
What is exception handling?
try, catch, finally, throw, and throws.What a strong candidate explains:- Exception hierarchy:
Throwableis the root. Two subclasses:Error(JVM-level problems likeOutOfMemoryError,StackOverflowError— generally not recoverable) andException(application-level problems).RuntimeExceptionextendsExceptionand represents unchecked exceptions. - Try-with-resources (Java 7+): The modern way to handle resource cleanup. Any class implementing
AutoCloseable(orCloseable) can be declared in the try header:
finally blocks in pre-Java-7 code and prevents resource leaks. Resources are closed in reverse declaration order.- Exception handling best practices:
- Catch specific exceptions, not
ExceptionorThrowable. - Never swallow exceptions silently (
catch (Exception e) {}is a cardinal sin). - Use exception chaining:
throw new BusinessException("Payment failed", cause)preserves the original stack trace. - Do not use exceptions for flow control —
try/catchis 100-1000x slower than anifcheck.
- Catch specific exceptions, not
- Multi-catch (Java 7+):
catch (IOException | SQLException e)handles multiple exception types in one block, reducing code duplication.
try/catch(Exception e).Red flag answer: Describing try/catch mechanics without mentioning try-with-resources, exception chaining, or anti-patterns.Follow-up:- “What happens if both the try block and the finally block throw exceptions?”
- “Why should you never catch
Throwablein production code?” - “How does try-with-resources handle cleanup when multiple resources are opened?”
Difference between checked and unchecked exceptions?
Difference between checked and unchecked exceptions?
throws). Examples: IOException, SQLException, ClassNotFoundException.
Unchecked: Extend RuntimeException, not enforced by compiler. Examples: NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException.What a strong candidate explains:- The philosophical divide: Checked exceptions represent recoverable conditions — the caller can and should take corrective action (retry, use a fallback, notify the user). Unchecked exceptions represent programming errors — bugs that should be fixed, not caught (null pointers, array bounds violations).
- The controversy: Checked exceptions are unique to Java — most modern languages (Kotlin, Scala, C#, Python, Go) chose not to include them. The criticism: they create verbose code, leak implementation details into APIs, and force callers to either handle exceptions they cannot meaningfully handle or rethrow them up the stack with ugly
throwsdeclarations. - Spring’s approach: Spring deliberately wraps checked exceptions in unchecked ones.
SQLExceptionbecomesDataAccessException(unchecked). This is a design choice: most callers cannot recover from a database error, so forcing them to catch it adds no value. - Modern best practice: Use checked exceptions sparingly — only when the caller can genuinely recover. For everything else, unchecked is cleaner. Many senior engineers and framework designers (including Rod Johnson, Spring’s creator) advocate this approach.
- “Why did Spring choose to wrap checked exceptions in unchecked ones?”
- “When would you create a custom checked exception vs an unchecked one?”
- “How does Kotlin handle the checked exception problem differently from Java?”
What is finally block used for?
What is finally block used for?
- Execution guarantees:
finallyruns even if: (1) no exception occurs, (2) an exception is caught, (3) an exception is not caught, (4) there is areturnstatement in try or catch. The only cases wherefinallydoes not run:System.exit(), JVM crash, or infinite loop/deadlock in try/catch. - The return value trap: If both
tryandfinallyhavereturnstatements, thefinallyreturn value wins. This is confusing behavior and a code smell:
- Suppressed exceptions: If
trythrows exception A andfinallythrows exception B, exception A is lost — only B propagates. Try-with-resources fixes this by attaching A as a suppressed exception on B, accessible viagetSuppressed(). This is a real improvement over manual finally blocks. - Modern replacement: Try-with-resources (
try (resource)) handles 90%+ of whatfinallywas used for, and handles it more correctly. Usefinallyonly for non-AutoCloseablecleanup or when you need cleanup logic that is not tied to a specific resource.
System.exit() behavior. These separate candidates who have debugged weird production issues from those who have only read tutorials.Red flag answer: “Finally always runs” without mentioning the exceptions to that rule or the return-value trap.Follow-up:- “What happens if both try and finally throw different exceptions? Which one propagates?”
- “When does finally NOT execute?”
- “Why is try-with-resources generally preferred over manually writing finally blocks?”
Explain custom exceptions
Explain custom exceptions
Exception (checked) or RuntimeException (unchecked) to represent business-level error conditions.What a strong candidate explains:- When to create one: When existing exceptions do not convey your domain meaning.
throw new InsufficientFundsException(accountId, amount, balance)is far more informative thanthrow new IllegalStateException("Not enough money"). It enables callers to catch and handle specific business conditions differently. - Design best practices:
- Include relevant context fields (IDs, amounts, timestamps) — not just a message string. This enables structured error handling upstream.
- Provide constructor overloads: message-only, message+cause, and cause-only for exception chaining.
- Make custom exceptions
finalif subclassing does not make sense. - Consider an exception hierarchy for your domain:
PaymentExceptionas a base, withInsufficientFundsException,CardDeclinedException,FraudDetectedExceptionas subtypes.
- Spring
@ResponseStatusintegration: Annotate custom exceptions with@ResponseStatus(HttpStatus.NOT_FOUND)to automatically map them to HTTP responses. Or use@ExceptionHandlerand@ControllerAdvicefor centralized exception-to-response mapping — the production-grade approach. - Anti-patterns to avoid:
- Creating exceptions for flow control (
UserNotFoundExceptionwhen you should just returnOptional.empty()). - Having a single
ApplicationExceptionwith error codes — this is effectively re-inventing C-style error codes and defeats the purpose of Java’s type-based exception hierarchy. - Putting business logic in exception constructors.
- Creating exceptions for flow control (
- “Should
UserNotFoundExceptionbe checked or unchecked? Defend your choice.” - “How do you map custom exceptions to HTTP responses in a Spring REST API?”
- “When should you use
Optionalinstead of throwing an exception?”
Difference between throw and throws?
Difference between throw and throws?
throwis used inside a method body to explicitly throw an exception instance:throw new IllegalArgumentException("invalid").throwsis a method signature declaration that lists exceptions the method may throw:void read() throws IOException.
throwis an action,throwsis a contract.throwcreates and dispatches an exception immediately.throwstells the compiler and callers what checked exceptions to expect — it is part of the method’s API contract.- Only checked exceptions require
throws. You can throwRuntimeExceptionsubclasses without declaring them. Declaring unchecked exceptions inthrowsis optional but can serve as documentation. - Throws and API design: Every checked exception in a
throwsclause is a commitment to your callers. Adding one later is a binary-compatible change (existing code still compiles if they do not catch it — wait, no: adding a new checked exception IS a breaking change because callers must now handle it). Removing one is also a breaking change if callers were catching it. This is why checked exceptions in public APIs require careful thought. - Exception translation pattern: Catch a low-level exception and throw a higher-level one to avoid leaking implementation details. A
UserRepository.findById()should throwUserNotFoundException, notSQLException— the caller should not know you are using a SQL database.
- “If you add a new checked exception to a public API’s
throwsclause, is that a breaking change?” - “What is exception translation, and why would you catch one exception only to throw another?“
6. Multithreading & Concurrency
What is a thread in Java?
What is a thread in Java?
Thread, implementing Runnable, or implementing Callable.What a strong candidate explains:- Three ways to create threads:
extends Thread— tightly couples your task to the thread mechanism. Avoid this: you cannot extend another class.implements Runnable— separates the task from the threading mechanism. Better design, butrun()cannot return a value or throw checked exceptions.implements Callable<V>— returns a result and can throw checked exceptions. Used withExecutorService.submit(), which returns aFuture<V>.
- Never create raw threads in production. Use
ExecutorService(thread pools). Raw threads have no reuse, no bounding, and no backpressure. Creating 10,000new Thread()instances in a burst will likely crash your JVM. Thread pools (Executors.newFixedThreadPool(n),ThreadPoolExecutorfor fine-tuning) manage thread lifecycle, bound concurrency, and queue excess work. - Java 21 Virtual Threads (Project Loom): A game-changer. Virtual threads are lightweight (a few KB stack vs ~1MB for platform threads). You can create millions of them. They are scheduled by the JVM, not the OS. This makes the “one-thread-per-request” model viable again for IO-bound services without the complexity of reactive programming.
Thread.ofVirtual().start(() -> ...)orExecutors.newVirtualThreadPerTaskExecutor(). - Thread cost: A platform thread on a 64-bit JVM costs ~1MB of stack memory by default. With 2,000 threads, that is 2GB just in stacks. Plus OS scheduling overhead. This is why reactive frameworks (WebFlux, Vert.x) exist — but virtual threads may make them unnecessary for many use cases.
- “Why should you never create raw threads in production? What do you use instead?”
- “What are virtual threads in Java 21, and how do they change the concurrency story?”
- “Explain the difference between
RunnableandCallable. When would you pick each?”
Explain the life cycle of a thread
Explain the life cycle of a thread
wait(), join(), LockSupport.park()) -> TIMED_WAITING (waiting with a timeout via sleep(), wait(timeout)) -> TERMINATED (completed execution).What a strong candidate explains:- RUNNABLE is not RUNNING. Java does not distinguish between “ready to run” and “actually executing on a CPU core.” The OS scheduler decides which runnable threads get CPU time. This is why
Thread.yield()is just a hint — the scheduler can ignore it. On a single-core machine, only one thread is truly running at any instant. - BLOCKED vs WAITING: This distinction matters for debugging. BLOCKED means the thread is trying to enter a
synchronizedblock/method but another thread holds the lock. WAITING means the thread voluntarily gave up execution (calledwait(),join(), orpark()). In thread dumps, BLOCKED threads indicate lock contention; WAITING threads are usually expected behavior (threads in a pool waiting for tasks). - How to read a thread dump:
jstack <pid>orkill -3 <pid>on Linux produces a thread dump showing every thread’s state and stack trace. In production incident response, the first thing you do is take 3-5 thread dumps 5 seconds apart and compare them. If the same threads are BLOCKED on the same lock in every dump, you have a lock contention problem. If threads are WAITING on the same object, you may have a deadlock. - State transitions that catch people:
sleep()goes to TIMED_WAITING, not BLOCKED. The thread still holds any locks it acquired.wait()goes to WAITING and releases the monitor lock. This is a critical difference fromsleep().- A thread cannot go from TERMINATED back to RUNNABLE — threads are not restartable.
- “How do you diagnose a production issue where the application seems frozen? Walk me through your approach.”
- “What is the difference between
sleep()andwait()in terms of lock behavior?” - “Can a terminated thread be restarted? What happens if you call
start()again?”
Difference between start() and run()?
Difference between start() and run()?
start() creates a new OS thread and executes run() in that new thread. Calling run() directly executes the method in the current thread — no new thread is created.What a strong candidate explains:- Under the hood:
start()calls a native method (start0()) that asks the OS to create a new thread, allocate a stack, and begin executing therun()method on that new thread. Callingrun()directly is just a regular method call — no concurrency, no parallelism. - Common bug: Calling
run()instead ofstart()is one of the most frequent beginner concurrency bugs. It compiles and runs without error — but everything executes sequentially on the main thread. In testing, this might even produce correct results (no race conditions because there is no concurrency), but performance will be wrong. - Double start: Calling
start()on a thread that has already been started throwsIllegalThreadStateException. A thread object is single-use. If you need to run the same task again, create a newThreadinstance — or better, submit it to anExecutorServicewhich handles thread reuse. - Debug tip: If your “concurrent” code is not faster than sequential, check if you are accidentally calling
run()instead ofstart().
- “What happens if you call
start()on a thread that has already finished executing?” - “In what scenario might calling
run()directly instead ofstart()actually be useful?”
What is synchronization?
What is synchronization?
synchronizedkeyword: Works on methods (locks onthisfor instance methods, locks onClassobject for static methods) or blocks (locks on a specified object). Every object in Java has an intrinsic monitor lock (mutex).- The real problem synchronization solves: Without it, two threads incrementing a shared counter (
count++) can interleave their read-modify-write operations, losing updates.count++is not atomic — it compiles to: load count, increment, store count. Two threads can both read 5, both increment to 6, and store 6 — losing one increment. - Beyond
synchronized—java.util.concurrent.locks:ReentrantLock— same semantics assynchronizedbut with additional features: tryLock (non-blocking attempt), lockInterruptibly (can be interrupted while waiting), and fairness option (FIFO ordering).ReadWriteLock— multiple concurrent readers, exclusive writer. Huge throughput improvement for read-heavy workloads (e.g., a cache with 95% reads).StampedLock(Java 8) — optimistic read locking for even better read performance.
- Atomic classes:
AtomicInteger,AtomicLong,AtomicReferenceuse CAS (Compare-And-Swap) hardware instructions for lock-free thread safety. For a simple counter,AtomicInteger.incrementAndGet()is 3-5x faster thansynchronizedunder high contention. - Deadlock: Occurs when two threads each hold a lock the other needs. Classic scenario: Thread A locks resource 1, then tries to lock resource 2. Thread B locks resource 2, then tries to lock resource 1. Both wait forever. Prevention: always acquire locks in a consistent global order.
synchronized (Lock API, atomics) and can explain deadlock scenarios. This is a must-know for any Java backend role.Red flag answer: “Use the synchronized keyword” as the complete answer, with no mention of Lock API, atomics, or deadlock.Follow-up:- “What is the advantage of
ReentrantLockoversynchronized?” - “Explain a deadlock scenario and how you would prevent it.”
- “When would you use
AtomicIntegerinstead ofsynchronized?”
Explain volatile keyword
Explain volatile keyword
volatile guarantees visibility — changes to a volatile variable by one thread are immediately visible to all other threads. It prevents the JVM from caching the variable in CPU registers or reordering reads/writes around it.What a strong candidate explains:- What volatile solves: Without it, the JVM and CPU may cache a variable’s value in a thread-local register. One thread updates the value, but another thread keeps reading the stale cached value. A common symptom: a boolean
runningflag checked in a loop — withoutvolatile, the loop may never see the flag change tofalseand run forever.
- What volatile does NOT solve: It does not provide atomicity.
volatile int count; count++is still not thread-safe becausecount++is a compound read-modify-write operation. Two threads can still interleave. For atomic compound operations, useAtomicIntegerorsynchronized. - Memory barrier semantics: A volatile write acts as a “release” fence — all writes before it become visible to other threads. A volatile read acts as an “acquire” fence — all subsequent reads see up-to-date values. This is the foundation of the Java Memory Model’s happens-before relationship.
- Common use cases: (1) Flags for thread termination (the
runningexample above), (2) Double-checked locking for singletons (theinstancefield must be volatile to prevent seeing a partially constructed object), (3) Publishing immutable objects between threads. - Volatile vs synchronized: Volatile is lighter weight (no lock acquisition) but limited (only visibility, not atomicity). Use volatile for simple flag/state publication; use synchronized or atomics when you need compound operations.
- “Is
volatilesufficient to makecount++thread-safe? Why or why not?” - “Explain the Java Memory Model’s happens-before relationship in the context of volatile.”
- “Why must the
instancefield in the double-checked locking singleton pattern be volatile?“
7. Java 8 & Functional Programming
What are lambda expressions?
What are lambda expressions?
(parameters) -> expression or (parameters) -> { statements; }. They implement functional interfaces (interfaces with a single abstract method).What a strong candidate explains:- Not just syntax sugar: Lambdas are not anonymous inner classes compiled differently. The JVM uses
invokedynamic(introduced in Java 7 for dynamic languages) to generate lambda implementations at runtime viaLambdaMetafactory. This means no extra.classfiles, no anonymous class overhead, and the JVM can optimize lambda calls more aggressively (including inlining). - Variable capture: Lambdas can capture local variables, but only if they are effectively final. This is because the lambda may outlive the stack frame where the variable was declared — the captured value is copied, not referenced. Mutating captured variables would create confusing semantics, so Java forbids it.
- Common functional interfaces:
Predicate<T>— takes T, returns boolean. Used infilter().Function<T, R>— takes T, returns R. Used inmap().Consumer<T>— takes T, returns void. Used inforEach().Supplier<T>— takes nothing, returns T. Used inorElseGet().BiFunction<T, U, R>— takes two params, returns one.
- Real-world impact: Lambdas + streams transformed Java code from verbose imperative loops to concise declarative pipelines. A 10-line loop with a temporary list, filter condition, and transformation becomes:
items.stream().filter(i -> i.isActive()).map(Item::getName).collect(toList()). - Gotcha — exception handling: Lambdas do not play well with checked exceptions.
Function<String, Integer>cannot throwIOException. You have to either catch inside the lambda (ugly), create custom functional interfaces withthrows, or use libraries like Vavr that provide checked functional interfaces.
- “How are lambdas compiled differently from anonymous inner classes?”
- “Why must captured variables be effectively final?”
- “How do you handle checked exceptions inside a lambda?”
What are streams in Java 8?
What are streams in Java 8?
filter, map, reduce, collect, and flatMap. They are not data structures — they are pipelines that process data from a source (collection, array, I/O channel).What a strong candidate explains:- Lazy evaluation: Intermediate operations (
filter,map,sorted) are lazy — they do not execute until a terminal operation (collect,forEach,count,reduce) is invoked. This enables short-circuiting:list.stream().filter(x -> x > 10).findFirst()stops processing as soon as the first match is found, even on a million-element list. - Stream pipeline lifecycle: Source -> intermediate operations (zero or more) -> terminal operation (exactly one). A stream can only be consumed once — calling a terminal operation “closes” the stream.
- Parallel streams:
list.parallelStream()orstream().parallel()splits work across multiple threads using the commonForkJoinPool. But be careful: parallel streams have overhead (splitting, thread coordination, merging). They only help for CPU-intensive operations on large datasets. For small collections or IO-bound work, parallel streams are slower. A rule of thumb: at least 10,000 elements and a non-trivial per-element computation. - Collectors:
Collectors.toList(),Collectors.toMap(),Collectors.groupingBy(),Collectors.partitioningBy(),Collectors.joining(). Custom collectors are possible viaCollector.of().Collectors.groupingBy()is the stream equivalent of SQL’sGROUP BYand is incredibly powerful. - Common mistakes:
- Modifying the source collection during stream processing (ConcurrentModificationException).
- Using
forEachfor everything instead ofmap+collect— this is “stream abuse” that is worse than a simple for loop. - Side effects in intermediate operations (non-deterministic behavior in parallel streams).
- “When would you NOT use parallel streams? What determines whether they help?”
- “What is the difference between
map()andflatMap()? Give a real example.” - “How does
Collectors.groupingBy()work? Can you nest collectors?”
What is Optional class?
What is Optional class?
Optional<T> is a container that may or may not hold a non-null value. Introduced in Java 8 to provide a better alternative to returning null and to make “absence of value” explicit in the type system.What a strong candidate explains:- The problem it solves:
nullis Tony Hoare’s “billion dollar mistake.” Returningnullfrom a method gives callers zero indication that absence is possible.Optionalmakes it explicit:Optional<User> findById(long id)clearly communicates that the user might not exist. - Creation:
Optional.of(value)(throws NPE if value is null),Optional.ofNullable(value)(wraps null safely),Optional.empty(). - Usage patterns (good):
- Anti-patterns (bad):
- Using
Optionalas a method parameter — use overloaded methods or null instead. Optional was designed for return types. - Using
Optionalfor class fields — it is notSerializableand adds overhead. Use nullable fields. - Calling
isPresent()thenget()— this is just null checking with extra steps. UseifPresent(),map(), ororElse(). - Using
Optionalin collections:List<Optional<String>>— just filter out nulls instead.
- Using
- Performance:
Optionalcreates an object on the heap. In hot paths processing millions of values, the allocation overhead matters. Java may eventually optimize this away with value types (Project Valhalla), but for now, avoid Optional in performance-critical loops.
if (opt.isPresent()) opt.get() — this misses the entire point of the API.Follow-up:- “Why should you not use Optional as a method parameter or a field type?”
- “What is the difference between
orElse()andorElseGet()? When does it matter?” - “How would you refactor a chain of null checks into Optional’s functional API?”
What are method references?
What are method references?
ClassName::methodName. Four types: static (Math::max), instance of a particular object (myObj::toString), instance of an arbitrary object (String::toLowerCase), and constructor (ArrayList::new).What a strong candidate explains:- The four types in detail:
- Static method reference:
Integer::parseIntis equivalent tos -> Integer.parseInt(s). - Bound instance method:
System.out::printlnis equivalent tox -> System.out.println(x). The instance (System.out) is captured. - Unbound instance method:
String::lengthis equivalent tos -> s.length(). The first argument becomes the receiver. - Constructor reference:
ArrayList::newis equivalent to() -> new ArrayList<>()or(capacity) -> new ArrayList<>(capacity)depending on context.
- Static method reference:
- When to use method references vs lambdas: Method references are preferred when the lambda simply delegates to an existing method with no additional logic. If you need to transform arguments, add conditions, or call multiple methods, use a lambda.
- Gotcha with overloaded methods: If a method is overloaded, the compiler uses the functional interface’s signature to determine which overload to bind. This can sometimes be confusing and may require an explicit lambda for clarity.
- Real-world readability:
names.stream().map(String::toUpperCase).collect(toList())is more readable thannames.stream().map(s -> s.toUpperCase()).collect(toList()). Butitems.stream().filter(i -> i.getPrice() > 100)cannot be expressed as a method reference (it has additional logic).
:: instead of arrow” — no awareness of the four forms or when to use lambdas instead.Follow-up:- “What is the difference between a bound and unbound instance method reference?”
- “Can you use a method reference when the method is overloaded? What happens?”
Explain functional interfaces
Explain functional interfaces
@FunctionalInterface (optional but recommended — the compiler enforces the SAM constraint).What a strong candidate explains:- Core functional interfaces in
java.util.function:
| Interface | Input | Output | Use Case |
|---|---|---|---|
Predicate<T> | T | boolean | Filtering |
Function<T,R> | T | R | Transformation |
Consumer<T> | T | void | Side effects |
Supplier<T> | none | T | Lazy generation |
UnaryOperator<T> | T | T | Same-type transform |
BiFunction<T,U,R> | T, U | R | Two-input transform |
@FunctionalInterfaceis a compile-time guard: Like@Override, it is not required for functionality, but it prevents accidental addition of a second abstract method. Without it, addingvoid anotherMethod()silently breaks all lambda call sites — with it, the compiler catches the error immediately.- Composition via default methods:
Predicatehasand(),or(),negate().Functionhascompose()andandThen(). This enables building complex behaviors from simple building blocks:
- Existing functional interfaces you already know:
Runnable(zero args, void),Callable<V>(zero args, returns V),Comparator<T>(two args, returns int). These predate Java 8 but are now usable as lambda targets. - Primitive specializations:
IntPredicate,LongFunction,DoubleSupplieravoid autoboxing overhead. In performance-critical code, preferIntStreamwithIntPredicateoverStream<Integer>withPredicate<Integer>.
- “Why do primitive specializations like
IntPredicateexist alongsidePredicate<Integer>?” - “How would you compose multiple predicates together? Show the API.”
- “What happens if you accidentally add a second abstract method to a functional interface that is used in 50 places?“
8. Memory Management & Garbage Collection
How does garbage collection work?
How does garbage collection work?
System.gc(), but the JVM is free to ignore it.What a strong candidate explains:- Generational hypothesis: Most objects die young. The JVM exploits this by dividing the heap into generations:
- Young Generation (Eden + Survivor S0/S1): New objects are allocated in Eden. When Eden fills, a Minor GC runs, copying surviving objects to a Survivor space. Objects that survive multiple Minor GCs (default threshold: 15 cycles) are promoted to Old Generation.
- Old Generation (Tenured): Long-lived objects. Collected during Major GC (or Full GC), which is much more expensive — can pause the application for seconds.
- GC algorithms (know at least 3):
- G1 (Garbage First): Default since Java 9. Region-based, targets a configurable pause time (
-XX:MaxGCPauseMillis=200). Good general-purpose collector. - ZGC: Ultra-low-latency (<1ms pauses) even with multi-terabyte heaps. Uses colored pointers and load barriers. Production-ready since Java 15.
- Shenandoah: Similar goals to ZGC, concurrent compaction. Available in OpenJDK.
- Parallel GC: Throughput-optimized, longer pauses. Good for batch processing.
- Serial GC: Single-threaded, for small heaps/containers.
- G1 (Garbage First): Default since Java 9. Region-based, targets a configurable pause time (
- GC tuning in practice: At a high-throughput service processing 50K requests/sec, GC pauses directly impact p99 latency. Steps: (1) Enable GC logging (
-Xlog:gc*), (2) Analyze with GCViewer or GCEasy, (3) Identify if pauses are Minor or Full GC, (4) Tune heap size and generation ratios, (5) Consider switching collector (G1 to ZGC for latency-sensitive services). - Never call
System.gc()in production. It triggers a Full GC that can pause the application for seconds. Some frameworks (like DirectByteBuffer cleanup) use it internally, but application code should not.
- “What is the difference between Minor GC and Full GC? Which one should you worry about?”
- “When would you choose ZGC over G1? What are the trade-offs?”
- “Walk me through how you would investigate a Java service with p99 latency spikes caused by GC.”
What are strong, weak, and soft references?
What are strong, weak, and soft references?
- Strong: Normal references (
Object o = new Object()). GC will not collect as long as a strong reference exists. - Soft: Collected only when the JVM is running low on memory. Created via
SoftReference<T>. - Weak: Collected at the next GC cycle if only weakly reachable. Created via
WeakReference<T>. - Phantom: Cannot access the referent. Used for cleanup actions before finalization. Created via
PhantomReference<T>with aReferenceQueue.
- Soft references for caches:
SoftReferenceis ideal for memory-sensitive caches. The object stays in memory as long as there is plenty of heap space, but is eligible for collection under memory pressure. Example: an image cache that holds images in memory when possible but lets the GC reclaim them before throwingOutOfMemoryError. Guava’sCacheBuilder.softValues()uses this. - Weak references for metadata:
WeakHashMapuses weak keys — entries are automatically removed when the key is no longer strongly referenced elsewhere. Common use case: associating metadata with objects without preventing their GC.ThreadLocalcleanup also relies on weak references. - Phantom references for finalization replacement: Phantom references are enqueued in a
ReferenceQueueafter the referent is finalized. You can use a daemon thread to poll the queue and perform cleanup. This is the modern replacement for the deprecatedfinalize()method. Java 9+CleanerAPI wraps this pattern. - Real-world production bug: A common memory leak pattern: using a
Map<Key, Value>where the Keys should be garbage collected but the Map prevents it. Switching toWeakHashMapor usingCaffeinecache with weak keys fixes the leak.
- “How would you implement a memory-sensitive cache using soft references?”
- “What is
WeakHashMapand when would you use it?” - “What replaced
finalize()in modern Java, and how does it use phantom references?”
What are memory leaks in Java?
What are memory leaks in Java?
OutOfMemoryError.What a strong candidate explains:- Common leak patterns:
- Static collections:
static List<Object> cache = new ArrayList<>();that grows forever. Static fields live as long as the ClassLoader (effectively forever in most apps). - Unclosed resources: Database connections, file handles, HTTP clients not closed. Connection pools exhaust, file descriptor limits hit.
- Listener/callback registration without deregistration: Registering an event listener and never removing it. The listener holds a reference to its enclosing object. Common in GUI applications and Spring event handling.
- Inner class references: Non-static inner classes hold an implicit reference to the enclosing instance. If the inner class instance outlives the outer, the outer cannot be collected.
- ThreadLocal variables: Not calling
remove()in thread pools. The thread lives forever (pool), the ThreadLocal value lives forever, and everything it references lives forever. This caused massive memory leaks at several companies running web apps on Tomcat. - String.intern() abuse: Interning user-provided strings (like session IDs) fills the String Pool permanently.
- Static collections:
- Detection tools:
- Heap dumps:
jmap -dump:format=b,file=heap.hprof <pid>or-XX:+HeapDumpOnOutOfMemoryError(automatic dump on OOM). - Analysis: Eclipse MAT (Memory Analyzer Tool), VisualVM, YourKit. Look at the “dominator tree” to find which objects retain the most memory.
- Monitoring: Track heap usage over time with JMX, Micrometer, or Prometheus. A steadily increasing heap between Full GCs (the “sawtooth” pattern with rising baseline) indicates a leak.
- Heap dumps:
- A war story: ThreadLocal memory leak in a Spring Boot app running on embedded Tomcat. Each request set a
ThreadLocal<UserContext>but never calledremove(). With a thread pool of 200 threads, this leaked 200UserContextobjects (each holding a database session). Over days, the heap grew by hundreds of MB. Fix:try/finallywiththreadLocal.remove(), or a servlet filter that clears ThreadLocals after each request.
- “How do you take and analyze a heap dump? Walk me through the process.”
- “What is the ThreadLocal memory leak pattern, and how do you prevent it?”
- “How can you tell from monitoring data that you have a memory leak vs just a too-small heap?”
How to prevent OutOfMemoryError?
How to prevent OutOfMemoryError?
- Types of OOM (they are not all the same):
Java heap space— the heap is full. Most common. Increase-Xmxor fix the leak.GC overhead limit exceeded— GC running constantly but freeing very little memory. Usually means the heap is nearly full with live objects.Metaspace— too many classes loaded (common with dynamic proxies, heavy reflection, or classloader leaks in app servers). Increase-XX:MaxMetaspaceSize.unable to create native thread— OS thread limit hit. Reduce thread count or increaseulimit -u.Direct buffer memory— too manyByteBuffer.allocateDirect()calls. Increase-XX:MaxDirectMemorySize.
- Prevention strategies:
- Size your heap correctly: Use load testing to determine steady-state memory usage. Set
-Xmxto 2-3x steady state. Never set it to the maximum available RAM — leave room for the OS, metaspace, thread stacks, and direct buffers. - Use streaming for large data: Process files and query results with streams/iterators instead of loading everything into memory. A 2GB CSV should be read line by line, not loaded into
List<String>. - Object pooling and caching with eviction: Use bounded caches (Caffeine, Guava) with size limits and TTL. Never use an unbounded
HashMapas a cache. - Close resources: Try-with-resources for everything. Connection pool limits prevent connection object accumulation.
- Monitor in production:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprofautomatically captures a heap dump when OOM occurs. Set up alerting on heap usage trends.
- Size your heap correctly: Use load testing to determine steady-state memory usage. Set
- JVM flags every Java developer should know:
-Xms/-Xmx: Initial and max heap size.-XX:+UseG1GCor-XX:+UseZGC: GC algorithm selection.-Xlog:gc*: GC logging.-XX:+HeapDumpOnOutOfMemoryError: Auto heap dump.-XX:MaxMetaspaceSize: Metaspace cap.
- “What is the difference between
Java heap spaceandGC overhead limit exceededOOM errors?” - “How do you size the JVM heap for a containerized application?”
- “What JVM flags do you always set in production and why?”
Explain finalize() method
Explain finalize() method
finalize() was a method called by the GC before collecting an object, intended for cleanup. It is deprecated since Java 9 and removed for removal in Java 18. Never use it.What a strong candidate explains:- Why finalize() is terrible:
- Non-deterministic: You have no idea when (or if)
finalize()will be called. The GC runs on its own schedule. Objects with finalizers require at least two GC cycles to be collected — they are put on a finalization queue, finalized, then collected in the next cycle. - Performance penalty: Objects with finalizers cannot be allocated in thread-local allocation buffers (TLABs) on some JVMs. They slow down GC by 50-100x for those objects.
- Resurrection risk: Inside
finalize(), you can storethisin a static field, “resurrecting” the object. This creates bizarre bugs and confuses the GC. - No ordering guarantees: If object A references object B and both have finalizers, there is no guarantee which finalizer runs first. A’s finalizer might access B after B has already been finalized.
- Exceptions are swallowed: If
finalize()throws an exception, it is silently ignored. No logging, no notification.
- Non-deterministic: You have no idea when (or if)
- Modern alternatives:
try-with-resources+AutoCloseable: The primary mechanism for deterministic cleanup. Close resources when you are done, not when the GC gets around to it.Cleaner(Java 9+): Registers cleaning actions that run when an object becomes phantom-reachable. Used by JDK internals (DirectByteBuffer cleanup). Better than finalize() but still non-deterministic — use as a safety net, not primary cleanup.
- If you see
finalize()in a codebase, it is almost certainly a bug or legacy code that needs refactoring.
finalize() is harmful and what the modern alternatives are. Advocating for finalize() is a red flag.Red flag answer: “finalize() cleans up before GC” without mentioning that it is deprecated, slow, and dangerous.Follow-up:- “What is the
CleanerAPI introduced in Java 9, and how does it improve onfinalize()?” - “Why does an object with a finalizer take at least two GC cycles to be collected?”
- “What is the modern best practice for ensuring a resource (like a native handle) gets cleaned up?“
9. Spring & Framework Concepts
What is Spring Framework?
What is Spring Framework?
- Core philosophy: Spring inverts control — instead of your code creating dependencies, the framework creates and wires them. This makes code testable (inject mocks), loosely coupled (depend on interfaces), and configurable (swap implementations without code changes).
- Key modules:
- Spring Core/IoC: The DI container. The foundation everything else builds on.
- Spring MVC: Web framework for building REST APIs and server-rendered pages.
- Spring Data: Abstracts database access (JPA, MongoDB, Redis, Elasticsearch) with repository interfaces.
- Spring Security: Authentication and authorization.
- Spring AOP: Cross-cutting concerns (logging, transactions, security) via proxies.
- Spring Cloud: Distributed systems patterns (service discovery, config server, circuit breakers).
- Why companies use Spring: It is the dominant Java backend framework because of its ecosystem maturity, extensive documentation, massive community, and opinionated-but-flexible design. Companies like Netflix (until they migrated parts to Go/Node), Amazon (internal services), and most Fortune 500 Java shops use Spring.
- Spring vs Spring Boot: Spring is the framework. Spring Boot is the opinionated auto-configuration layer on top. Spring Boot eliminated the “XML hell” of early Spring with sensible defaults, embedded servers, and starter dependencies.
- “Explain Inversion of Control in Spring without using the word ‘annotation’ — what is the underlying concept?”
- “How does Spring create and manage bean instances? Walk me through the lifecycle.”
- “What problem does Spring solve that you could not solve with plain Java?”
Explain dependency injection (DI)
Explain dependency injection (DI)
- Three injection types (and which to prefer):
- Constructor injection (recommended): Dependencies are provided via the constructor. Fields can be
final, ensuring immutability. All dependencies are required — the object cannot be created in an invalid state. Since Spring 4.3,@Autowiredis optional on single-constructor beans. - Setter injection: Dependencies set via setter methods. Use for optional dependencies. Allows reconfiguration after construction (rarely needed).
- Field injection (
@Autowiredon fields): Convenient but problematic — cannot make fieldsfinal, hides dependencies (not visible in constructor), makes unit testing harder (need reflection or Spring test context to inject).
- Constructor injection (recommended): Dependencies are provided via the constructor. Fields can be
- Why constructor injection wins: It makes dependencies explicit (you see them in the constructor signature), enforces immutability (
finalfields), and enables easy testing (just call the constructor with mocks — no Spring context needed).
- Circular dependency problem: If Bean A depends on Bean B and Bean B depends on Bean A, Spring cannot construct either. Spring 5 throws an error at startup for constructor injection circular dependencies (earlier versions silently used field injection workarounds). The fix: redesign — circular dependencies almost always indicate a design flaw. Extract the shared logic into a third bean.
@Qualifierand@Primary: When multiple beans implement the same interface, use@Qualifier("beanName")to select the specific one, or@Primaryto designate a default.
@Autowired on a field” as the primary approach, with no awareness of constructor injection or why it is preferred.Follow-up:- “Why is constructor injection preferred over field injection?”
- “How do you handle circular dependencies in Spring? Is there a design-level fix?”
- “How would you inject different implementations of the same interface depending on the profile or environment?”
What is the difference between BeanFactory and ApplicationContext?
What is the difference between BeanFactory and ApplicationContext?
- BeanFactory: The basic IoC container. Provides bean creation and wiring. Lazy initialization by default.
- ApplicationContext: Extends BeanFactory with enterprise features. Eager initialization by default.
- ApplicationContext adds:
- Event publishing:
ApplicationEventPublisherfor the observer pattern. Publish custom domain events (OrderCreatedEvent) and have any number of listeners react without coupling. - Internationalization (i18n):
MessageSourcefor localized messages. - Environment abstraction: Access to properties, profiles (
@Profile("prod")), and property sources. - AOP integration: Automatic proxy creation for
@Transactional,@Cacheable,@Async. - Bean lifecycle hooks:
@PostConstruct,@PreDestroy,BeanPostProcessor,BeanFactoryPostProcessor.
- Event publishing:
- Lazy vs eager initialization: BeanFactory creates beans on first request (lazy). ApplicationContext creates all singleton beans at startup (eager). This means ApplicationContext fails fast — misconfiguration errors surface at startup, not at runtime. This is a production advantage: better to fail in deployment than at 3 AM under load.
- In practice, you always use ApplicationContext. Specifically,
AnnotationConfigApplicationContextfor standalone apps orSpringApplication.run()in Spring Boot (which creates aWebApplicationContext). Direct BeanFactory use is essentially never needed in modern Spring. - Bean scopes:
singleton(default — one instance per container),prototype(new instance per request),request(one per HTTP request),session(one per HTTP session). Understanding scopes is critical: injecting a prototype-scoped bean into a singleton gives you the same prototype instance forever (must useObjectProvider<T>or method injection for correct behavior).
- “What happens if you inject a prototype-scoped bean into a singleton? How do you fix it?”
- “How do bean lifecycle callbacks (
@PostConstruct,@PreDestroy) work? When do they execute?” - “What is a
BeanPostProcessorand when would you write a custom one?”
Explain Spring Boot
Explain Spring Boot
- Auto-configuration magic: Spring Boot examines your classpath and automatically configures beans. If
spring-boot-starter-data-jpais on the classpath and you have aDataSourceconfigured, Spring Boot auto-createsEntityManagerFactory,TransactionManager, and repository beans. The key:@EnableAutoConfiguration(included in@SpringBootApplication) triggersMETA-INF/spring.factoriesscanning (orMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsin 3.0+). - Starter dependencies: Curated dependency bundles.
spring-boot-starter-webpulls in Spring MVC, Jackson, Tomcat, validation.spring-boot-starter-testpulls in JUnit 5, Mockito, AssertJ, Spring Test. This eliminates dependency version conflicts — the Boot BOM (Bill of Materials) ensures compatible versions. - Externalized configuration: Application properties (
application.ymlor.properties) with profiles (application-prod.yml), environment variables, command-line arguments. Configuration precedence order matters: command-line args > env vars >application-{profile}.yml>application.yml. Twelve-Factor App compliance out of the box. - Spring Boot Actuator: Production monitoring endpoints:
/actuator/health(for load balancers),/actuator/metrics(Micrometer metrics),/actuator/info,/actuator/env. Integrates with Prometheus, Datadog, New Relic. In production, you expose/healthpublicly and secure everything else. - Spring Boot 3.0 changes: Requires Java 17+, migrated from Java EE (
javax.*) to Jakarta EE (jakarta.*), native compilation support via GraalVM (AOT compilation for sub-second startup — important for serverless/FaaS).
- “How does Spring Boot auto-configuration work internally? What triggers it?”
- “How do you override or exclude a specific auto-configuration?”
- “What is Spring Native / GraalVM native image, and when would you use it?”
What are common Spring annotations?
What are common Spring annotations?
@Component, @Service, @Repository, @Controller, @RestController, @Autowired, @Configuration, @Bean, @Value, @Transactional, @Profile.What a strong candidate explains (not just lists):- Stereotype annotations (component scanning):
@Component— generic Spring-managed bean.@Service— business logic layer. No additional behavior over@Component, but signals intent.@Repository— data access layer. Adds automatic exception translation: vendor-specific exceptions (like Hibernate’sConstraintViolationException) are wrapped into Spring’sDataAccessExceptionhierarchy.@Controller— MVC controller, returns views.@RestController=@Controller+@ResponseBody(returns JSON/XML directly).
- Configuration annotations:
@Configuration— declares a bean definition source. Methods annotated with@Beanproduce Spring-managed instances.@Configurationclasses are CGLIB-proxied so that@Beanmethods calling other@Beanmethods return the same singleton (not a new instance).@Bean— method-level bean declaration, used when you cannot annotate the source class (third-party libraries).
- Behavior annotations:
@Transactional— wraps the method in a database transaction via AOP proxy. Gotcha:@Transactionaldoes not work onprivatemethods (proxy cannot intercept) or on self-invocation (callingthis.method()bypasses the proxy). This causes silent transaction bugs that are hard to debug.@Async— executes the method in a separate thread. Same proxy limitations as@Transactional.@Cacheable— caches the return value.@CacheEvictclears the cache.
- The proxy trap (critical to understand): Spring creates a proxy around your bean to implement
@Transactional,@Async,@Cacheable. When you call a method onthis(self-invocation), you bypass the proxy, and the annotation has no effect. This is the #1 Spring annotation gotcha in production.
@Transactional.Red flag answer: Listing annotations without explaining @Transactional proxy limitations or the difference between @Repository and @Component.Follow-up:- “Why does
@Transactionalnot work when you call it from within the same class? How do you fix it?” - “What is the difference between
@Componentand@Bean? When would you use each?” - “How does
@Repositoryexception translation work?“
10. Java Interview Best Practices
Common performance optimization tips
Common performance optimization tips
- Data structure selection matters most. Switching from
LinkedListtoArrayListfor random access, fromHashMaptoEnumMapfor enum keys, or fromTreeMaptoHashMapwhen sorting is not needed can yield 2-10x improvement with zero algorithmic changes. - String concatenation in loops: Never use
+in a loop — it creates a newStringobject each iteration. UseStringBuilderfor explicit appending. Java’s compiler optimizes single-statement concatenation ("a" + b + "c") but not loop concatenation. - Stream vs loop performance: Streams add overhead (object creation for lambdas, stream pipeline setup). For simple operations on small collections (<1000 elements), a for-loop is faster. For large collections or parallelizable operations, streams can be faster. Benchmark, do not assume.
- Connection pooling: Never create database connections per request. Use HikariCP (Spring Boot default) with appropriate pool sizing. A formula from HikariCP’s wiki:
connections = ((core_count * 2) + effective_spindle_count). For an 8-core server with SSDs, start with ~20 connections. - Avoid premature optimization. Profile first with async-profiler, JFR (Java Flight Recorder), or YourKit. Identify actual hotspots. The 80/20 rule applies: 80% of time is spent in 20% of code. Optimizing the wrong code wastes engineering time.
- JVM flags:
-XX:+UseG1GC(or-XX:+UseZGCfor low latency),-Xmsand-Xmxset to the same value (avoids resize pauses),-XX:+AlwaysPreTouch(page-in memory at startup, avoids latency spikes later).
- “How would you profile a Java application that is slower than expected?”
- “What is the HikariCP connection pool sizing formula, and why does it work?”
Common mistakes to avoid
Common mistakes to avoid
- Forgetting
equals()andhashCode(): Put an object in aHashSet, mutate a field used inequals(), and the set now contains a “ghost” entry you can never find or remove. Tools: Lombok@EqualsAndHashCode, IDE generation, or Java records. - Neglecting resource closure: Pre-Java-7 code littered with
finallyblocks that themselves threw exceptions. Modern fix: always use try-with-resources. Spotbugs and SonarQube flag unclosed resources automatically. - Ignoring concurrency: Using
HashMapin a multithreaded context causes infinite loops (the internal linked list can form a cycle during concurrent resize — this was a real production incident at many companies running Java 7). Fix:ConcurrentHashMap. - Catching
Exceptionbroadly:catch (Exception e) { log.error("error", e); }masks bugs. Catch specific exceptions and handle them appropriately. Let unexpected exceptions propagate. - Mutable objects as Map keys: See the
equals()/hashCode()point above. Always use immutable keys (String, Integer, records, value objects). - Blocking in reactive pipelines: If you use WebFlux or any reactive framework, calling
Thread.sleep(), synchronous JDBC, orsynchronizedblocks destroys throughput by blocking the event loop thread. Use reactive drivers (R2DBC) and non-blocking APIs. - Not configuring thread pool sizes: Default
ForkJoinPool.commonPool()hasavailableProcessors() - 1threads. If your parallel streams share this pool with other framework code and block on IO, the entire pool stalls.
- “Have you encountered a HashMap infinite loop bug? Explain how it happens at the implementation level.”
- “How do you prevent mutable-key bugs in a large codebase?”
How to prepare for coding interviews?
How to prepare for coding interviews?
- Data structures & algorithms in Java: Focus on arrays, strings, HashMaps, trees, graphs, and dynamic programming. Practice on LeetCode (aim for 150-200 medium problems). Know Java’s built-in data structures cold:
PriorityQueuefor heaps,Dequefor stacks/queues,TreeMapfor ordered maps withfloorKey()/ceilingKey(). - Java-specific interview patterns:
- Know the Collections API deeply:
Collections.sort()withComparator,Map.merge(),Map.computeIfAbsent(),List.subList(). - Master Streams for data processing questions:
groupingBy,toMap,flatMap,reduce. - String manipulation:
StringBuilder,String.chars(), regex withPatternandMatcher.
- Know the Collections API deeply:
- System design with Java specifics: Discuss thread pools, connection pools, caching (Caffeine, Redis), message queues (Kafka), load balancing, and database sharding. Show you know how to implement these in Spring Boot.
- Build a portfolio project: A Spring Boot REST API with JPA, proper exception handling, validation, testing (JUnit 5 + Mockito), Docker, and CI/CD. This demonstrates production readiness.
- Mock interviews: Practice explaining your thought process out loud. Interviewers evaluate communication as much as correctness. Use the “clarify, approach, code, test” framework.
- “Walk me through how you would approach a problem you have never seen before.”
- “Which Java collections are most useful for solving coding problems, and why?”
Most asked Java interview questions
Most asked Java interview questions
- HashMap internals — How
put()andget()work, bucket collisions, the linked-list-to-tree threshold. This is the single most common senior Java question. equals()andhashCode()contract — What breaks when they are inconsistent. Write a correct implementation.synchronizedvsvolatilevsAtomicInteger— Different tools for different concurrency problems.- JVM memory model — Heap vs stack, Young Gen vs Old Gen, GC algorithms.
- OOP principles with real examples — Not definitions. “Tell me about a time you used polymorphism to solve a real problem.”
- Lambda expressions and Streams — Write a stream pipeline that groups, filters, and transforms. Know
flatMap. - Exception handling best practices — Checked vs unchecked, try-with-resources, custom exceptions.
- Spring DI and
@Transactional— Constructor injection, the proxy self-invocation trap. - Thread pool configuration — How
ThreadPoolExecutorworks, core vs max pool size, the task queue. - Immutability — How to create immutable objects, why
Stringis immutable, records.
- “Pick any one of these topics and explain it to me as if I know nothing about Java.”
Tips for Java system design interviews
Tips for Java system design interviews
- Thread pool design:
ThreadPoolExecutorhas core pool size (threads created even when idle), max pool size (upper bound), keep-alive time (how long excess threads live), and a work queue (bounded vs unbounded). Unbounded queues (LinkedBlockingQueue) are dangerous — they let the queue grow until OOM. Use bounded queues with rejection policies (CallerRunsPolicyprovides backpressure). - Caching layers: L1 (in-process, Caffeine — sub-microsecond), L2 (distributed, Redis — sub-millisecond). Use Caffeine for hot data with bounded size and TTL. Use Redis when multiple service instances need shared cache state. Spring’s
@Cacheableabstracts both. - Connection pooling: HikariCP for JDBC, Apache HttpClient PoolingHttpClientConnectionManager for HTTP. Key metrics to monitor: active connections, idle connections, wait time. Connection leaks (not returning connections to pool) cause pool exhaustion — set
leakDetectionThresholdin HikariCP. - Microservices patterns in Spring:
- Service discovery: Spring Cloud + Eureka or Kubernetes DNS.
- Circuit breaker: Resilience4j (successor to Hystrix). Prevents cascade failures.
- API gateway: Spring Cloud Gateway for routing, rate limiting, authentication.
- Distributed tracing: Micrometer Tracing with Zipkin or Jaeger.
- Event-driven architecture: Spring + Kafka. Use
@KafkaListenerfor consumers,KafkaTemplatefor producers. Understand consumer groups, partition assignment, exactly-once semantics, and dead letter topics for failed message handling.
- “How would you size a
ThreadPoolExecutorfor an IO-bound vs CPU-bound workload?” - “Design a caching strategy for a read-heavy service with 100K requests/sec. What cache levels do you use?”
- “How does a circuit breaker work, and when would you use one?“
11. Strings and Immutability
Why is String immutable in Java?
Why is String immutable in Java?
String is immutable by design — once created, its character content cannot be changed. Any operation that appears to modify a String (like concat(), replace(), toUpperCase()) creates a new String object.What a strong candidate explains:- Security: Strings are used for class loading, network connections, database URLs, file paths, and cryptographic keys. If Strings were mutable, code that receives a validated file path could have that path changed after validation but before use (a TOCTOU vulnerability). Immutability eliminates this entire class of security bugs.
- Thread safety: Immutable objects are inherently thread-safe — no synchronization needed. Strings are shared across threads constantly (HTTP headers, configuration values, cache keys). If they were mutable, every String access would need synchronization.
- String Pool optimization: Because Strings are immutable, the JVM can safely deduplicate them via the String Pool (interning). Literal strings like
"hello"are stored once in the pool and shared by all references. This saves significant memory — in a typical application, 25-40% of heap is String objects. - Caching hashCode:
String.hashCode()is computed once and cached (lazy initialization). Since the content cannot change, the hash never changes. This makes Strings extremely efficient asHashMapkeys. Without immutability, the cached hash would become stale after mutation. StringBuildervsStringBuffer: For mutable string operations, useStringBuilder(not thread-safe, faster) orStringBuffer(thread-safe, slower). In practice, always useStringBuilder—StringBuffersynchronization is almost never needed and was a pre-Java-5 design decision.
- “What is the String Pool? Where does it live in memory?”
- “How does
String.hashCode()caching improve HashMap performance?” - “When would you use
StringBuildervsStringBuffer? IsStringBufferever actually needed?“
12. Java Design Patterns
Explain Singleton pattern and its pitfalls in Java
Explain Singleton pattern and its pitfalls in Java
- The naive approach fails:
public static Singleton instance;with a lazyif (instance == null)check is not thread-safe. Two threads can both seenulland create separate instances. - Double-checked locking (correct version):
volatile keyword is critical — without it, the JVM might reorder the write to instance before the constructor finishes, letting another thread see a partially-constructed object.- Enum singleton (Effective Java recommendation):
enum Singleton { INSTANCE; }— thread-safe, serialization-safe, reflection-safe (enum constructors cannot be called via reflection). This is the simplest correct implementation. - Spring’s approach: Spring beans are singletons by default (within the container scope). You rarely need to implement the Singleton pattern manually — Spring manages it. But
@Scope("singleton")in Spring is per-container, not per-JVM. Multiple Spring contexts means multiple “singletons.” - When Singleton is an anti-pattern: It introduces global state, makes unit testing harder (cannot substitute the instance), and hides dependencies. In modern Java with DI frameworks, manual singletons are almost always a code smell. Let the framework manage instance lifecycle.
- “Why is the
volatilekeyword required in double-checked locking? What happens without it?” - “How does Spring manage singleton scope, and how is it different from the GoF Singleton pattern?”
- “When is Singleton appropriate vs when is it a code smell?”
What is the Builder pattern and when should you use it?
What is the Builder pattern and when should you use it?
- The problem it solves: Telescoping constructors — when a class has many optional fields, you end up with constructors like
new User(name, null, null, email, null, true, null). This is error-prone and unreadable. - Implementation:
- Lombok
@Builder: Generates the entire Builder pattern with a single annotation. Used in 90%+ of Spring Boot projects. Saves hundreds of lines of boilerplate. Can be combined with@AllArgsConstructor(access = AccessLevel.PRIVATE)to enforce Builder-only construction. - When to use Builder: (1) Classes with more than 3-4 constructor parameters, (2) Classes with many optional fields, (3) Immutable objects with complex construction, (4) Creating test fixtures with readable setup code.
- Builder vs Constructor vs Factory:
- Constructor: Fine for 1-3 required parameters.
- Factory method: When you need to return different subtypes or cached instances.
- Builder: When construction is complex, has many optional steps, or readability of the construction site matters.
- “How does Lombok’s
@Builderwork under the hood?” - “How would you make a Builder that validates required fields at build time?“
13. Generics and Type System
Explain generics and type erasure in Java
Explain generics and type erasure in Java
- Type erasure: The compiler removes all generic type information after type checking.
List<String>andList<Integer>are both justListat runtime. This means: (1) You cannot donew T(), (2) You cannot doinstanceof List<String>, (3) You cannot create generic arraysnew T[]. This is Java’s backward compatibility trade-off — generics were added in Java 5 but had to interoperate with Java 1.4 bytecode. - Bounded type parameters:
<T extends Comparable<T>>— T must implement Comparable. Used in sorting algorithms.<T extends Number>— T must be Number or a subclass.- Multiple bounds:
<T extends Comparable<T> & Serializable>— T must satisfy both.
- Wildcards (PECS — Producer Extends, Consumer Super):
List<? extends Number>— read items as Number, but cannot add (except null). It is a “producer” of Numbers.List<? super Integer>— can add Integers, but read items only as Object. It is a “consumer” of Integers.List<?>— unbounded wildcard, read-only.
- PECS in practice:
Collections.copy(List<? super T> dest, List<? extends T> src)— the source produces T values, the destination consumes them. This guideline is from Effective Java and is the key to writing correct generic APIs. - Reified generics in other JVM languages: Kotlin has
reifiedtype parameters (viainlinefunctions) that retain type info at runtime. Scala hasClassTag/TypeTag. Java may get this eventually (Project Valhalla).
List<String>” — no mention of type erasure, wildcards, or PECS.Follow-up:- “Why can’t you write
new T()orinstanceof List<String>in Java?” - “Explain PECS with a real example. When would you use
? extendsvs? super?” - “How does type erasure affect serialization/deserialization of generic types (e.g., Jackson)?“
14. Modern Java Features (Java 11-21)
What new features in Java 11-21 should every developer know?
What new features in Java 11-21 should every developer know?
- Java 11 (LTS):
varfor local type inference,Stringnew methods (isBlank(),strip(),lines(),repeat()), HTTP Client API (java.net.http), single-file source execution (java FileName.java). - Java 14: Switch expressions (
->syntax, no fall-through),NullPointerExceptionwith helpful messages showing exactly which variable was null. - Java 16: Records (
record Point(int x, int y) {}— immutable data carriers with auto-generatedequals(),hashCode(),toString()), Pattern matching forinstanceof(if (obj instanceof String s)— no cast needed). - Java 17 (LTS): Sealed classes (
sealed class Shape permits Circle, Square— restrict which classes can extend). This enables exhaustive pattern matching in switch. - Java 21 (LTS): Virtual threads (lightweight threads, millions per JVM), pattern matching for switch, sequenced collections (
SequencedCollection,SequencedMapwithgetFirst(),getLast()), record patterns.
- Virtual threads are the biggest change since lambdas. They eliminate the need for reactive programming (WebFlux, RxJava) in most IO-bound applications. Instead of 200 platform threads handling 10,000 concurrent requests with async callbacks, you create 10,000 virtual threads — one per request — with simple blocking code. The JVM multiplexes them onto a small pool of platform threads.
- Records replace 80% of Lombok usage for data carrier classes. They enforce immutability, generate
equals()/hashCode()/toString(), and work well with pattern matching. - Sealed classes + pattern matching enable algebraic data types in Java — similar to Kotlin’s
sealed classor Rust’s enums. The compiler can verify exhaustiveness in switch expressions.
- “How do virtual threads work? What is the relationship to platform threads?”
- “When would you use a record vs a regular class?”
- “How do sealed classes enable exhaustive pattern matching?”
What are Java Records and when should you use them?
What are Java Records and when should you use them?
record User(String name, int age) {} auto-generates: a canonical constructor, final fields, accessor methods (name(), age()), equals(), hashCode(), and toString().What a strong candidate explains:- What records replace: Boilerplate data classes that are 50+ lines for 3 fields (fields, constructor, getters, equals, hashCode, toString). Records reduce this to one line. They also replace most Lombok
@Valueand@Datausage. - Restrictions by design: Records cannot extend other classes (they implicitly extend
Record). Fields are final — no setters. You cannot add instance fields outside the record header. These constraints are features: they guarantee immutability and value-based equality. - Custom behavior allowed: You can add custom constructors (compact constructors for validation), instance methods, static methods, and implement interfaces:
- Where records shine: DTOs, API response/request objects, value objects in domain-driven design, pattern matching, keys in Maps. Spring supports records for
@RequestBodydeserialization,@ConfigurationProperties, and more. - Where records do NOT fit: JPA entities (entities need mutable state, no-arg constructor, and are identity-based, not value-based), objects with complex mutable lifecycle, classes that need inheritance.
@Data” — they are closer to @Value (immutable), and this conflation shows misunderstanding of the immutability constraint.Follow-up:- “Can you use records as JPA entities? Why or why not?”
- “How do records interact with pattern matching in Java 21?”
- “What is a compact constructor, and when would you use one?”
Conclusion & Interview Tips
Key Focus Areas
- OOP concepts and Java 8+ features (lambdas, streams, Optional)
- Multithreading, collections internals (especially HashMap), and memory management
- Spring Boot fundamentals, dependency injection, and the
@Transactionalproxy trap - Clean code, exception handling, and immutability patterns
- Modern Java (records, virtual threads, sealed classes, pattern matching)
During the Interview
- Start with a crisp one-liner answer, then go deeper layer by layer
- Discuss time and space complexity with specific Big-O for collection operations
- Always mention trade-offs — “it depends” is fine IF you explain what it depends on
- Use real examples: “In a service handling 10K requests/sec, I would…”
- Show debugging instincts: “The first thing I would check is…”
- Know your tools: mention profilers (async-profiler, JFR), monitoring (Micrometer, Actuator), and build tools (Maven, Gradle)
Red Flags Interviewers Watch For
- Textbook definitions with zero real-world context
- “Use synchronized everywhere” without knowing alternatives
- Not knowing HashMap internals at a senior level
- Field injection preference over constructor injection
- No awareness of Java features after Java 8
- Unable to explain WHY, only WHAT