A complete set of 100 Java interview questions and detailed answers for Fresher, Mid-Level, and Senior developers β covering Java basics, OOP, Collections, Exception Handling, Multithreading, Java 8+ features, JVM internals, Design Patterns, Modern Java (17/21), and Spring Boot.
π
Last Updated
March 2026
β
Questions
100 Q&A
π―
Level
Fresher + Mid + Senior
How to Use This Guide
This guide contains 100 Java interview questions organized by level and topic. Each question includes a detailed, professional answer that covers not just the what, but also the why β exactly what interviewers expect from strong candidates.
Level
Questions
Experience
Topics Covered
π’ Fresher
Q1βQ25
0β1 year
Basics, Data Types, OOP Fundamentals, Strings, Arrays
Multithreading, JVM, GC, Design Patterns, Modern Java, Spring
Pro tip: Read each answer fully even if you think you know it β senior interviewers follow up on surface answers with deeper questions. Understanding the why behind every concept separates average candidates from top performers.
These are the most commonly asked questions in campus placements and entry-level Java interviews. Every fresher must know all 15 of these cold.
Java is a high-level, class-based, object-oriented programming language developed by James Gosling at Sun Microsystems in 1995. It is platform-independent because Java source code (.java) is compiled into bytecode (.class) by javac β not into native machine code. This bytecode runs on any operating system that has a JVM installed, enabling the famous 'Write Once, Run Anywhere' (WORA) principle. The JVM acts as an abstraction layer between bytecode and the underlying OS/hardware.
JVM (Java Virtual Machine): Executes bytecode. Handles memory management via Garbage Collection. Provides platform independence. JRE (Java Runtime Environment) = JVM + Java Class Libraries (java.lang, java.util, java.io, etc.) β minimum needed to RUN Java programs. JDK (Java Development Kit) = JRE + compiler (javac) + developer tools (javadoc, jdb, jar, jshell) β required to WRITE, COMPILE, and RUN Java. Simple rule: JDK β JRE β JVM. Developers install JDK. End-users install JRE. Current recommended version: Java 21 LTS (free from OpenJDK).
Java has exactly 8 primitive types: byte (8-bit, -128 to 127), short (16-bit), int (32-bit, most common β ~Β±2.1 billion), long (64-bit, use L suffix: 100L), float (32-bit decimal, use F suffix: 3.14F), double (64-bit decimal, default for decimals), char (16-bit Unicode character, e.g., 'A'), boolean (true or false). Key fact: these sizes are FIXED across all platforms β int is always 32 bits in Java unlike C/C++. Each has a wrapper class (Integer, Long, Float, Double, Character, Boolean, Byte, Short) for use with Collections and Generics.
== is the reference equality operator β compares if two variables point to the SAME object in memory. For primitives, == compares values directly. .equals() is a method β compares the LOGICAL CONTENT of objects. By default (Object's implementation), equals() behaves like ==, but classes like String, Integer, List override it to compare content. Example: String s1 = new String('Java'); String s2 = new String('Java'); s1 == s2 β false (different heap objects); s1.equals(s2) β true (same content). Rule: always use .equals() for String comparison. Always override hashCode() when overriding equals().
The String Pool is a special cache area inside the Java Heap. When you write String s = 'Hello', the JVM checks the pool first. If 'Hello' already exists, it returns the same reference β no new object created. This memory optimization works because Strings are immutable. Example: String s1 = 'Java'; String s2 = 'Java'; s1 == s2 β true (same pool object). But: String s3 = new String('Java') β creates a new heap object bypassing the pool, so s1 == s3 β false. String.intern() forces a string to be added to or looked up from the pool. In Java 7+, the String Pool moved from PermGen to the main heap β allowing GC when no references remain.
String immutability is achieved through: (1) String class declared final β cannot be subclassed. (2) Internal char array (value[]) is private and final β cannot be accessed or changed after construction. (3) No mutating/setter methods in the String API. Benefits: Thread-safe by nature β can be shared across threads without synchronization. String Pool optimization β safe to share references since content never changes. HashMap key safety β hash remains stable. Security β file paths and class names passed as Strings cannot be altered maliciously. Performance β hash is cached in a private field after first call.
String: Immutable. Every modification creates a new object β inefficient in loops. Thread-safe by nature. StringBuilder (Java 5+): Mutable β modifies same internal char array. NOT synchronized. Faster than StringBuffer. Use in single-threaded string building. StringBuffer: Mutable AND synchronized (thread-safe). Slower due to lock overhead. Available since Java 1.0. When to use: Constant or rarely modified text β String. Building strings in loops (single thread) β StringBuilder. Building strings across multiple threads β StringBuffer. Performance rule: String concatenation in a loop is O(nΒ²). StringBuilder.append() is O(n) amortized.
Autoboxing is the automatic conversion from a primitive to its wrapper class. Unboxing is the reverse. Introduced in Java 5. Examples: Integer i = 5 (int autoboxed to Integer). int x = i (Integer unboxed to int). This enables primitives in Collections: List<Integer> list = new ArrayList<>(); list.add(42) β 42 is autoboxed. Performance note: boxing creates heap objects and adds GC pressure. Integer caches values -128 to 127 (Integer.valueOf() reuses cached instances) β so Integer a = 127; Integer b = 127; a == b β true, but Integer a = 128; Integer b = 128; a == b β false. Classic interview trap.
static means 'belongs to the class, not to any instance.' Uses: static variable (class variable): one copy shared by all objects β useful for counters, constants. static method: callable without an object (ClassName.method()). Cannot access instance fields/methods directly. main() is static so JVM calls it without creating an object. static block: executes once when the class is first loaded β used for complex static initialization. static nested class: doesn't need an outer class instance. static import: use static members without class prefix β import static java.lang.Math.*. Key restriction: static methods cannot be overridden (they are hidden, not overridden).
A constructor is a special block for initializing a new object. It has the same name as the class and NO return type (not even void). Called automatically by new. Differences from methods: Name must match class name; methods can have any name. No return type; methods must declare one. Called by JVM via new; methods called explicitly. Purpose: initialization; methods: behavior. Constructors are NOT inherited; methods are. Types: default (no args β auto-generated if none defined), parameterized (takes args), copy constructor (takes same-type object). Constructor chaining: this() calls another constructor in same class; super() calls parent constructor β both must be the FIRST statement.
Array: Fixed size at creation β cannot grow or shrink. Can hold primitives and objects. No built-in add/remove methods. Access: arr[i]. Length: arr.length (no parentheses). ArrayList: Dynamic size β grows/shrinks automatically. Holds only objects (primitives autoboxed). Rich API β add(), remove(), contains(), sort(), size(). Backed by a dynamic array internally. Implements List interface. Access: list.get(i). Size: list.size(). When to use arrays: fixed-size data, primitive performance-critical code, multidimensional data. When to use ArrayList: when size is unknown upfront, need List methods, work with Collections framework.
final (keyword): Three uses β final class cannot be extended (e.g., String), final method cannot be overridden, final variable cannot be reassigned after initialization. finally (block): Part of try-catch β always executes after try/catch for cleanup. Only skipped when System.exit() is called or JVM crashes. finalize() (method): Defined in Object β called by GC before collecting the object. Deprecated in Java 9, removed in Java 18. Unreliable β no guarantee when or if called. Use Cleaner or try-with-resources instead. Memory trick: final = keyword for immutability/restriction. finally = block for cleanup. finalize = legacy GC hook.
Java has four access modifiers controlling visibility: private: accessible only within the same class β most restrictive. default (no keyword): accessible within the same package only β package-private. protected: accessible within same class, same package, AND subclasses (even in other packages). public: accessible from anywhere β least restrictive. Visibility order (least to most restrictive): public > protected > default > private. Applied to: classes (only public or default), methods, fields, constructors. Best practice: always use the most restrictive access level that still meets requirements β typically private fields with public getters/setters (encapsulation).
Type casting converts a value from one type to another. Widening (implicit/automatic): converts smaller type to larger β no data loss. byte β short β int β long β float β double. Example: int i = 100; double d = i; β automatic. Narrowing (explicit): converts larger type to smaller β possible data loss β requires explicit cast. Example: double d = 9.99; int i = (int) d; β result: 9 (fractional part lost). Object casting: Upcasting (child to parent) β implicit and safe: Animal a = new Dog(); Downcasting (parent to child) β explicit, may throw ClassCastException: Dog d = (Dog) a; β use instanceof check before downcasting: if (a instanceof Dog) { Dog d = (Dog) a; }. Java 16+: if (a instanceof Dog d) β pattern matching avoids explicit cast.
break: Exits the current loop or switch statement entirely. Execution continues at the statement after the loop/switch. continue: Skips the rest of the current iteration and jumps to the next iteration of the loop. return: Exits the current method entirely β optionally returning a value. Labeled break/continue: Java supports labels for breaking out of nested loops: outerLoop: for(...) { for(...) { break outerLoop; } } β breaks out of both loops. Interview trap: break exits only the INNERMOST loop by default. To exit outer loops, use labeled break. return in finally overrides try/catch return β very bad practice.
Fresher Level β OOP Concepts (Q16βQ25)
OOP is the most heavily tested topic in Java fresher interviews. These 10 questions cover the four pillars, inheritance rules, and interface vs abstract class β master every one.
Encapsulation: Bundling data (fields) and methods together in a class, and restricting direct access to internal state using access modifiers (private fields + public getters/setters). Goal: data hiding and controlled access. Inheritance: A child class acquires fields and methods of a parent class using extends. Enables code reuse and IS-A relationships. Java supports single class inheritance but multiple interface implementation. Polymorphism: 'Many forms.' Compile-time: Method Overloading (same name, different parameters β same class). Runtime: Method Overriding (child overrides parent method; JVM resolves at runtime based on actual object type). Abstraction: Hiding implementation complexity, exposing only the essential interface. Achieved via abstract classes (partial) and interfaces (full). Focuses on WHAT, not HOW.
Method Overloading (Compile-time polymorphism): Same method name, DIFFERENT parameter list (number, type, or order of parameters) within the SAME class. Resolved at compile time. Return type alone cannot distinguish overloads. Example: int add(int a, int b) and double add(double a, double b, double c). Method Overriding (Runtime polymorphism): Subclass provides its own implementation of a method from the superclass. SAME name, SAME parameter list, same or covariant return type. Resolved at RUNTIME by JVM based on actual object type. Annotate with @Override for safety. Rules: cannot reduce access, cannot throw broader checked exceptions, static/private/final methods cannot be overridden. Key: overloading = same class, different signature; overriding = parent-child, same signature.
An abstract class is declared with the abstract keyword and cannot be instantiated directly. It can have: abstract methods (no body β subclasses MUST implement), concrete methods (with body β optionally overridden), constructors, instance fields, and static members. When to use: When closely related classes need to share common code/state (IS-A relationship). When subclasses have common fields or partial implementations. When you want protected/package-private abstract methods (interfaces only allow public). Abstract class vs interface: abstract class = one parent only + can have state; interface = multiple implementation + no instance state. Example: abstract class Shape { abstract double area(); void display() { System.out.println(area()); } } β Circle and Rectangle extend Shape.
An interface defines a contract β a set of method signatures that implementing classes must fulfill. Before Java 8: only abstract methods and public static final constants. Java 8 additions: default methods (concrete methods with default keyword β implementing classes inherit but can override), static methods (utility methods on the interface). This allowed adding new methods to existing interfaces without breaking all implementations β critical for evolving the Collections API. Java 9: private methods and private static methods (for code reuse within default methods). A class can implement MULTIPLE interfaces β enabling multiple inheritance of behavior. Key rule: if a class implements two interfaces with conflicting default methods, it MUST override the method β use InterfaceName.super.method() to call a specific interface's default.
Runtime polymorphism (dynamic dispatch) is when the JVM decides WHICH overridden method to call at runtime based on the actual object type, not the reference type. Example: class Animal { void sound() { System.out.println('Animal'); } } class Dog extends Animal { void sound() { System.out.println('Woof'); } } Animal a = new Dog(); a.sound(); β prints 'Woof'. Even though the reference is Animal type, the actual object is Dog, so Dog's sound() executes. This is the core power of polymorphism β you can write code against the Animal type and it works correctly for all subclasses. Used everywhere: List<Shape> shapes β calling area() on each shape calls the correct subclass implementation.
Abstract class: Can have constructors, instance fields (state), any access modifiers (private, protected, public). Methods can be abstract or concrete. A class can extend only ONE abstract class. Use when classes share code and state (IS-A with common implementation). Interface: No constructors, no instance fields (only public static final constants). Java 8+: default/static methods allowed. Java 9+: private methods allowed. A class can implement MULTIPLE interfaces. Use to define capabilities/contracts and enable multiple inheritance of behavior. Key decision rule: Need shared implementation/state with IS-A β abstract class. Need multiple inheritance of type or behavior, or defining a capability contract β interface. Real examples: AbstractList (abstract class), List (interface), Comparable (interface), Number (abstract class).
Static methods CANNOT be overridden β they are HIDDEN (method hiding). Static methods belong to the class, not instances. If a subclass defines a static method with same signature as parent's static method, it hides (not overrides) it. The method called depends on reference type (compile-time), not actual object type (runtime). So there is no polymorphism for static methods. Private methods CANNOT be overridden β private is not visible to subclasses at all. If a subclass defines a method with the same signature as parent's private method, it creates an entirely new, independent method in the subclass β not an override. @Override on such a method causes a compile error.
Encapsulation is the OOP principle of bundling data (fields) and the methods that operate on that data within a single unit (class), and restricting direct external access to the internal state. Implementation in Java: Declare all fields as private β prevents external direct access. Provide public getter methods to read field values. Provide public setter methods to modify field values (with validation). Example: class BankAccount { private double balance; public double getBalance() { return balance; } public void deposit(double amount) { if(amount > 0) balance += amount; } } Benefits: Data validation in setters. Ability to change internal implementation without breaking client code. Prevents invalid states. Controlled access (read-only fields with only getters). Part of JavaBean convention.
Constructor chaining is calling one constructor from another to avoid code duplication. this(): Calls another constructor in the SAME class. Must be the FIRST statement in the constructor body. Example: class Point { int x, y; Point() { this(0, 0); } Point(int x, int y) { this.x = x; this.y = y; } } β default constructor delegates to parameterized one. super(): Calls the PARENT class constructor. Also must be the FIRST statement. If you don't explicitly write super(), Java automatically inserts super() (no-arg) as the first line. This means parent constructor always executes before child constructor body. Cannot use both this() and super() in same constructor β both must be first. this (without parentheses): refers to current instance. super (without parentheses): refers to parent class member.
IS-A relationship (Inheritance): Implemented using extends (class) or implements (interface). Represents a type hierarchy β a Dog IS-A Animal. Use when: the child class genuinely IS a specialized version of the parent. Enables polymorphism. Example: class Dog extends Animal. HAS-A relationship (Composition/Aggregation): One class contains a reference to another class as a field. Represents ownership or association. Example: class Car { private Engine engine; } β a Car HAS-A Engine. Prefer HAS-A over IS-A when: No genuine specialization relationship exists. You need to use multiple unrelated behaviors. The contained class may change independently. Gang of Four principle: 'Favor composition over inheritance.' Composition provides looser coupling, better encapsulation, and easier testing (can inject mock dependencies).
Mid Level β Collections Framework (Q26βQ38)
Collections questions are asked in virtually every Java interview at mid-level. Interviewers expect you to know not just the API but the internal implementation and performance characteristics.
The Java Collections Framework (JCF) is a unified architecture in java.util for storing and manipulating groups of objects. It provides interfaces, implementation classes, and algorithms. Core interfaces: Collection (root β defines add, remove, size, iterator), List (ordered, duplicates allowed β ArrayList, LinkedList, Vector), Set (no duplicates β HashSet, LinkedHashSet, TreeSet), Queue (FIFO processing β PriorityQueue, ArrayDeque), Map (key-value pairs, NOT a Collection β HashMap, TreeMap, LinkedHashMap). Important: Map does NOT extend Collection β it is a separate hierarchy. You can get Collection views of a Map: map.keySet(), map.values(), map.entrySet(). All in java.util. Collections (plural) is a utility class with static helper methods: sort(), shuffle(), unmodifiableList().
ArrayList: Backed by a dynamic resizable array. Random access (get by index): O(1). Insertion/deletion in middle: O(n) β elements must shift. Excellent cache performance (contiguous memory). No extra memory per element. LinkedList: Backed by a doubly-linked list of Node objects. Random access: O(n) β must traverse from head/tail. Insertion/deletion at ends or with iterator: O(1). Each node has 24 bytes extra overhead (object header + prev + next pointers). Also implements Deque β usable as Stack or Queue. Default choice: ArrayList for almost everything. Switch to LinkedList only when you need Deque behavior or have extremely frequent insertions at list ends. Even for many insertions, ArrayList is often faster due to better CPU cache locality.
HashMap uses an array of buckets (Node[] table, default size 16). put(key, value): Calls key.hashCode(), applies a secondary hash function to reduce clustering, computes bucket index as (n-1) & hash. If bucket is empty, stores Node directly. If collision (multiple keys same bucket), appends to chain using equals() to find duplicates. Java 8 improvement: when a bucket's linked list exceeds 8 nodes, it converts to a Red-Black Tree β worst case improves from O(n) to O(log n). get(key): Same hash β bucket β equals() chain. Resizing: when entries exceed capacity Γ loadFactor (default 16 Γ 0.75 = 12), table doubles and all entries rehash β O(n) operation. Null keys: ONE null key allowed (stored at bucket 0 with special handling). Thread safety: NOT thread-safe β use ConcurrentHashMap for concurrent access.
HashMap: No guaranteed iteration order. O(1) average get/put/remove. Allows one null key. Best for fast key-value lookup when order doesn't matter. LinkedHashMap: Maintains INSERTION order (or access order with accessOrder=true constructor param). O(1) average operations. Allows one null key. Best for: preserving put order, LRU Cache implementation (accessOrder=true + override removeEldestEntry()). TreeMap: Always sorted by key (natural Comparable order or custom Comparator). O(log n) for all operations (Red-Black Tree). NO null keys β throws NullPointerException. Implements NavigableMap: firstKey(), lastKey(), headMap(), tailMap(), floorKey(), ceilingKey() for range queries. Best for: sorted key iteration, finding nearest keys, range-based access.
HashSet: Backed by HashMap internally. O(1) average add/remove/contains. No guaranteed order. Allows ONE null element. Best for: fast deduplication when order doesn't matter. LinkedHashSet: Extends HashSet, backed by LinkedHashMap. O(1) average operations. Preserves INSERTION order. Allows ONE null. Best for: deduplication while maintaining input order. TreeSet: Backed by TreeMap (Red-Black Tree). O(log n) operations. Always SORTED (natural or Comparator order). NO null elements β compareTo(null) throws NullPointerException. Implements NavigableSet: ceiling(), floor(), higher(), lower(), headSet(), tailSet(), subSet(), descendingSet(). Best for: sorted unique elements, range queries, auto-complete prefix search. Java 21: TreeSet and LinkedHashSet implement SequencedSet β getFirst(), getLast(), reversed() available.
Fail-fast iterators: Used by ArrayList, HashMap, HashSet, etc. Maintain a modCount counter. If collection is structurally modified (add/remove) during iteration (by anything other than the iterator itself), immediately throws ConcurrentModificationException on next iterator.next() call. Purpose: loudly signal concurrent modification bugs early. Fix: use Iterator.remove() for safe removal, or List.removeIf(), or copy then removeAll(). Fail-safe iterators: Used by CopyOnWriteArrayList, ConcurrentHashMap, CopyOnWriteArraySet. Operate on a snapshot/copy of the collection's data. Structural modifications during iteration do NOT throw exceptions. Drawback: may see stale data; CopyOnWriteArrayList creates a full copy on every write β expensive for write-heavy workloads. ConcurrentHashMap is weakly consistent β reflects state at some point during iteration.
Comparable (java.lang): Defines the NATURAL ordering of a class. Implemented within the class itself via compareTo(T other). Returns negative (this < other), zero (equal), positive (this > other). Used by Collections.sort(), Arrays.sort(), TreeSet/TreeMap when no Comparator provided. One natural ordering per class. Example: class Student implements Comparable<Student> { public int compareTo(Student o) { return Integer.compare(this.rollNo, o.rollNo); } } Comparator (java.util): Defines EXTERNAL, CUSTOM ordering. Separate from the class β doesn't require modifying the class. Java 8 factory methods: Comparator.comparing(), comparingInt(), thenComparing(), reversed(). Multiple orderings possible for same class. Use Comparable for ONE obvious natural order; use Comparator for multiple orderings or when you can't modify the class.
Wrong approach: for (String s : list) { if (s.equals('X')) list.remove(s); } β throws ConcurrentModificationException because list is structurally modified during for-each iteration (which uses an iterator internally that tracks modCount). Correct approaches: Iterator.remove(): Iterator<String> it = list.iterator(); while(it.hasNext()) { if(it.next().equals('X')) it.remove(); } β safe because the iterator's own remove() updates modCount consistently. Java 8 removeIf(): list.removeIf(s -> s.equals('X')); β cleanest, most readable approach. Collect then removeAll(): Collect elements to remove in a separate list, then call list.removeAll(toRemove) after iteration. CopyOnWriteArrayList: iterating over a snapshot β safe but expensive. Best choice: removeIf() for simple conditions, Iterator.remove() for complex logic during traversal.
Default initial capacity: 16 (always a power of 2 β enables efficient bitwise & for bucket index computation). Default load factor: 0.75. When entries exceed capacity Γ loadFactor (16 Γ 0.75 = 12 entries), HashMap resizes (doubles capacity and rehashes all entries β O(n) operation). Lower load factor β fewer collisions, more memory wasted. Higher load factor β more collisions, better memory use. 0.75 is the empirically optimized balance. Custom: new HashMap<>(32, 0.9f) β initial capacity 32, load factor 0.9. Pre-sizing: if you know you'll store N entries, initialize with capacity = N/0.75 + 1 to avoid resizing: new HashMap<>((int)(N/0.75) + 1). This is an important optimization for large maps.
Both retrieve and remove the head of the queue, but they differ in behavior when the queue is EMPTY: remove(): Throws NoSuchElementException if the queue is empty. Use when an empty queue represents an unexpected/error condition. poll(): Returns null if the queue is empty. Use when an empty queue is a valid, expected condition. Similarly for retrieval without removal: element() vs peek(): element() throws NoSuchElementException if empty. peek() returns null if empty. For adding: add() throws IllegalStateException if capacity-constrained queue is full. offer() returns false if full (safer). General rule: prefer poll() and peek() for safer, exception-free queue operations in normal application logic. remove() and element() for cases where emptiness is a programming error.
java.util.Collections is a final class with all static utility methods for operating on Collection objects. Key methods: Collections.sort(list) β sorts in natural order (uses TimSort for object arrays β stable O(n log n)). Collections.sort(list, comparator) β custom sort. Collections.reverse(list) β reverses order. Collections.shuffle(list) β randomizes order. Collections.min(collection) / Collections.max(collection) β find extremes. Collections.frequency(collection, element) β count occurrences. Collections.unmodifiableList(list) β returns read-only view (mutations throw UnsupportedOperationException). Collections.synchronizedList(list) β thread-safe wrapper (must manually sync during iteration). Collections.nCopies(n, element) β list of n copies. Collections.disjoint(c1, c2) β true if no common elements. Collections.swap(list, i, j) β swap elements. Note: List.of() (Java 9+) is preferred over unmodifiableList for truly immutable lists.
ConcurrentHashMap (java.util.concurrent): Java 8+ implementation uses bucket-level locking β each bucket has its own lock (synchronized on the bin head node). For empty buckets, uses CAS (Compare-And-Swap) for lock-free insertion. Read operations (get()) are completely lock-free β use volatile reads. This means N threads can concurrently write to N different buckets simultaneously β high throughput. Does NOT allow null keys or null values. Size tracking uses LongAdder-like mechanism. Synchronized HashMap (Collections.synchronizedMap(new HashMap<>())): Wraps the entire map with a single mutex (synchronized on the map object). ALL operations β even reads β acquire the same lock. Only one thread at a time for all operations β very low concurrency. Requires manual synchronization during iteration. Performance: ConcurrentHashMap is dramatically faster under concurrent load. Use ConcurrentHashMap for all concurrent scenarios. synchronized HashMap is essentially obsolete.
List.of(e1, e2, e3) (Java 9+): Fully IMMUTABLE β add/remove/set throws UnsupportedOperationException. Does NOT allow null elements (throws NullPointerException). Size is fixed. Preferred for constant lists. Arrays.asList(array): Returns a FIXED-SIZE list backed by the array β set() is allowed (modifies underlying array), but add()/remove() throws UnsupportedOperationException. Allows null. Changes to the list reflect in the original array and vice versa. new ArrayList<>(collection): Fully MUTABLE β add, remove, set all work. Allows null. Creates an independent copy β no link to original array. Use: constant list β List.of(). Need to wrap array as list with index modification β Arrays.asList(). Need fully mutable list from existing data β new ArrayList<>(List.of(...)) or new ArrayList<>(Arrays.asList(...)).
Mid Level β Exception Handling (Q39βQ48)
Exception handling questions test your understanding of Java's compile-time safety, runtime behavior, and resource management. These come up in virtually every technical round.
Throwable is the root of all Java exceptions and errors. It has two direct subclasses: Error: JVM-level unrecoverable failures β should NOT be caught. Examples: OutOfMemoryError (heap full), StackOverflowError (deep recursion), VirtualMachineError. Exception: Application-level problems that can be handled. Two categories: Checked Exceptions (subclasses of Exception excluding RuntimeException): Compiler FORCES handling via try-catch or throws declaration. Represent recoverable external conditions. Examples: IOException, SQLException, ParseException, ClassNotFoundException. Unchecked Exceptions (subclasses of RuntimeException): Compiler does NOT enforce handling. Represent programming bugs/errors. Examples: NullPointerException, ArrayIndexOutOfBoundsException, ClassCastException, IllegalArgumentException, NumberFormatException.
throw: A statement used INSIDE a method body to explicitly throw an exception object. Immediately terminates the current method execution. Example: throw new IllegalArgumentException('Age must be positive'); throws: A keyword used in a METHOD SIGNATURE to declare that the method may propagate one or more checked exceptions to its caller. Does NOT handle the exception β only declares it. Example: public void readFile(String path) throws IOException, FileNotFoundException. Key distinction: throw is an ACTION (triggers an exception). throws is a DECLARATION (announces potential exceptions to callers). A method can have throws without ever executing throw (if the risky code path is not always reached). throw always results in exactly one exception object; throws can list multiple comma-separated exception types.
The finally block executes ALWAYS after try and catch, regardless of whether an exception was thrown, caught, or not. Primary use: resource cleanup β closing files, DB connections, releasing locks. finally executes even when: no exception occurs (normal flow), exception is thrown and caught, exception is thrown and NOT caught, a return statement executes inside try or catch (finally runs before the method actually returns). Cases where finally does NOT execute: System.exit() is called β JVM terminates immediately. JVM process crashes (kill -9, power failure, fatal Error). Infinite loop or deadlock in try block (thread never reaches finally). Critical gotcha: if finally contains a return statement, it OVERRIDES the return value from try/catch β very bad practice. If finally throws an exception, it suppresses the original try exception.
Try-with-resources (Java 7, JEP 334) automatically closes resources that implement AutoCloseable after the try block exits β whether normally or via exception. Syntax: try (FileReader fr = new FileReader(path); BufferedReader br = new BufferedReader(fr)) { ... } β fr and br are closed automatically. Multiple resources: closed in REVERSE order of declaration (br first, then fr). Suppressed exceptions: if both the try body and close() throw exceptions, the close() exceptions are added as suppressed: mainException.getSuppressed(). The original exception is preserved. Java 9 improvement: effectively-final variables can be used directly: Connection c = getConnection(); try (c) { ... }. Advantages over finally: No null-check needed before close(). Suppressed exceptions preserved (unlike finally which discards try exception if finally also throws). Cleaner, less error-prone code. All standard Java I/O streams and JDBC resources implement AutoCloseable.
Multi-catch (Java 7) allows a single catch block to handle multiple exception types using the pipe | operator: catch (IOException | SQLException | ParseException e) { handleError(e); }. Reduces code duplication when different exceptions share identical handling logic. Limitation β implicit final: In a multi-catch block, the exception variable e is implicitly FINAL β you cannot reassign e inside the catch block. This prevents type narrowing of the caught exception. Limitation β cannot catch related exceptions: You cannot catch a parent and child exception in the same multi-catch β catch(Exception | IOException e) is a compile error because IOException IS-A Exception. The types must be disjoint (no supertype-subtype relationship). Multi-catch vs separate catch blocks: separate catch blocks allow different handling per exception type. Multi-catch is for when handling is identical and you want to avoid repetition.
Exception chaining is catching a low-level exception and rethrowing a higher-level, domain-specific exception while preserving the original as the 'cause.' Why: Low-level exceptions (SQLException, IOException) leak implementation details. Business layers should throw meaningful domain exceptions (OrderFailedException, UserNotFoundException). But the original root cause must be preserved for debugging. How: All Throwable constructors accept a cause: throw new ServiceException('Order failed', originalSQLException). Retrieving cause: exception.getCause() returns the wrapped exception. getCause() chains through all levels. printStackTrace() prints the entire chain β 'Caused by: ...' for each wrapped exception. Best practice: ALWAYS pass original exception as cause when wrapping. Never do throw new MyException(e.getMessage()) β this LOSES the stack trace. Spring's DataAccessException hierarchy demonstrates this well.
Execution order rules: Normal execution (no exception): try body executes β finally executes. catch is skipped. Exception caught: try body up to exception β matching catch executes β finally executes. Exception not caught: try body up to exception β finally executes β exception propagates up call stack. return in try: try return expression evaluated β finally executes β method returns (finally can modify return value if it contains its own return statement β bad practice). Multiple catch blocks: only the FIRST matching catch executes. Order matters β more specific exceptions must come before broader ones (IOException before Exception). Dead code: placing a broader exception catch before a narrower one (Exception before IOException) causes a compile error β 'exception has already been caught.' Java 7+: Exceptions in finally are not automatically added as suppressed β they replace the original exception from try. Try-with-resources handles this correctly with addSuppressed().
CAN you catch an Error: Yes, technically β Error extends Throwable, so catch(Error e) or catch(Throwable t) compiles. SHOULD you: Almost never. Errors represent JVM-level catastrophic failures (OutOfMemoryError, StackOverflowError, VirtualMachineError) that the application typically cannot recover from. Catching them may hide serious JVM problems, lead to undefined behavior, or prevent proper JVM shutdown. Rare legitimate cases: OutOfMemoryError: some frameworks catch this to perform emergency cleanup (log final state, gracefully stop accepting new requests) before actually dying β but this is expert-level and must be extremely careful. AssertionError: can be caught in unit tests. LinkageError: plugin architectures catching class loading failures for one plugin without crashing others. Best practice: let Errors propagate. Set an UncaughtExceptionHandler on threads for logging. Use JVM flags like -XX:+HeapDumpOnOutOfMemoryError for post-mortem analysis.
Checked exceptions (extends Exception, not RuntimeException): Compiler forces handling/declaration. Represent recoverable external conditions where the caller can take meaningful action. Examples: IOException (handle missing file), SQLException (retry with fallback DB). Unchecked exceptions (extends RuntimeException): Compiler doesn't enforce. Represent programming bugs β NullPointerException means null wasn't checked; ArrayIndexOutOfBoundsException means index wasn't validated. Modern framework position: Spring translates ALL checked database/infrastructure exceptions to unchecked DataAccessException hierarchy. Hibernate throws unchecked exceptions. Effective Java (Josh Bloch): Use checked for conditions callers CAN recover from; unchecked for programming errors. Modern trend: Most enterprise Java today uses unchecked exceptions exclusively, with global @ExceptionHandler in Spring to catch and translate at the API boundary. Kotlin eliminated checked exceptions entirely. Reason: checked exceptions don't work with lambdas/streams (functional interfaces can't declare checked exceptions).
Creating custom checked exception: class InsufficientFundsException extends Exception { private double amount; public InsufficientFundsException(double amount) { super('Insufficient funds: required ' + amount); this.amount = amount; } public InsufficientFundsException(double amount, Throwable cause) { super('Insufficient funds: required ' + amount, cause); this.amount = amount; } public double getAmount() { return amount; } } Creating custom unchecked exception: class UserNotFoundException extends RuntimeException { private Long userId; public UserNotFoundException(Long userId) { super('User not found: id=' + userId); this.userId = userId; } } Best practices: Always provide: (1) String message constructor, (2) Throwable cause constructor, (3) both message+cause constructor. Add domain-specific fields (like amount, userId) for richer error handling. Choose checked for recoverable conditions, unchecked for programming errors. Name exceptions clearly ending in 'Exception' (checked) or meaningful names. Never extend Error or Throwable directly.
Mid Level β Java 8+ Features (Q49βQ58)
Java 8 was the most transformative release in Java's history. Lambda expressions, Streams, and Optional are asked in almost every Java interview above fresher level.
A lambda expression is an anonymous function β a concise way to represent a functional interface instance without a full anonymous class. Syntax: (parameters) -> expression or (parameters) -> { statements; }. Examples: Runnable r = () -> System.out.println('Hello'); Comparator<String> c = (s1, s2) -> s1.length() - s2.length(); Function<Integer, Integer> square = n -> n * n. Functional Interface: An interface with EXACTLY ONE abstract method (SAM β Single Abstract Method). @FunctionalInterface annotation enforces this at compile time (adding a second abstract method causes compile error). Built-in functional interfaces (java.util.function): Predicate<T>: Tβboolean (filtering). Function<T,R>: TβR (transformation). Consumer<T>: Tβvoid (side effects). Supplier<T>: ()βT (lazy provision). BiFunction<T,U,R>: (T,U)βR. UnaryOperator<T>: TβT. BinaryOperator<T>: (T,T)βT. Method references shorthand: Class::staticMethod, instance::method, Class::instanceMethod, Class::new.
The Stream API (java.util.stream, Java 8) enables functional-style, declarative data processing. Streams are not data structures β they compute on demand via lazy evaluation. Creating streams: collection.stream(), Arrays.stream(arr), Stream.of(a,b,c), IntStream.range(0,10). Intermediate operations (LAZY β return a Stream, execute only when terminal is triggered): filter(Predicate), map(Function), flatMap(Function<T,Stream<R>>), sorted(), distinct(), limit(n), skip(n), peek(Consumer). Terminal operations (EAGER β trigger execution, produce final result): forEach(Consumer), collect(Collector), reduce(), count(), sum(), min(), max(), findFirst(), findAny(), anyMatch(), allMatch(), noneMatch(), toList() (Java 16). A stream without a terminal operation does nothing. Parallel streams: stream().parallel() splits work across ForkJoinPool.commonPool(). Use for CPU-bound, stateless operations on large datasets β NOT for I/O or when order matters.
map(Function<T,R>): Applies a function to each element, producing EXACTLY ONE output per input. Stream element count unchanged. If function returns a Stream, you get a Stream<Stream<R>> β nested. Example: Stream.of('hello','world').map(String::toUpperCase) β ['HELLO','WORLD']. flatMap(Function<T,Stream<R>>): Applies a function that returns a Stream per element, then FLATTENS all streams into one. Zero, one, or many outputs per input. Example: Stream.of('hello world','java').flatMap(s -> Arrays.stream(s.split(' '))) β ['hello','world','java']. Flattening nested collections: List<List<Integer>> nested; nested.stream().flatMap(Collection::stream).collect(toList()) β flat list. Optional.flatMap: avoids Optional<Optional<T>> β use when mapping function returns Optional. Rule: function returns plain value β map(). Function returns Stream/collection β flatMap(). Analogy: map = one-to-one. flatMap = one-to-many then flatten.
Optional<T> (java.util.Optional, Java 8) is a container that may or may not hold a non-null value. Forces explicit null handling, eliminating silent NullPointerException. Creating: Optional.of(value) β throws NPE if null. Optional.ofNullable(value) β empty if null. Optional.empty() β explicitly empty. Key methods: isPresent() / isEmpty() (Java 11). get() β returns value or throws NoSuchElementException (avoid using directly). orElse(T default) β return value or default. orElseGet(Supplier<T>) β lazily compute default (preferred when default is expensive). orElseThrow() β throw NoSuchElementException. orElseThrow(Supplier<X>) β throw custom exception. ifPresent(Consumer) β run action if present. map(Function) β transform if present. filter(Predicate) β filter value. flatMap(Function<T,Optional<R>>) β avoid nested Optionals. Best practices: Use as METHOD RETURN TYPE when absence is meaningful. NOT as method parameter or field. Never use get() without isPresent(). Prefer orElse/orElseGet over isPresent() + get() pattern.
Default methods (Java 8) are methods in interfaces with a concrete implementation using the default keyword: default void greet() { System.out.println('Hello'); }. They were introduced primarily to enable BACKWARD COMPATIBILITY when evolving interfaces. Problem: Adding a new abstract method to an existing interface (like Collection) would break ALL classes implementing it β they'd need to implement the new method. Solution: Default methods provide a default implementation. Existing classes inherit the default behavior without modification. New classes can override it. Java 8 added many default methods to existing interfaces: List.sort(), Map.forEach(), Map.getOrDefault(), Map.putIfAbsent(), Map.merge(), Map.compute(). Conflict resolution: If a class implements two interfaces with the same default method, it MUST override the method. To call a specific interface's default: InterfaceName.super.methodName(). Java 9 added private methods in interfaces β for code reuse between default methods within the same interface.
Collectors.toList() (Java 8): Returns a mutable List (usually ArrayList). No null restrictions. Result list can be modified freely. Most commonly used before Java 16. Collectors.toUnmodifiableList() (Java 10): Returns an UNMODIFIABLE list. add/remove/set throws UnsupportedOperationException. Does NOT allow null elements. Wraps result in Collections.unmodifiableList(). Stream.toList() (Java 16): Terminal operation directly on Stream β no need for collect(). Returns an UNMODIFIABLE list. Does NOT allow null elements. More concise: list.stream().filter(...).toList() vs list.stream().filter(...).collect(Collectors.toList()). Current best practice: Use Stream.toList() for read-only results (Java 16+). Use Collectors.toList() when you need a mutable result or are on Java 8-15. Use Collectors.toCollection(ArrayList::new) when you need a specific List type.
Collectors.groupingBy(classifier) groups stream elements by a key function and returns Map<K, List<T>>. Example: Map<String, List<Employee>> byDept = employees.stream().collect(Collectors.groupingBy(e -> e.department)). Downstream collectors add aggregation: groupingBy(key, counting()) β Map<K, Long> β count per group. groupingBy(key, summingInt(e -> e.salary)) β Map<K, Integer> β sum per group. groupingBy(key, mapping(e -> e.name, toList())) β Map<K, List<String>> β map values to names first. groupingBy(key, averagingDouble(e -> e.salary)) β Map<K, Double>. groupingBy(key, toUnmodifiableList()) β immutable value lists. Multi-level: groupingBy(e -> e.dept, groupingBy(e -> e.city)) β Map<String, Map<String, List<Employee>>>. Related: partitioningBy(predicate) β always Map<Boolean, List<T>> with exactly two groups. toMap(keyMapper, valueMapper) β for direct key-to-value mapping (throws on duplicate keys by default; provide merge function to handle).
Method references are shorthand lambda expressions that call a single existing method. Four types: (1) Static method reference: ClassName::staticMethod β Integer::parseInt. Equivalent lambda: x -> Integer.parseInt(x). (2) Instance method of a specific instance: instance::instanceMethod β myPrinter::print. Equivalent: () -> myPrinter.print(). Used when you have a specific object whose method you want to call. (3) Instance method of an arbitrary instance of a type: ClassName::instanceMethod β String::toUpperCase. Equivalent: s -> s.toUpperCase(). The method is called on the first parameter of the lambda. Most commonly used. (4) Constructor reference: ClassName::new β ArrayList::new. Equivalent: () -> new ArrayList<>() or (n) -> new ArrayList<>(n). Used with Supplier<T>, Function<Integer, T[]>, etc. Method references are MORE readable than lambdas when the lambda just calls one existing method. They also aid JIT optimization.
Java 11 (LTS, Sept 2018) key additions: String methods: isBlank() β true if empty or whitespace only. lines() β returns Stream<String> of lines. strip() / stripLeading() / stripTrailing() β Unicode-aware whitespace removal (unlike trim() which only handles ASCII). repeat(n) β 'ab'.repeat(3) β 'ababab'. File methods: Files.readString(path) β read entire file as String. Files.writeString(path, string) β write String to file. HttpClient (standard): java.net.http.HttpClient β modern, async HTTP client supporting HTTP/2 and WebSocket. Replaces deprecated HttpURLConnection for simple cases. Predicate.not(): Predicate.not(String::isBlank) β negation of method reference (cleaner than ((Predicate<String>)String::isBlank).negate()). var in lambda: (var x, var y) -> x + y β allows annotations on lambda parameters. Running Java: java HelloWorld.java β runs single-file programs without explicit compilation. Removals: Nashorn JavaScript engine removed, Java EE/CORBA modules removed from JDK.
Records (JEP 395, GA in Java 16) are immutable data carrier classes with dramatically reduced boilerplate. Declaration: record Point(int x, int y) {} β this single line auto-generates: private final int x and y fields, public canonical constructor Point(int x, int y), accessor methods x() and y() (NOT getX()), equals() based on all components, hashCode() based on all components, toString() printing 'Point[x=1, y=2]'. Key characteristics: Records implicitly extend java.lang.Record β cannot extend other classes (but can implement interfaces). All fields are private final β immutable by design. Can have additional methods, static fields, static methods. Compact constructor for validation: record Range(int start, int end) { Range { if(start > end) throw new IllegalArgumentException(); } }. When to use: DTOs, value objects, query results, tuple-like returns. When NOT: JPA entities (require mutable, no-arg constructors), classes needing inheritance, mutable objects. Jackson supports records natively. Lombok @Value is superseded by records in Java 16+.
Concurrency questions separate mid-level from senior candidates. Interviewers expect deep understanding of JMM, thread safety mechanisms, and real-world concurrency patterns.
Way 1 β Extend Thread class: class MyThread extends Thread { public void run() { ... } } new MyThread().start(). Way 2 β Implement Runnable interface: class MyTask implements Runnable { public void run() { ... } } new Thread(new MyTask()).start(). Or with lambda: new Thread(() -> { ... }).start(). Preferred: Implementing Runnable. Reasons: Java supports only single class inheritance β extending Thread prevents extending any other class, limiting design. Separation of concerns β Runnable defines the TASK; Thread defines the execution mechanism. The same Runnable can be submitted to different executors (ThreadPool, virtual thread, etc.). Java 5+: Also use Callable<V> (returns a result, throws checked exceptions) with ExecutorService.submit() β Future<V>. Java 21 Virtual Threads: Thread.ofVirtual().start(task) or Executors.newVirtualThreadPerTaskExecutor() β one virtual thread per task, minimal OS overhead. Best practice: Always use ExecutorService/thread pools rather than creating raw threads.
Thread.sleep(millis): Static method on Thread class. Pauses the CURRENT thread for specified duration. Does NOT release any locks/monitors the thread holds. Can be called anywhere β no synchronization required. Woken only by: time expiring or Thread.interrupt() (throws InterruptedException). Object.wait(): Instance method on Object β called as object.wait(). RELEASES the monitor/lock of the object β allows other synchronized threads to proceed. Must be called INSIDE a synchronized block/method (else throws IllegalMonitorStateException). Woken by: object.notify() (wakes one waiting thread), object.notifyAll() (wakes all), or wait(timeout) expiry. Designed for inter-thread communication (producer-consumer pattern). Key difference: sleep() = timed pause, no lock release, anywhere. wait() = conditional wait, releases lock, must be synchronized. Modern alternative: java.util.concurrent.locks.Condition (lock.newCondition().await()/signal()) is the modern, flexible replacement for wait/notify.
volatile guarantees VISIBILITY β reads and writes to a volatile variable always go to main memory, bypassing CPU caches. Without volatile, each thread may cache the variable value in its CPU cache/registers and see stale values. volatile ensures: All threads always see the most recent write. Write to volatile happens-before all subsequent reads of that same volatile variable (Java Memory Model guarantee). What volatile does NOT guarantee: ATOMICITY of compound operations. int++ on a volatile int is still NOT atomic (read + increment + write = 3 operations that can be interleaved). For atomic compound operations: use AtomicInteger, AtomicLong, AtomicReference, or synchronized. Typical use: boolean running = volatile true; β simple flag read by one thread, written by another. Correct for: single write + multiple reads pattern. Not for: check-then-act (if counter == 0 then decrement) β still needs synchronization or AtomicInteger.
Deadlock: Two or more threads are blocked forever, each waiting to acquire a lock held by the other. Classic example: Thread-1 holds Lock-A, waits for Lock-B. Thread-2 holds Lock-B, waits for Lock-A. Neither can proceed β permanent block. Four Coffman conditions (all must hold for deadlock): Mutual Exclusion (only one thread can hold a lock), Hold and Wait (thread holds a lock while waiting for another), No Preemption (locks cannot be forcibly taken), Circular Wait (threads form a cycle of lock waiting). Prevention strategies: Lock ordering: Always acquire multiple locks in the same global order β eliminates circular wait. tryLock() with timeout: Lock lock; if(!lock.tryLock(timeout, unit)) { // give up, retry }. Lock-free data structures: ConcurrentHashMap, AtomicInteger β no explicit locks. Minimize lock scope: hold locks for the shortest time possible. Detection: jstack <pid> shows thread dump with 'Found one Java-level deadlock' β monitor with VisualVM or jconsole.
Problems with raw threads: Thread creation is expensive (OS resource, ~1MB stack). One thread per task doesn't scale. No task queuing, rejection handling, or lifecycle management. Executor framework (java.util.concurrent, Java 5): ExecutorService provides: submit(Callable/Runnable) β Future<T>. shutdown() β no new tasks, waits for existing to finish. shutdownNow() β attempts to stop running tasks. awaitTermination(). Factory methods via Executors: Executors.newFixedThreadPool(n) β n worker threads, queues excess tasks. Executors.newCachedThreadPool() β unlimited threads, reuses idle ones, times out after 60s idle. Executors.newSingleThreadExecutor() β single thread, tasks processed in order. Executors.newScheduledThreadPool(n) β for delayed/periodic tasks. CompletableFuture (Java 8): Async task chaining and composition. Virtual threads (Java 21): Executors.newVirtualThreadPerTaskExecutor() β creates a new virtual thread per task β massive scalability with simple code. Best practice: Always use ExecutorService over raw threads. Always shutdown() gracefully. Size thread pools based on workload type: CPU-bound β Runtime.getRuntime().availableProcessors(). I/O-bound β much larger or use virtual threads.
The Java Memory Model (JLS Chapter 17) defines the rules for how threads interact through memory β what value a read can return given a prior write by another thread. Problem: Multi-core CPUs with caches and out-of-order execution, plus JVM JIT optimizations, mean threads can see inconsistent views of shared data without explicit rules. JMM defines happens-before relationships: If action A happens-before action B, then A's results are guaranteed visible to B. Happens-before is established by: synchronized: unlock of a monitor happens-before every subsequent lock on the SAME monitor. volatile: write to a volatile variable happens-before every subsequent read of that same variable. Thread.start(): All actions before start() happen-before any action in the started thread. Thread.join(): All actions in a thread happen-before join() returns in the joining thread. Transitivity: if A hb B and B hb C, then A hb C. Practical implication: Without synchronization or volatile, the JVM/CPU is free to reorder memory operations β threads may see stale values even for writes that appear 'earlier' in program order.
synchronized provides mutual exclusion + visibility: only one thread can execute a synchronized region on the same monitor at a time. Other threads block until the monitor is released. Synchronized method: public synchronized void method() { ... } β locks on 'this' (instance methods) or on the Class object (static methods). Entire method body is synchronized β coarse-grained. Synchronized block: synchronized(lockObject) { // critical section only } β locks on specified object. Fine-grained β only the minimum required code is synchronized. Advantages of blocks over methods: Better performance β non-synchronized code runs concurrently. Use private final lock objects to prevent external locking on 'this'. Supports multiple independent locks for unrelated data. Example: private final Object lock = new Object(); synchronized(lock) { ... }. synchronized also provides visibility: changes made inside synchronized block are visible to all threads that subsequently enter any synchronized block on the SAME monitor object. Modern best practice: Use java.util.concurrent.locks.ReentrantLock for advanced needs (tryLock, interruptible acquisition, read/write locks). Use ConcurrentHashMap/AtomicInteger where possible to avoid explicit synchronization.
Atomic classes (java.util.concurrent.atomic) provide lock-free thread-safe operations on single variables using hardware-level CAS (Compare-And-Swap) instructions. Core classes: AtomicInteger: incrementAndGet(), decrementAndGet(), addAndGet(delta), getAndSet(newVal), compareAndSet(expected, update). AtomicLong: same operations for long values. AtomicBoolean: thread-safe boolean. AtomicReference<T>: thread-safe object reference. AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray: thread-safe arrays. LongAdder (Java 8): Better than AtomicLong for high-concurrency counting β uses multiple cells to reduce contention. Better throughput when many threads increment simultaneously. LongAccumulator: generalization of LongAdder with custom accumulation function. When to use: Simple counter shared across threads: AtomicInteger. High-frequency counter (100+ threads): LongAdder. Single boolean flag for thread stopping: AtomicBoolean. Lock-free CAS-based state machine: AtomicReference with compareAndSet(). Performance: CAS-based atomics are faster than synchronized for simple operations due to no locking overhead β but can spin under high contention.
Future<T> (Java 5) limitations: blocking get() β must block to retrieve result. No callbacks when result is ready. No composition of multiple futures. No exception handling pipeline. Cannot manually complete. CompletableFuture<T> (Java 8) β fully composable async programming: Async execution: CompletableFuture.supplyAsync(() -> fetchData()) β runs on ForkJoinPool. Non-blocking callbacks: .thenApply(result -> transform(result)), .thenAccept(result -> process(result)), .thenCompose(result -> anotherCompletableFuture(result)) β flatMap for futures. Combining: thenCombine(otherFuture, (r1, r2) -> merge(r1, r2)). CompletableFuture.allOf(f1, f2, f3) β wait for all. CompletableFuture.anyOf(f1, f2) β wait for first. Exception handling: .exceptionally(ex -> fallback). .handle((result, ex) -> ex != null ? fallback : result). Custom thread pool: supplyAsync(task, myExecutor). Manual completion: complete(value), completeExceptionally(ex). Java 21 + Virtual Threads: submit async tasks to virtual thread executor for massive concurrency with synchronous-looking code.
ThreadLocal<T> provides thread-local variables β each thread accessing a ThreadLocal variable gets its own independent copy of the value. No synchronization needed. Common uses: Storing per-request data in web applications (current user, request ID, locale, database connection). Database connection per thread. SimpleDateFormat instances (not thread-safe β use one per thread). Usage: ThreadLocal<String> requestId = new ThreadLocal<>(); requestId.set(UUID.randomUUID().toString()); requestId.get() β returns this thread's value. requestId.remove() β CRITICAL: removes value for current thread. Risks: Memory leak in thread pools: Thread pool threads are reused. If ThreadLocal.remove() is not called after use, the value persists in the thread β potentially across multiple requests. Large objects or accumulated data can cause significant memory leaks and even cross-request data contamination (security issue). Rule: ALWAYS call remove() in a finally block after use in thread pool contexts. Java 21 alternative: ScopedValue (JEP 446, preview) β immutable, bounded scope, automatically cleaned up, no memory leak risk, better for virtual threads.
Virtual Threads (JEP 444, GA Java 21) are lightweight JVM-managed threads that eliminate the bottleneck of OS thread-per-request model. Platform threads (before): Each Java thread = one OS thread. OS threads are expensive (~1MB stack). Practical limit: ~10,000 threads per JVM. Reactive/async programming was the workaround β but complex, hard to debug. Virtual threads: Many-to-few mapping β JVM multiplexes millions of virtual threads onto small pool of OS carrier threads. When a virtual thread BLOCKS (I/O, sleep, etc.): carrier thread is released, runs other virtual threads. When unblocked: remounted to any available carrier thread. Creation: Thread.ofVirtual().start(task) or Executors.newVirtualThreadPerTaskExecutor(). Benefits: Write simple synchronous code that scales to millions of concurrent connections. Same debugging as regular threads (stack traces, thread dumps). Near-zero creation cost. Spring Boot 3.2+: spring.threads.virtual.enabled=true configures Tomcat and schedulers automatically. Limitation: synchronized blocks PIN the carrier thread β replace with ReentrantLock for truly non-blocking behavior. Not for CPU-bound tasks β doesn't parallelize compute.
Producer-Consumer: Producers generate data and put it in a shared buffer. Consumers take data from the buffer and process it. They run concurrently, decoupled through the buffer. Classic implementation with BlockingQueue (recommended): BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100); Producer: queue.put(task) β blocks if queue full. Consumer: Task t = queue.take() β blocks if queue empty. BlockingQueue handles all synchronization internally β no manual wait/notify needed. Implementations: ArrayBlockingQueue(capacity) β bounded, fair option. LinkedBlockingQueue(capacity) β bounded/unbounded. PriorityBlockingQueue β priority-ordered. SynchronousQueue β zero capacity, direct handoff. Traditional wait/notify (educational): Manually synchronize on shared buffer, use while(buffer.isEmpty()) wait(), use notify() after add/remove β complex and error-prone. ExecutorService as consumer: Use a thread pool as the consumer β submit() tasks to the pool, pool threads process concurrently. Kafka/RabbitMQ at scale: For distributed producer-consumer across services, use message brokers.
JVM internals questions demonstrate that you understand what happens below the language level β essential for performance tuning, diagnosing production issues, and senior/lead roles.
JVM partitions memory into: Heap: Largest area. All objects and arrays (created with new) stored here. Shared across all threads. Managed by Garbage Collector. Split into Young Generation (Eden + Survivor S0/S1) and Old/Tenured Generation. Method Area / Metaspace (Java 8+): Stores class-level metadata β class structures, method bytecode, constant pools, static variables. Java 7 and earlier: PermGen (fixed size). Java 8+: Metaspace (native memory β grows dynamically, configurable with -XX:MaxMetaspaceSize). JVM Stack (per thread): Stack frames for each method invocation. Each frame has: local variables, operand stack, constant pool reference. StackOverflowError when stack depth exceeded. Program Counter (PC) Register (per thread): Address of currently executing JVM instruction. Native Method Stack (per thread): Supports native (JNI) method calls. Direct Memory: Off-heap memory for java.nio ByteBuffer.allocateDirect() β high-performance NIO, not managed by GC. Tuning flags: -Xms (initial heap), -Xmx (max heap), -Xss (stack size per thread), -XX:MaxMetaspaceSize.
Generational GC exploits the 'weak generational hypothesis': most objects die young. Heap is divided into generations: Young Generation: New objects allocated in Eden Space. When Eden fills, Minor GC runs β fast (milliseconds). Most objects are collected (died young). Survivors move to Survivor spaces (S0/S1) β alternate between them each GC cycle. Each surviving GC increments the object's 'age.' Object promoted to Old Gen when age exceeds threshold (default 15). Old Generation (Tenured): Long-lived objects. Collected in Major/Full GC β more expensive (larger area, Stop-The-World pause). Full GC: Collects both Young and Old generations β most expensive, causes long STW pause. Stop-The-World (STW): Most GC events pause ALL application threads momentarily. Modern GCs minimize this. Garbage collectors: G1GC (default Java 9+): Region-based, balanced throughput/latency, typical pauses 100-200ms. ZGC (Java 21 generational): Sub-millisecond pauses, massive heaps. Shenandoah: Also sub-10ms concurrent GC. Flags: -XX:+UseG1GC, -XX:+UseZGC, -XX:MaxGCPauseMillis=200.
JIT (Just-In-Time) compiler converts 'hot' bytecode to native machine code at runtime for dramatically improved performance. Tiered Compilation (default Java 7+): C1 (client compiler): Fast compilation, basic optimizations β quick startup, moderate performance. C2 (server compiler): Slow compilation, aggressive optimizations β peak performance for long-running code. JVM monitors execution, counts invocations (default 10,000 for methods, 10,500 for loops), promotes hot code to C2. Key optimizations: Method inlining: Replaces method call with method body β eliminates call overhead, enables further optimizations (most impactful). Loop unrolling: Executes multiple iterations per loop body β reduces loop overhead. Devirtualization: Converts virtual method calls to direct calls when JIT can prove only one implementation is used β enables inlining. Intrinsics: Replaces standard method calls (Math.abs(), Arrays.copyOf()) with hand-optimized native assembly. Dead code elimination: Removes code that never executes. Constant folding: Computes constant expressions at compile time. Escape analysis: Enables stack allocation and scalar replacement for non-escaping objects β avoids heap allocation entirely.
Escape analysis is a JIT compiler optimization that determines whether an object 'escapes' the method that creates it β i.e., whether other code outside the method can access it. An object does NOT escape if: it is not passed to another method, not returned from the method, not stored in a field or static variable. When an object doesn't escape, JIT can apply: Stack allocation: Object is allocated on the thread stack instead of the heap. Stack allocation is instant and automatic β object is reclaimed when the method returns, no GC needed. Scalar replacement: Object fields are stored as individual local variables (in registers) β the object itself is NEVER allocated anywhere. Lock elision: If a synchronized block operates on a non-escaping object (no other thread can see it), the lock is eliminated β synchronization overhead removed. Example: StringBuilder sb = new StringBuilder(); sb.append('a'); return sb.toString(); β the StringBuilder never escapes (the String result escapes, not sb), so JIT may stack-allocate or scalar-replace it. This is why small, short-lived objects in Java are surprisingly cheap β often never hit the heap.
Memory leak signs: Continuously growing heap usage without corresponding traffic growth. Frequent Full GCs that don't recover memory. OutOfMemoryError: Java heap space in logs. Degrading performance over time. Diagnosis steps: Monitor: JVM metrics via Micrometer/Prometheus β heap_used, gc_pause_duration trends. Alert on continuous growth. Heap dump analysis: Trigger: -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/. Manual: jmap -dump:format=b,file=heap.hprof <pid>. Analyze with Eclipse MAT β Dominator Tree shows what retains most memory. Common leak causes and fixes: Static collections: Map/List in static field accumulates indefinitely β use expiring cache (Caffeine), evict stale entries. ThreadLocal leaks: ThreadLocals in thread pool threads persist across requests β always call remove() in finally. Event listeners not deregistered β explicit removeListener() or WeakReference listeners. Connection/resource not closed β try-with-resources. Hibernate 1st-level cache in batch β session.clear() periodically. ClassLoader leaks during redeploy β ensure proper shutdown hook cleanup.
G1GC (Garbage-First, default since Java 9): Region-based heap (equal regions 1-32MB). Collects regions with most garbage first. Concurrent marking reduces STW time. Typical STW pauses: 100-200ms. Target: balanced throughput and latency for most server apps. Best for: heaps 4-50GB, most production Java apps. Flag: -XX:+UseG1GC -XX:MaxGCPauseMillis=200. ZGC (production Java 15, massively improved Java 21 Generational ZGC): Fully concurrent β almost all work done with app running. STW pauses: sub-millisecond regardless of heap size (tested to 16TB). Java 21 Generational ZGC significantly improved throughput. Best for: latency-critical apps (APIs, trading, gaming), very large heaps (>50GB). Flag: -XX:+UseZGC. Shenandoah: Also concurrent, sub-10ms pauses. Red Hat's contribution. Concurrent compaction (moves objects while app runs). Best for: similar to ZGC, medium-large heaps. Flag: -XX:+UseShenandoahGC. Selection: Default/most apps β G1GC. Sub-millisecond latency required β ZGC (Java 21+). Read Hat environments β Shenandoah. Short-lived batch jobs β Parallel GC (-XX:+UseParallelGC) for maximum throughput.
Class loading loads .class files into JVM memory. Three phases: Loading β finds and reads .class file. Linking β Verification (bytecode valid?), Preparation (static fields default values), Resolution (symbolic references β direct references). Initialization β runs static initializers and assigns static variable values. ClassLoader hierarchy (parent delegation model): Bootstrap ClassLoader: Loads core Java classes (java.lang, java.util, etc.) from JDK. Written in native code β no Java class. Platform ClassLoader (formerly Extension): Loads platform classes from JDK modules. Application ClassLoader: Loads your application classes from classpath/modulepath. Custom ClassLoader: Override findClass() to load from network, database, encrypted JARs, or dynamic bytecode. Parent Delegation Model: Each ClassLoader first delegates to its PARENT. Only if parent cannot find the class does the child try to load it. Prevents: malicious classes replacing core Java classes (you can't load a fake java.lang.String). Custom ClassLoaders: OSGi (module isolation), Java EE app servers (web application isolation), hot-reload (Spring DevTools), dynamic plugin systems.
Java Flight Recorder (JFR): Low-overhead (<1-2% overhead), always-on profiling built into JVM (free in OpenJDK 11+). Records: method profiling (sampled call stacks), JVM events (GC, class loading, JIT compilation, thread start/stop), I/O events, heap statistics, lock contention, memory allocation, exceptions. Enable: java -XX:StartFlightRecording=filename=recording.jfr,duration=60s,settings=profile MyApp. At runtime: jcmd <pid> JFR.start duration=60s filename=recording.jfr. Analyze: JDK Mission Control (JMC) β GUI for reading .jfr files. Shows hot methods, GC pauses, thread states, allocation rates, lock contention. Production safe: designed to run 24/7 with minimal overhead. async-profiler: Complements JFR β avoids JFR's safepoint bias (JFR can miss true hot paths between safepoints). Samples at arbitrary points. CPU flame graphs show EXACTLY where time is spent. Output: interactive HTML flame graphs. Production diagnosis workflow: (1) Monitor symptoms (high CPU, high latency, heap growth). (2) JFR for broad view. (3) async-profiler CPU flame graph for specific hot paths. (4) Heap dump for memory leaks. (5) Thread dump (jstack) for deadlocks.
OutOfMemoryError (OOM) is an Error (not Exception) thrown when the JVM cannot allocate an object or load a class because insufficient memory is available and GC cannot free enough. Different types: 'Java heap space': Most common β heap is full, GC cannot reclaim enough. Causes: too many live objects, memory leaks, heap too small. Fix: increase -Xmx, fix memory leaks, optimize object lifecycles. 'GC overhead limit exceeded': JVM spending >98% of time in GC, recovering <2% of heap β essentially thrashing. Usually precedes 'Java heap space'. Fix: same as above. 'Metaspace': Class metadata area full. Causes: too many loaded classes (dynamic class generation, too many libraries). Fix: -XX:MaxMetaspaceSize=256m, check for ClassLoader leaks. 'Direct buffer memory': java.nio direct ByteBuffer allocation failed β off-heap memory exhausted. Fix: increase -XX:MaxDirectMemorySize, close ByteBuffers explicitly. 'Unable to create new native thread': OS cannot create more threads β too many platform threads. Fix: reduce thread count, use virtual threads, increase OS thread limit. Diagnostic tool: -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/ β automatic heap dump on OOM for post-mortem analysis.
Java provides four reference strengths controlling GC eligibility: Strong reference (default): Object obj = new Object() β as long as a strong reference exists, GC NEVER collects the object. Most common. Soft reference (java.lang.ref.SoftReference): GC collects only when JVM is about to throw OutOfMemoryError β prefers to keep soft-reachable objects if memory permits. Ideal for: memory-sensitive caches (cache grows when memory available, shrinks under pressure). SoftReference<byte[]> cache = new SoftReference<>(data). Weak reference (java.lang.ref.WeakReference): GC collects at the NEXT GC cycle regardless of memory pressure. Object not guaranteed to survive until next GC. Used by: WeakHashMap (keys are weak references β entry removed when key collected), canonicalized mappings, event listeners without preventing listener GC. Phantom reference (java.lang.ref.PhantomReference): Enqueued AFTER object is finalized but BEFORE memory is reclaimed. Cannot retrieve the referent (get() returns null). Used for: cleanup actions after finalization, replacing deprecated finalize(). java.lang.ref.Cleaner (Java 9+) uses phantom references internally. ReferenceQueue: Used with weak/soft/phantom references to receive notifications when GC clears references.
Senior Level β Advanced & Modern Java (Q81βQ100)
These questions cover modern Java (17β21), design patterns, Spring Boot, system design scenarios, and best practices expected from senior/lead developers.
Sealed classes (JEP 409, Java 17) restrict which classes can extend or implement them. Syntax: public sealed interface Shape permits Circle, Rectangle, Triangle {} Each permitted subtype must be declared final, sealed, or non-sealed. Why useful: For closed type hierarchies (domain types with a fixed set of variants), you want the COMPILER to know all possible types β enabling exhaustive checks. Pattern Matching Switch (Java 21) + sealed classes: switch (shape) { case Circle c -> Math.PI * c.radius() * c.radius(); case Rectangle r -> r.width() * r.height(); case Triangle t -> calculateArea(t); } β no default needed; compiler verifies ALL cases covered because it knows all permitted subtypes. Missing any subtype β compile error. Combine with records: record Circle(double radius) implements Shape {} β powerful algebraic data types. Real uses: Error handling (sealed Result<T> permits Success<T>, Failure<T>), API response types, state machines, AST nodes in parsers/compilers.
Pattern matching eliminates manual type-checking and casting. instanceof pattern (Java 16, JEP 394): Before: if (obj instanceof String) { String s = (String) obj; s.length(); } After: if (obj instanceof String s) { s.length(); } β check + bind in one expression, s is automatically cast and scoped. Guarded pattern: if (obj instanceof String s && s.length() > 5) { ... } Switch pattern (Java 21, JEP 441): switch (obj) { case Integer i -> 'int: ' + i; case String s when s.length() > 5 -> 'long string'; case String s -> 'short string'; case null -> 'null'; default -> 'other'; } switch now works on ANY reference type. when clause adds guard conditions. null handling in switch (previously NPE). Record patterns (Java 21, JEP 440): if (obj instanceof Point(int x, int y)) { } β destructures records in patterns. In switch: case Point(int x, int y) when x == y -> 'diagonal'. Nested: case ColorPoint(Point(int x, int y), Color c) -> .... These features together make Java code for type-based dispatching dramatically cleaner and safer.
Singleton ensures only ONE instance of a class exists throughout the application. Best implementation β Enum Singleton (Josh Bloch, Effective Java): enum Database { INSTANCE; public void query(String sql) { ... } } β handles serialization (enum serialization preserves singleton), reflection attacks (cannot reflectively create enum instances), and thread safety (class initialization is thread-safe) automatically. Best by far. Bill Pugh (Initialization-on-Demand Holder): class Singleton { private Singleton() {} private static class Holder { static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } } β lazy (created only when getInstance() first called), thread-safe (class initialization is thread-safe), no synchronization overhead. Double-Checked Locking (acceptable): private static volatile Singleton instance; public static Singleton getInstance() { if(instance==null) { synchronized(Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; } β volatile is REQUIRED to prevent partial initialization visibility. Avoid: synchronized getInstance() in every call (performance), eager initialization via static field (OK but not lazy).
Builder pattern constructs complex objects step-by-step via a fluent API, avoiding telescoping constructors (constructors with many parameters where most are optional). Problem: class Pizza(String size, boolean cheese, boolean pepperoni, boolean mushrooms, boolean olives, boolean bacon) β 32 possible combinations, most parameters often null/false. Solution with Builder: Pizza pizza = new Pizza.Builder('L').cheese(true).pepperoni(true).build(); Implementation: class Pizza { private final String size; private final boolean cheese; private Builder(String size) { this.size = size; } // private constructor public static class Builder { String size; boolean cheese; boolean pepperoni; Builder(String size) { this.size = size; } Builder cheese(boolean v) { this.cheese = v; return this; } Pizza build() { return new Pizza(this); } } } Benefits: Readable construction. Immutable objects (build() creates final object). Optional parameters handled cleanly. Validates invariants in build() before object creation. Java ecosystem examples: StringBuilder, Stream.Builder, HttpRequest.Builder, Lombok @Builder (generates automatically), Record compact constructors. When to use: 4+ constructor parameters, many optional parameters, want to enforce immutability, need clear parameter semantics.
Language features added since Java 8: var local type inference (Java 10). switch expressions (Java 14) β return values, no fall-through. Text Blocks (Java 15) β multi-line strings without escaping. Records (Java 16) β immutable data carriers auto-generating constructor/equals/hashCode/toString. instanceof pattern matching (Java 16) β combined type check and bind. Sealed classes (Java 17) β restricted type hierarchies. Pattern matching switch (Java 21) β exhaustive type-based dispatch. Record patterns (Java 21) β destructuring in patterns. Platform: Virtual Threads (Java 21) β million-thread concurrency. Sequenced Collections (Java 21) β getFirst(), getLast(), reversed(). Metaspace replaces PermGen (Java 8). Module system (Java 9). HttpClient standard API (Java 11). GC: G1GC default, ZGC/Shenandoah for ultra-low latency, Generational ZGC (Java 21). API additions: String.isBlank(), lines(), strip(), repeat() (Java 11). Files.readString/writeString (Java 11). Stream.toList() (Java 16). Optional.ifPresentOrElse(), stream(), or() (Java 9). Collectors.teeing() (Java 12). Removals/deprecations: finalize() deprecated (Java 9), removed (Java 18). javax.* β jakarta.* (Spring Boot 3 requires Java 17+ and Jakarta EE 9+). CMS GC removed. Nashorn JS engine removed.
S β Single Responsibility: A class has ONE reason to change. Bad: UserService handles user data AND sends emails. Good: UserService for data, EmailService for emails. O β Open/Closed: Open for extension, closed for modification. Bad: adding new payment type requires modifying processPayment() switch statement. Good: interface PaymentProcessor with implementations β add new type by adding a class, not modifying existing code. L β Liskov Substitution: Subtypes must be substitutable for their base type. Bad: Square extends Rectangle but violates Rectangle's invariant that width and height are independent. Good: separate Square and Rectangle classes without inheritance. I β Interface Segregation: Clients should not depend on methods they don't use. Bad: interface Worker { work(); eat(); sleep(); } β robots don't eat/sleep. Good: separate Workable, Eatable, Sleepable interfaces. D β Dependency Inversion: High-level modules depend on abstractions, not concretions. Bad: OrderService creates new MySQLRepository(). Good: OrderService depends on Repository interface β implementation injected via Spring @Autowired. SOLID is the foundation of testable, maintainable, extensible Java code.
@Transactional marks a method or class where database operations execute within an atomic transaction. Internal working: Spring creates a CGLIB (for classes) or JDK dynamic proxy (for interfaces) around the bean. When the annotated method is called, the proxy intercepts, opens a transaction (or joins existing per propagation), delegates to actual method, commits on success, rolls back on exception. Propagation settings: REQUIRED (default) β join existing or create new. REQUIRES_NEW β always new, suspend existing. NESTED β savepoint within existing. NOT_SUPPORTED β run without transaction. MANDATORY β must exist or throw. Isolation levels: READ_COMMITTED (most DBs default), REPEATABLE_READ, SERIALIZABLE. rollbackFor: By default, only RuntimeException triggers rollback. For checked exceptions: @Transactional(rollbackFor = Exception.class). Common pitfalls: Self-invocation β calling @Transactional method from within the SAME class bypasses the proxy β no transaction. Fix: inject self or restructure. Private methods β @Transactional on private is silently ignored. Requires proxy. Checked exceptions β IOException won't rollback by default. LazyInitializationException β accessing lazy associations outside open transaction.
Dependency Injection (DI): Spring's IoC container manages object creation and wiring. Instead of classes creating their own dependencies (new Repository()), Spring injects them. @Autowired: Marks a field, constructor, or setter for injection. Spring resolves by TYPE. Constructor injection (PREFERRED): @Autowired public Service(Repository repo) β ensures dependencies at construction, supports immutable (final) fields, easier testing (inject via constructor in unit tests without Spring). Field injection: @Autowired private Repo repo β concise but harder to test. When MULTIPLE beans of the same type exist, Spring throws NoUniqueBeanDefinitionException. @Qualifier('beanName'): Disambiguates β specifies WHICH bean to inject when multiple exist. Example: @Autowired @Qualifier('mysqlRepository') private UserRepository repo. @Primary: Marks a bean as the DEFAULT when multiple beans of the same type exist. @Autowired without @Qualifier resolves to the @Primary bean. Example: @Primary @Bean public MySQLRepository mysqlRepo() { ... } Spring also supports: @Profile('prod') β activate bean only in production profile. @ConditionalOnProperty β conditional bean creation. Constructor injection is now the universally recommended approach β enforces required dependencies, enables immutable classes.
GraalVM Native Image compiles Java applications Ahead-Of-Time (AOT) to a standalone native executable β no JVM required at runtime. Benefits: Near-instant startup: milliseconds vs JVM warmup seconds (critical for serverless, CLI, Kubernetes). Very low memory footprint: 50-80% less RSS than JVM-based. Smaller container images: no JVM bundled (use distroless). Drawbacks: No dynamic class loading at runtime β reflection, Class.forName(), dynamic proxies must be registered in configuration files (or detected by Tracing Agent). No JIT β peak throughput lower than JVM (no runtime profile-guided optimization). Longer build time: minutes for native-image compilation. Spring Boot 3 + Spring Framework 6: First-class Native Image support. Spring AOT (Ahead-Of-Time) processing auto-generates native metadata. @NativeHint annotations. Micronaut and Quarkus: Designed from ground up for native compilation β compile-time DI, no reflection-heavy IoC. When to use native: Serverless (AWS Lambda cold starts), CLI tools, startup-critical microservices, minimal-footprint containers. When NOT to use: Long-running servers where JIT warmup pays off (high-throughput APIs, large enterprise systems with complex class hierarchies).
Key strategies: Thread model: Java 21 Virtual Threads + Spring MVC β simple synchronous code scaling to thousands of concurrent requests. Or Spring WebFlux (Reactor) for pure reactive approach. Connection pooling: HikariCP (Spring Boot default) β tune maximumPoolSize, connectionTimeout. Proper sizing for your DB. Caching: Caffeine (@Cacheable) for frequently-read slow-changing data. Redis for distributed cache across instances. Reduce DB load: JOIN FETCH / EntityGraph to avoid N+1. DTOs/projections instead of full entities. Batch operations. Async for non-critical work: @Async, Kafka for emails/notifications/analytics β don't block request thread. HTTP optimization: Compression (server.compression.enabled=true). HTTP/2. ETags for conditional requests. Efficient serialization: Jackson with modules. Avoid circular references. Protobuf for inter-service. Database: Proper indexes on query columns. Connection pool sizing. Read replicas for read-heavy workloads. CQRS for extreme scale. Observability: Micrometer metrics, distributed tracing (OpenTelemetry), structured logging. JVM tuning: Appropriate GC (G1GC/ZGC), heap sizing, JIT warmup probes. Profile FIRST (async-profiler, JFR) before optimizing β fix actual bottlenecks, not guesses.
Immutable object: state cannot change after creation. All modifications return new objects. Benefits: Thread-safe by nature (no synchronization needed). Safe as HashMap keys (hash never changes). Enables caching and sharing (same object can be referenced by many). Easier to reason about (no side effects). Design rules: Declare class as final (prevent mutable subclasses). All fields private and final. No setters or any mutating public methods. For mutable field types (Date, arrays, List): defensive copy in CONSTRUCTOR AND in GETTERS. Example: public final class ImmutablePerson { private final String name; private final List<String> hobbies; public ImmutablePerson(String name, List<String> hobbies) { this.name = name; this.hobbies = List.copyOf(hobbies); } public List<String> getHobbies() { return hobbies; } // already unmodifiable copy } Modern Java: Use java.time.LocalDate (already immutable) instead of java.util.Date. Use List.copyOf(), Map.copyOf() for immutable copies. Use record for automatic immutability (all fields final, no setters). Effective Java: 'Classes should be immutable unless there's a very good reason to make them mutable.'
Runnable (java.lang): Functional interface with void run() β takes no arguments, returns nothing, cannot throw checked exceptions. Used for fire-and-forget tasks. Submit to ExecutorService with execute() (no return) or submit() (returns Future<?>). Lambda: Runnable r = () -> System.out.println('running');. Callable<V> (java.util.concurrent): Functional interface with V call() throws Exception β takes no arguments, RETURNS a value of type V, CAN throw checked exceptions. Used when task produces a result. Submit to ExecutorService.submit(callable) β Future<V>. Result retrieved via future.get() (blocks) or future.isDone() polling. Lambda: Callable<String> c = () -> fetchData(); // can throw IOException. When to use: Fire-and-forget background task, no result needed β Runnable. Task that produces a result or might throw checked exceptions β Callable. Modern: CompletableFuture.supplyAsync(supplier) provides async result with richer chaining than raw Callable/Future. Virtual threads (Java 21): Both Runnable and Callable work transparently with virtual threads via Executors.newVirtualThreadPerTaskExecutor().
Proxy provides a surrogate that controls access to another object. The proxy and real object implement the same interface β callers cannot distinguish them. Types: Static Proxy: Manual implementation β verbose, compile-time only. JDK Dynamic Proxy: java.lang.reflect.Proxy.newProxyInstance() creates a proxy at runtime. Requires an interface. InvocationHandler intercepts all method calls β can add behavior (logging, security, transactions) before/after delegation. CGLIB Proxy: Byte-code-level subclassing β works WITHOUT interfaces. Spring uses CGLIB for classes, JDK dynamic proxy for interfaces. Spring AOP (Aspect-Oriented Programming) uses proxies: @Transactional β transactional proxy opens/commits/rolls back. @Cacheable β cache proxy checks cache before method, stores result after. @Secured/@PreAuthorize β security proxy checks authorization. @Async β async proxy submits method to thread pool and returns immediately. When you call a @Transactional method, you're actually calling the proxy β which does the real work. Self-invocation limitation: calling @Transactional from within the same class calls the real method, bypassing the proxy β hence no transaction. Always inject the bean and call it from outside for proxy to intercept.
Inheritance (IS-A): class Dog extends Animal β Dog inherits all of Animal's fields and methods. Creates tight coupling β changes in Animal can unexpectedly break Dog. Violates encapsulation (subclass exposed to parent internals). Java: single inheritance only. Fragile base class problem: adding a method to base class that conflicts with subclass method causes subtle bugs. Composition (HAS-A): class Dog { private Animal animal; } β Dog CONTAINS an Animal reference and delegates to it. Loose coupling β the inner object can change independently. Supports multiple behaviors. Runtime flexibility β composed object can be swapped. 'Favor composition over inheritance' (Gang of Four): Prefer composition UNLESS there is a genuine IS-A relationship AND you need polymorphism. Example: Instead of class Stack extends Vector (Java's mistake β exposes add() which bypasses stack semantics), use class Stack { private List<T> elements; push/pop delegate to list }. When inheritance IS appropriate: Genuine IS-A relationship (Dog IS-A Animal β biologically correct). Need polymorphism (Animal a = new Dog(); a.speak()). Hierarchy is shallow (1-2 levels) and stable. Testing advantage: composition allows injecting mock dependencies; inheritance makes mocking harder.
Sequenced Collections (JEP 431, Java 21) introduced three new interfaces that add consistent first/last element access to collections with a defined encounter order. New interfaces: SequencedCollection<E>: extends Collection. New methods: getFirst(), getLast(), addFirst(e), addLast(e), removeFirst(), removeLast(), reversed() (returns a live reversed view). SequencedSet<E>: extends Set + SequencedCollection. SequencedMap<K,V>: extends Map. New methods: firstEntry(), lastEntry(), putFirst(), putLast(), pollFirstEntry(), pollLastEntry(), reversed(), sequencedKeySet(), sequencedValues(), sequencedEntrySet(). Collections implementing these: List (ArrayList, LinkedList), Deque (ArrayDeque), LinkedHashSet (SequencedSet), LinkedHashMap (SequencedMap), TreeSet/TreeMap via their sorted interfaces. Before Java 21 workarounds eliminated: list.get(list.size()-1) β list.getLast(). new ArrayList<>(list).stream()... reversed β list.reversed().stream(). ((LinkedHashMap)map).entrySet().stream().reduce((a,b) -> b) β map.lastEntry(). TreeSet: addFirst()/addLast() throw UnsupportedOperationException β TreeSet controls its own order.
N+1 problem: Loading N entities causes 1 query for the parent list + N separate queries for each child association (lazy loading) = N+1 total queries. Example: 100 Orders each with a Customer β 101 queries total. Solutions: JOIN FETCH (JPQL): SELECT o FROM Order o JOIN FETCH o.customer β single query with SQL JOIN fetching orders + customers together. @EntityGraph: @EntityGraph(attributePaths = {'customer'}) on repository method β declaratively specifies eager fetching. Multiple associations: @EntityGraph(attributePaths = {'customer', 'items'}) β be careful with multiple collections (cartesian product problem). @BatchSize (Hibernate-specific): @BatchSize(size=25) on the association β fetches associations in batches: ceil(N/25) queries instead of N. FetchMode.SUBSELECT: @Fetch(FetchMode.SUBSELECT) β all associations fetched in a single subselect query. DTO projections: SELECT new OrderDTO(o.id, o.customer.name) FROM Order o β fetch only needed data without association loading. Blaze-Persistence / Spring Data projections: Interface-based projections for efficient partial entity loading. Detection: spring.jpa.show-sql=true + Hibernate statistics. For production: use p6spy or datasource-proxy to log and measure actual query count.
Event-driven architecture (EDA): Services communicate asynchronously via events rather than synchronous API calls. Producers publish events to a message broker. Consumers subscribe and process events independently. Key concepts: Event: An immutable record of something that happened (OrderPlaced, PaymentProcessed). Topic: Named channel for events of same type. Consumer Group: Multiple consumers sharing a topic's partitions for parallel processing. Apache Kafka: Distributed, fault-tolerant, high-throughput event streaming platform. Producers publish to topics. Consumers read at their own pace (offset-based, replayable). Retention: events stored for days/weeks β consumers can replay. Partitioning: topics split into partitions for parallel consumption. Spring Kafka: Producer: KafkaTemplate<String, Object>.send(topic, key, event). Consumer: @KafkaListener(topics = 'orders') void handle(OrderEvent event). Benefits of EDA: Decoupling β services don't know about each other. Resilience β consumer failure doesn't affect producer. Scalability β multiple consumers parallel process. Auditability β event log is complete history. Patterns: Saga (distributed transactions via compensating events), CQRS (separate write/read models via events), Event Sourcing (events as source of truth). Challenge: Eventual consistency β not immediately consistent. Requires idempotent consumers.
Centralized exception handling with @ControllerAdvice: @RestControllerAdvice class GlobalExceptionHandler { @ExceptionHandler(UserNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) ErrorResponse handleNotFound(UserNotFoundException ex) { return new ErrorResponse(404, ex.getMessage()); } @ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) ErrorResponse handleValidation(ConstraintViolationException ex) { ... } @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) ErrorResponse handleGeneral(Exception ex) { log.error('Unexpected error', ex); return new ErrorResponse(500, 'Internal server error'); } } Consistent error response format: { 'status': 404, 'error': 'Not Found', 'message': 'User 123 not found', 'timestamp': '2026-03-16T10:00:00Z', 'path': '/api/users/123' } β RFC 7807 Problem Details format standardizes this. Don't leak internals: NEVER expose stack traces or internal exception messages in production API responses. Log the full exception server-side. Use Problem Details (Spring 6 default): ErrorResponseException or ProblemDetail. Validation: Spring Validation (@Valid, @NotNull) + MethodArgumentNotValidException β 400 responses with field-level errors. Business exceptions: Create specific domain exceptions (UserNotFoundException, InsufficientFundsException) β never throw generic Exception from business logic.
Effective unit testing principles: Arrange-Act-Assert (AAA) pattern: clear setup, single action, focused assertions. Single behavior per test: each test verifies ONE thing β makes failures easy to diagnose. Descriptive test names: shouldThrowExceptionWhenAgeIsNegative() β explains what is tested and expected outcome. Use Mockito for isolation: @ExtendWith(MockitoExtension.class) @Mock UserRepository repo; @InjectMocks UserService service; when(repo.findById(1L)).thenReturn(Optional.of(user)); β test UserService in isolation without real DB. AssertJ for readable assertions: assertThat(result).isNotNull().extracting(User::getName).isEqualTo('Alice'); Common pitfalls: Testing implementation not behavior: testing which methods were called instead of what result was produced β brittle tests break on refactoring. Mocking too much: if you mock 5 dependencies for one test, you're testing nothing real. Over-specification: verify(repo, times(1)) everywhere β breaks when implementation changes without behavior changes. No edge case testing: only happy path β misses NullPointerException, empty collections, boundary values. Flaky tests: tests that sometimes pass, sometimes fail β usually due to time dependencies, random data, or test order coupling. Test coverage theater: achieving 80% coverage with trivial tests instead of meaningful ones. TDD benefit: writing tests first drives better API design.
Core Technical Skills: Deep Java internals β JVM, GC tuning, JMM, JIT, profiling. Modern Java 21 β Virtual Threads, Records, Sealed Classes, Pattern Matching, Sequenced Collections. Spring Boot 3 β WebFlux/MVC with Virtual Threads, Spring Security OAuth2, Spring Data R2DBC, Native Image. Cloud-native β Docker, Kubernetes, AWS/GCP/Azure, 12-factor apps. Microservices β distributed systems, Saga pattern, CQRS, event sourcing, service mesh. Concurrency β CompletableFuture, ForkJoinPool, concurrent collections, Virtual Threads, Structured Concurrency (Java 21 preview). Observability β Micrometer, Prometheus, Grafana, OpenTelemetry distributed tracing. Testing β JUnit 5, Mockito, Testcontainers, ArchUnit, contract testing. Performance β JFR, async-profiler, JMH benchmarking, query optimization. Architecture β system design, DDD, clean architecture, SOLID, design patterns. AI-augmented development β GitHub Copilot, CodeWhisperer β senior developers guide and review AI-generated code. Soft skills: mentoring juniors, technical writing (ADRs), cross-team collaboration, driving technical decisions with data, estimating, requirement refinement. Salary India 2026: βΉ18β50+ LPA depending on company and expertise. Java remains one of the highest-paying and most in-demand languages globally.
Interview Quick Tips β Do's and Don'ts
Knowing the answers is only half the battle. How you communicate your knowledge in the interview room determines whether you get the offer.
βΆ
β Structure your answers β Start with a one-sentence definition, then explain HOW it works, then give a REAL WORLD USE CASE. Interviewers remember concrete examples more than abstract definitions.
βΆ
β Know the WHY β Don't just say what something is β explain WHY it was designed that way. 'HashMap allows one null key because null has no hashCode, and it's handled as a special case at bucket 0' β this signals deep understanding.
βΆ
β Mention trade-offs β Senior candidates compare options. 'I'd use ArrayList here because random access is frequent, but if we were inserting at the front constantly, I'd reconsider LinkedList or ArrayDeque.'
βΆ
β Connect to real projects β 'In my previous project, we had an N+1 problem with Hibernate that caused 200ms latency per request. We fixed it with JOIN FETCH and reduced it to 15ms.' Real impact numbers impress interviewers.
βΆ
β Don't memorize without understanding β Interviewers follow up on every answer. 'HashMap is O(1)' will be followed by 'Why? How? What's the worst case?' If you memorized without understanding, you'll get stuck.
βΆ
β Don't bluff β If you don't know, say 'I haven't worked with that specific technology, but based on my understanding of X, I'd expect Y.' Honesty + reasoning is far better than confident wrong answers.
βΆ
β Don't skip basics β Senior candidates fail on basics because they assume basics won't be asked. equals()/hashCode() contract, volatile guarantees, and String immutability are asked at ALL levels.
βΆ
β Mention Java 21 features β In 2026, mentioning Virtual Threads, Records, Sealed Classes, and Pattern Matching signals that you keep up with modern Java β this differentiates you from candidates stuck on Java 8.
Practice Scenario Questions β Think Through These
Senior interviews often include open-ended scenario questions. Practice articulating structured answers to these:
Scenario 1: Your Spring Boot application has intermittent OutOfMemoryError in production. Walk through how you would diagnose and fix it.
Senior
β AnswerStep 1 β Gather evidence: Add -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/. Enable GC logging: -Xlog:gc*. Set up Micrometer metrics for heap_used and gc_pause_duration trending over time. Step 2 β Analyze heap dump: Use Eclipse MAT. Look at Dominator Tree β what retains most memory? Identify the largest retained heap. Step 3 β Common causes to check: Static Map/List accumulating data (caches without eviction). ThreadLocal leaks in thread pool threads. Connection/resource not closed (try-with-resources missed). Hibernate 1st-level cache in long-running batch (session.clear()). Too many class definitions (Metaspace OOM). Step 4 β Fix based on findings: Add Caffeine cache with expiry. Call ThreadLocal.remove() in finally. Add try-with-resources. Tune -Xmx if heap is genuinely too small. Step 5 β Verify fix: Load test, monitor heap over time β should plateau not grow continuously.
Scenario 2: You have a HashMap in a multi-threaded application and threads are seeing incorrect data. What could be wrong and how do you fix it?
Senior
β AnswerRoot cause: HashMap is NOT thread-safe. Under concurrent access (multiple threads reading and writing), HashMap can: Return incorrect values (stale cached values). Cause infinite loops (Java 6 resizing bug creates circular linked list in old HashMap). Throw ConcurrentModificationException. Corrupt internal structure silently. Solutions (in order of preference): ConcurrentHashMap: Best choice for concurrent key-value operations. Lock-free reads, bucket-level write locks. Does NOT allow null keys/values. Collections.synchronizedMap(): Simple wrapper β all methods synchronized. Must manually synchronize during iteration: synchronized(map) { for(Map.Entry e : map.entrySet()) { ... } }. Lower concurrency than ConcurrentHashMap. Immutable Map (if writes only happen at initialization): Collections.unmodifiableMap() or Map.copyOf() β safe to read from multiple threads. If a HashMap is ONLY read after initialization (no writes), it IS safe because reads don't modify structure. For complex scenarios: use ConcurrentHashMap with compute(), merge(), or computeIfAbsent() for atomic read-modify-write operations.
Scenario 3: A REST endpoint that calls 3 external microservices is taking 3 seconds. How do you optimize it?
Senior
β AnswerCurrent problem: 3 sequential external calls each taking ~1 second = 3 seconds total. Solution β Parallel calls with CompletableFuture: CompletableFuture<UserDTO> userFuture = CompletableFuture.supplyAsync(() -> userService.getUser(id)); CompletableFuture<OrderDTO> orderFuture = CompletableFuture.supplyAsync(() -> orderService.getOrders(id)); CompletableFuture<InventoryDTO> inventoryFuture = CompletableFuture.supplyAsync(() -> inventoryService.getStock(id)); CompletableFuture.allOf(userFuture, orderFuture, inventoryFuture).join(); β 3 calls in parallel, total time = slowest call (~1 second) instead of sum (3 seconds). With Spring WebFlux: Mono.zip(userMono, orderMono, inventoryMono) β reactive parallel composition. Additional optimizations: Caching: Cache user/inventory data that rarely changes (@Cacheable with Caffeine/Redis). Circuit breaker: Resilience4j @CircuitBreaker β if one service is down, return fallback immediately instead of waiting for timeout. Timeout: @TimeLimiter β don't wait more than 500ms per call. Fallback: return partial data if one service fails. With Java 21 Virtual Threads: each service call can be a separate virtual thread, blocking I/O doesn't waste OS threads.
Scenario 4: You need to design a leaderboard for a gaming app that shows top 100 players, updates in real-time, and handles 100,000 concurrent users. Which Java collections/data structures would you use?
Senior
β AnswerIn-process (single server): TreeMap<Integer, Set<String>> (score β set of players at that score) for sorted score β player mapping. Or TreeSet<Player> with custom Comparator (score desc, then name asc). NavigableMap operations: descendingKeySet(), subMap() for range queries. Adding/removing/updating: O(log n). Getting top 100: treeSet.stream().limit(100). Distributed (100,000 concurrent users): Redis Sorted Set (ZSET): ZADD leaderboard score player. ZREVRANGE leaderboard 0 99 WITHSCORES β top 100 in O(log n + 100). ZINCRBY leaderboard 50 player β atomic score increment. Redis handles 100K+ operations/second. Spring Data Redis: ZSetOperations<String, String> zOps = redisTemplate.opsForZSet(); zOps.add('leaderboard', playerId, score); zOps.reverseRangeWithScores('leaderboard', 0, 99); Caching: Cache top-100 result for 1-5 seconds with Caffeine β reduces Redis calls under extreme load. Real-time updates: WebSocket (Spring WebSocket + STOMP) to push leaderboard changes to connected clients. Or Server-Sent Events for simpler one-way push.
Scenario 5: How would you migrate a Java 8 application to Java 21?
Senior
β AnswerPhase 1 β Assessment: Inventory all Java 8 specific features and deprecated/removed APIs. Key breaking changes: javax.* β jakarta.* (if using Jakarta EE β Spring Boot 3 requires this). Nashorn JS engine removed (Java 11). PermGen removed (already gone in Java 8, but verify Metaspace settings). sun.* and com.sun.* internal APIs restricted by module system. finalize() deprecated. Phase 2 β Dependency update: Update Spring Boot to 3.x (requires Java 17+). Update all dependencies to Jakarta EE compatible versions. Run dependency-check for incompatible libraries. Phase 3 β Code changes: Replace javax.* imports with jakarta.* (IDE bulk replace or OpenRewrite tool). Replace Nashorn with GraalVM JS or Rhino. Replace Date/Calendar with java.time (LocalDate, etc.) β not required but highly recommended. Remove Thread.stop()/suspend() usage. Phase 4 β Module system: If needed, add --add-opens for reflection into JDK internals used by some frameworks. Or upgrade those frameworks. Phase 5 β Enable modern features: Add spring.threads.virtual.enabled=true for Virtual Threads. Refactor VOclasses to records. Replace if-else instanceof chains with pattern matching switch. Phase 6 β Test thoroughly: Run full test suite on Java 17 first (LTS before 21). Then upgrade to Java 21 LTS. Use jdeprscan to find deprecated API usage.
Scenario 6: A junior developer on your team says 'I use String concatenation in a loop to build a large report string β it works fine in testing.' What do you tell them?
Mid
β AnswerExplanation for the junior: The issue is performance at scale. String concatenation with + in a loop creates a NEW String object on every iteration because Strings are immutable. Example: String result = ''; for(int i = 0; i < 100000; i++) { result += data[i]; } β This creates 100,000 String objects. Each concatenation copies the entire existing string into a new one. Time complexity: O(nΒ²) β catastrophic for large reports. Memory: 100,000 intermediate Strings, massive GC pressure. Fix β use StringBuilder: StringBuilder sb = new StringBuilder(); for(int i = 0; i < 100000; i++) { sb.append(data[i]); } String result = sb.toString(); StringBuilder modifies the SAME internal buffer (amortized O(n) append). Only ONE final String created at the end. Performance improvement: For 100,000 elements, StringBuilder can be 1000x faster than String +. Benchmark it with JMH if the team needs convincing. Also mention: String.join(), Collectors.joining() in streams, or a StringJoiner for delimiter-separated values are even cleaner. Modern IDEs (IntelliJ) warn about String concatenation in loops with a 'Performance' inspection.
Scenario 7: Explain how you would implement retry logic with exponential backoff for a flaky external API call in Spring Boot.
Senior
β AnswerOption 1 β Resilience4j (recommended): @Retry(name = 'externalApi', fallbackMethod = 'fallback') public String callExternalApi(String input) { return restTemplate.getForObject(url, String.class); } Configuration in application.yml: resilience4j.retry.instances.externalApi: max-attempts: 3, wait-duration: 1s, enable-exponential-backoff: true, exponential-backoff-multiplier: 2 β waits 1s, then 2s, then 4s between retries. Exponential backoff with jitter: add random jitter to prevent thundering herd (all retries hitting the server simultaneously after an outage). RetryConfig.custom().intervalBiFunction(exponentialRandomBackoff(1000, 2, 0, TimeUnit.MILLISECONDS, 30000)). Option 2 β Spring Retry: @Retryable(retryFor = {HttpServerErrorException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2, random = true)) public String callApi() { ... } @Recover public String fallback(HttpServerErrorException e) { return 'fallback response'; }. Option 3 β CompletableFuture + manual retry loop (for full control): retry with delay using ScheduledExecutorService. Best practice: Retry only on transient errors (5xx, timeout), NOT on client errors (4xx). Always set a maximum retry count and maximum total time. Combine with Circuit Breaker β stop retrying when the service is clearly down. Log each retry attempt with delay and attempt number.
Scenario 8: Your team is debating whether to use ArrayList or LinkedList for a queue implementation. What is your recommendation and why?
Mid
β AnswerRecommendation: Use ArrayDeque, not ArrayList or LinkedList. Detailed reasoning: ArrayDeque vs LinkedList for queue: ArrayDeque is implemented as a circular resizable array. offer()/poll() (add/remove from ends) are O(1) amortized β same as LinkedList. ArrayDeque has NO per-element node overhead β LinkedList has 24 bytes overhead per node (object header + prev + next pointers). ArrayDeque has BETTER CACHE PERFORMANCE β contiguous memory; LinkedList nodes scattered across heap β cache misses. ArrayDeque does NOT allow null elements (safer queue contract). LinkedList allows null, which can cause confusion with poll() returning null for 'empty' vs 'contains null'. Benchmark: ArrayDeque typically 2-3x faster than LinkedList for queue operations on modern hardware due to cache locality. When LinkedList makes sense: When you need to use the queue as a doubly-linked list (addFirst/addLast/removeFirst/removeLast all efficiently), AND you have a use case that genuinely benefits from linked structure (rare). ArrayList for queue: NEVER β remove from front is O(n) due to element shifting. Summary: Queue/Deque implementations: ArrayDeque (fast, cache-friendly, recommended). Concurrent queue: ConcurrentLinkedQueue or ArrayBlockingQueue. Priority queue: PriorityQueue. Blocking: LinkedBlockingQueue.
Conclusion β Your Java Interview Roadmap
You now have 100 comprehensive Java interview questions and answers spanning every level from fresher to senior. But reading alone won't get you the job β you need to internalize these concepts, practice articulating them clearly, and connect them to your real project experience.
JVM, GC, Concurrency, Modern Java 17-21, System Design, Spring
All 100, plus system design scenarios
βΆ
π₯ Day 1β3 β Read and understand Q1βQ25 (Basics + OOP). Write code for every concept. Explain each answer out loud to yourself or a friend.
βΆ
π₯ Day 4β7 β Study Q26βQ58 (Collections + Exception Handling + Java 8). Build small programs using Streams, Optional, and Collections.
βΆ
π₯ Day 8β12 β Study Q59βQ100 (Concurrency + JVM + Advanced). Use VisualVM, jstack, and profilers at least once.
βΆ
π₯ Day 13β14 β Practice scenario questions. Mock interview with a colleague. Review your weak areas. Code the data structures from scratch.
βΆ
π‘ Key mindset β Interviewers are not looking for people who memorized a book. They want developers who UNDERSTAND deeply, can THINK through problems, and communicate clearly. Be that developer.
Java is not dying β Java is evolving. With Virtual Threads, Records, Pattern Matching, and continued enterprise adoption, Java in 2026 is more relevant and more powerful than ever. Master it, and you open doors to some of the best-paying engineering roles in the world. β
Frequently Asked Questions (FAQ)
JVM (Java Virtual Machine) executes bytecode and provides platform independence. JRE (Java Runtime Environment) = JVM + Java Class Libraries β used to run Java programs. JDK (Java Development Kit) = JRE + compiler (javac) + developer tools β used to write, compile, and run Java programs. Developers always install JDK.
== compares object references (memory addresses). .equals() compares the logical content/value of objects. For String, always use .equals() for content comparison. Classes like String and Integer override .equals() to compare content rather than reference.
ArrayList uses a dynamic array β O(1) random access, O(n) insertions in middle. LinkedList uses a doubly-linked list β O(n) random access, O(1) insertions at ends with iterator. ArrayList is the default choice; use LinkedList when you need Deque behavior or very frequent insertions at list ends.
Checked exceptions (extends Exception, not RuntimeException) β compiler forces you to handle or declare them. Examples: IOException, SQLException. Unchecked exceptions (extends RuntimeException) β compiler does not enforce handling. Examples: NullPointerException, ArrayIndexOutOfBoundsException. Errors β JVM-level failures, don't catch. Examples: OutOfMemoryError.
Virtual Threads (JEP 444, Java 21) are lightweight JVM-managed threads that don't block OS threads during blocking operations. When a virtual thread blocks (I/O, sleep), the carrier OS thread is released to run other virtual threads. This enables writing simple synchronous code that scales to millions of concurrent requests without reactive frameworks.