Skip to main content

Java Interview Questions (2026)

100 real interview questions with in-depth answers — 30 basic, 40 intermediate, 30 advanced. Updated April 2026.

Preparing for a Java Developer role?

The JRE (Java Runtime Environment) is what end-users need to run Java programs — it bundles the JVM plus the standard class libraries. The JDK (Java Development Kit) is a superset of the JRE and adds the compiler (`javac`), debugger, and development tools needed to build Java programs. The JVM (Java Virtual Machine) is the execution engine inside the JRE that loads bytecode, verifies it, JIT-compiles it to native code, and manages memory.

java
// javac (JDK tool) compiles source → bytecode
// java  (JVM launcher) runs the bytecode
javac Hello.java   // produces Hello.class
java  Hello        // JVM executes Hello.class

First the `javac` compiler converts `.java` source files into platform-independent `.class` bytecode files. At runtime the JVM class loader reads the bytecode, the bytecode verifier checks it for safety, and the JIT compiler (C1/C2) progressively compiles hot methods to native machine code. This "write once, run anywhere" model means the same `.class` files run on any platform that has a compatible JVM.

Java has eight primitives: `byte`, `short`, `int`, `long`, `float`, `double`, `char`, and `boolean`. They are stored directly on the stack (or inline in objects) and have no methods. Each has a corresponding wrapper class (`Integer`, `Double`, etc.) that boxes the value in a heap object and adds utility methods like `parseInt`. Wrappers are required when collections or generics demand `Object` references.

java
int x = 42;           // primitive, stack-allocated
Integer y = 42;       // boxed, heap object
System.out.println(Integer.toBinaryString(x)); // utility method

Autoboxing is the automatic conversion of a primitive to its wrapper (e.g. `int` → `Integer`) when the context requires an object. Unboxing is the reverse. Problems arise in tight loops (excessive heap allocations), when comparing boxed values with `==` (compares identity, not value), and when an unboxed `null` throws a `NullPointerException`.

java
List<Integer> list = new ArrayList<>();
list.add(5);          // autoboxes int → Integer
int val = list.get(0);// unboxes Integer → int
Integer a = 200, b = 200;
System.out.println(a == b); // false! cached only for -128..127

String immutability means once a `String` is created its char sequence never changes. This enables safe sharing across threads without synchronization, allows `String` to be cached in the string pool, and makes `String` suitable as `HashMap` keys (the hash never changes). Internally `String` stores characters in a `private final byte[]` (Java 9+ compact strings) that is never exposed.

java
String s = "hello";
s.toUpperCase();       // returns new String, s is unchanged
System.out.println(s); // still "hello"

The string pool (a.k.a. string constant pool) is a region of heap memory where the JVM caches string literals so identical literals share the same object. String literals are automatically pooled at compile time. `intern()` forces a runtime string into the pool (or returns the pooled copy if it already exists), enabling identity comparison with `==` and reducing memory for large sets of duplicate strings.
`StringBuilder` and `StringBuffer` both provide mutable character sequences via `append`, `insert`, `delete`, etc. The key difference is thread safety: `StringBuffer` methods are synchronized, making it safe for concurrent use but slower. `StringBuilder` is unsynchronized and significantly faster in single-threaded contexts, which is almost always the right choice. Both are preferred over repeated `+` string concatenation in loops. ```java StringBuilder sb = new StringBuilder(); for (int i = 0; i < 1000; i++) sb.append(i); String result = sb.toString(); ```
`==` compares object references (identity); it returns `true` only when both variables point to the exact same object in memory. `.equals()` compares character content. For string literals `==` may accidentally work due to the string pool, but for `new String(...)` or strings derived at runtime it will fail. ```java String a = new String("hi"); String b = new String("hi"); System.out.println(a == b); // false (different objects) System.out.println(a.equals(b)); // true (same content) ```
On a variable: the reference (or primitive value) cannot be reassigned after initialization, though the object it points to can still be mutated. On a method: the method cannot be overridden in subclasses. On a class: the class cannot be subclassed at all (e.g. `String`, `Integer`). Using `final` communicates design intent and can help the JIT inline method calls. ```java final int MAX = 100; // constant final List<String> list = new ArrayList<>(); list.add("ok"); // mutation allowed — reference is final, not content ```
`static` marks a member as belonging to the class rather than any instance. Static fields are shared across all instances; static methods can be called without an object. Static initializer blocks run once when the class is loaded. Overuse of `static` mutable state creates hidden global state and makes testing harder; prefer instance fields where possible.
`public` — accessible from anywhere. `protected` — accessible within the same package and in subclasses (including those in other packages). package-private (no keyword) — accessible only within the same package. `private` — accessible only within the declaring class. The principle of least privilege favors `private` by default, widening access only when needed.
A constructor initializes a new object; it has the same name as the class and no return type. `this` refers to the current object instance and is used to disambiguate instance fields from parameters with the same name, or to delegate to another constructor in the same class via `this(...)` (constructor chaining). Constructor chaining must be the first statement. ```java class Point { int x, y; Point(int x, int y) { this.x = x; this.y = y; } Point() { this(0, 0); } // delegates to above } ```
Overloading is compile-time polymorphism: multiple methods share a name but differ in parameter types or count. The compiler selects the correct version at compile time. Overriding is runtime polymorphism: a subclass provides a new implementation for an inherited method with the same signature. The JVM dispatches to the subclass version via virtual dispatch at runtime. Overriding requires `@Override` (recommended), and the method must not be `static`, `private`, or `final`.
`super` references the parent class. In a constructor, `super(...)` calls the parent constructor and must be the first statement (if not using `this(...)`). In a method body, `super.methodName()` explicitly invokes the overridden parent version, which is useful when extending behavior rather than replacing it.
An abstract class can have constructors, instance state, concrete methods, and abstract methods; a class can extend only one abstract class. An interface historically only had abstract methods, but since Java 8 can also have `default` and `static` methods, and since Java 9 `private` methods. A class can implement multiple interfaces. Use an abstract class when sharing implementation state; use interfaces to define contracts and enable multiple inheritance of type. ```java abstract class Animal { abstract void speak(); void breathe() { /* impl */ } } interface Flyable { default void fly() { /* default impl */ } } ```
`instanceof` tests whether an object is an instance of a type and returns a boolean. Java 16 introduced pattern matching for `instanceof`: the type check and cast can be combined in one expression, binding a new variable that is already cast in the true branch, eliminating explicit casts. ```java // Before Java 16 if (obj instanceof String) { String s = (String) obj; ... } // Java 16+ if (obj instanceof String s) { System.out.println(s.length()); } ```
Widening conversion (e.g. `int` → `long`, `float` → `double`) is implicit and safe because the target type can represent all values of the source. Narrowing conversion (e.g. `double` → `int`, `long` → `byte`) requires an explicit cast and may lose precision or truncate value. For reference types, widening (upcasting) is always safe; narrowing (downcasting) requires an explicit cast and will throw `ClassCastException` at runtime if the object is not actually of that type. ```java double d = 3.99; int i = (int) d; // truncates → 3, not 4 ```
Arrays are fixed-size, zero-indexed objects that hold elements of a single type. They are objects on the heap; `length` is a field, not a method. Multidimensional arrays are arrays of arrays. Arrays do not resize; use `ArrayList` or `Arrays.copyOf` when dynamic sizing is needed. ```java int[] arr = new int[5]; // all zeros String[] names = {"Alice", "Bob"}; int[][] matrix = new int[3][3]; ```
`ArrayList` is backed by a dynamic array: O(1) random access, O(n) insertion/deletion in the middle, amortized O(1) append. `LinkedList` is a doubly-linked list: O(n) random access, O(1) insertion/deletion given an iterator position, higher per-element memory due to node overhead. In practice `ArrayList` outperforms `LinkedList` for most workloads because of CPU cache locality. `LinkedList` is better when you frequently add/remove at the head or use it as a `Deque`.
`HashMap` stores entries in a hash table: average O(1) get/put, no ordering guarantee. `TreeMap` stores entries in a Red-Black tree sorted by key: O(log n) operations but maintains natural order, enabling `firstKey()`, `lastKey()`, `subMap()` etc. Choose `HashMap` for maximum throughput; use `TreeMap` when you need sorted iteration or range queries.
Checked exceptions (subclasses of `Exception` but not `RuntimeException`) must be either caught or declared with `throws`; the compiler enforces this. Unchecked exceptions (subclasses of `RuntimeException` or `Error`) do not need to be declared. Checked exceptions model recoverable error conditions (e.g. `IOException`); unchecked model programming bugs (e.g. `NullPointerException`, `IllegalArgumentException`). Many modern frameworks avoid checked exceptions because they pollute API signatures.
`try` wraps the code that may throw; `catch` handles specific exception types in order; `finally` always runs after the try/catch block — even if an exception is thrown and not caught, or if a `return` statement is hit inside `try` or `catch`. The rare case where `finally` does NOT execute: `System.exit()` is called, or the JVM crashes, or the thread is forcibly killed.
For checked exceptions, the method signature uses `throws ExceptionType`. Callers must then either catch it or propagate it with their own `throws`. Unchecked exceptions do not require a `throws` declaration though it is sometimes added for documentation. ```java public void readFile(String path) throws IOException { Files.readAllBytes(Path.of(path)); } ```
Generics allow classes, interfaces, and methods to be parameterized by type, enabling type-safe containers without casting. At compile time the type parameter is checked; at runtime it is erased (type erasure). They prevent `ClassCastException` at runtime and remove boilerplate casts. ```java List<String> list = new ArrayList<>(); list.add("hello"); String s = list.get(0); // no cast needed ```
The enhanced for loop (`for (Type x : collection)`) iterates over any object implementing `Iterable<T>` or over arrays. It translates to an `Iterator` call under the hood. It is simpler but does not expose the index; use a classic `for` loop when you need the index or need to remove elements during iteration.
Varargs (`Type... name`) allow a method to accept zero or more arguments of a type without the caller constructing an array. The JVM creates an array and passes it. Varargs must be the last parameter. They simplify APIs like `System.out.printf(String format, Object... args)` but can interact badly with generics due to heap pollution warnings. ```java int sum(int... nums) { int total = 0; for (int n : nums) total += n; return total; } sum(1, 2, 3, 4); // OK ```
Enums are type-safe constants that are full-fledged classes. They can have fields, constructors, and methods, implement interfaces, and each constant can override methods. The JVM guarantees each constant is a singleton, so `==` comparison is safe. Enums also work in `switch` expressions and support `EnumSet`/`EnumMap` for efficient collection operations. ```java enum Planet { MERCURY(3.303e+23), EARTH(5.976e+24); private final double mass; Planet(double mass) { this.mass = mass; } } ```
Classic null safety involves null checks before dereferencing (`if (x != null)`), using `Objects.requireNonNull()` to fail fast at method entry, defensive copying for collections, and annotating with `@NonNull`/`@Nullable` for tooling. The core rule: never return null from a method that returns a collection — return empty collections instead. Java 8's `Optional` provides a functional alternative to null returns.
Every Java class inherits from `Object`, which provides: `equals()`, `hashCode()`, `toString()`, `clone()`, `getClass()`, `finalize()`, `wait()`, `notify()`, `notifyAll()`. The critical contract: if `a.equals(b)` is true, then `a.hashCode()` must equal `b.hashCode()`. Violating this breaks `HashMap`, `HashSet`, and any hash-based structure because equal objects must land in the same bucket. ```java @Override public boolean equals(Object o) { ... } @Override public int hashCode() { return Objects.hash(field1, field2); } ```
`throw` is a statement that actually raises an exception object at runtime: `throw new IllegalArgumentException("bad input")`. `throws` is a declaration in a method signature listing checked exceptions the method may propagate to callers. `throw` is imperative; `throws` is declarative.
At the root is `Iterable<T>`, then `Collection<T>` which splits into `List` (ordered, allows duplicates), `Set` (no duplicates), and `Queue`/`Deque`. `Map<K,V>` is separate (not a `Collection`). Key implementations: `ArrayList`, `LinkedList` (List); `HashSet`, `LinkedHashSet`, `TreeSet` (Set); `HashMap`, `LinkedHashMap`, `TreeMap` (Map); `ArrayDeque`, `PriorityQueue` (Queue). Java 21 added the `SequencedCollection` / `SequencedMap` interfaces to uniformly access first/last elements across ordered collections. ```java SequencedCollection<String> sc = new ArrayList<>(List.of("a","b","c")); sc.addFirst("z"); // Java 21 sc.getFirst(); // "z" ```
`HashMap` is not thread-safe — concurrent modification causes data corruption or infinite loops. `ConcurrentHashMap` divides the table into segments (pre-Java 8) or uses CAS + synchronized on individual buckets (Java 8+), allowing multiple threads to read and write concurrently without global locking. It never throws `ConcurrentModificationException` and allows null-free `putIfAbsent`, `compute`, and `merge` operations. Read operations are generally lock-free. Unlike `Collections.synchronizedMap()`, it does not lock the entire map for each operation.
`Comparable<T>` defines the "natural ordering" of a class via `compareTo(T o)` — the class itself implements it, and it establishes a single canonical order. `Comparator<T>` is an external strategy object that defines an ordering independent of the class, useful for multiple sort orderings or third-party classes. `List.sort(comparator)` and `Collections.sort` accept both. ```java // Comparable: natural order class Employee implements Comparable<Employee> { public int compareTo(Employee e) { return this.id - e.id; } } // Comparator: ad-hoc list.sort(Comparator.comparing(Employee::getName).thenComparing(Employee::getId)); ```
An `Iterator<T>` provides `hasNext()` and `next()`, decoupling traversal from collection internals. Fail-fast iterators (used by `ArrayList`, `HashMap`) track a `modCount`; if the collection is structurally modified during iteration (except through the iterator itself), they throw `ConcurrentModificationException`. Fail-safe iterators (used by `ConcurrentHashMap`, `CopyOnWriteArrayList`) operate on a snapshot or tolerate modification without throwing, but may not reflect the latest state.
Streams provide a declarative pipeline over data. Intermediate operations (`filter`, `map`, `flatMap`, `sorted`, `distinct`, `limit`) are lazy — they build a pipeline but do not execute until a terminal operation is called. Terminal operations (`collect`, `reduce`, `forEach`, `count`, `findFirst`) trigger execution. `reduce` folds elements to a single value; `collect` accumulates into a container via a `Collector`. ```java List<String> names = employees.stream() .filter(e -> e.getSalary() > 50_000) .map(Employee::getName) .sorted() .collect(Collectors.toList()); ```
`Optional<T>` is a container that either holds a non-null value or is empty, designed to make absent values explicit in return types instead of returning `null`. Use it for method return types where absence is a meaningful result. Avoid using it as a field type, constructor/method parameter, or in collections. Key methods: `isPresent()`, `ifPresent(Consumer)`, `orElse(T)`, `orElseGet(Supplier)`, `orElseThrow()`, `map()`, `flatMap()`, `filter()`. ```java Optional<User> user = repository.findById(id); String name = user.map(User::getName).orElse("Unknown"); ```
`Predicate<T>` takes a T and returns boolean (`test`). `Function<T,R>` maps T → R (`apply`). `Consumer<T>` takes T, returns void (`accept`). `Supplier<T>` takes nothing, returns T (`get`). `BiFunction<T,U,R>` maps (T,U) → R. `UnaryOperator<T>` is a special `Function<T,T>`. These are the building blocks of the Stream API and can be composed: `Predicate.and()`, `Function.andThen()`, `Function.compose()`. ```java Predicate<String> nonEmpty = s -> !s.isEmpty(); Function<String, Integer> len = String::length; Function<String, Integer> composed = len.compose(String::trim); ```
A lambda is a concise representation of a functional interface instance. Unlike anonymous inner classes, lambdas do not create a new scope — `this` inside a lambda refers to the enclosing class, not the lambda itself. Lambdas also do not create a dedicated `.class` file; the JVM generates them at runtime via `invokedynamic` and `LambdaMetafactory`, which is faster and more memory-efficient. Lambdas capture effectively-final variables from their enclosing scope. ```java // Anonymous inner class Runnable r1 = new Runnable() { public void run() { System.out.println("hi"); } }; // Lambda Runnable r2 = () -> System.out.println("hi"); ```
Method references are a shorthand for lambdas that simply call an existing method. The four kinds: (1) static: `ClassName::staticMethod` → `(args) -> ClassName.staticMethod(args)`; (2) instance on particular object: `instance::method` → `(args) -> instance.method(args)`; (3) instance on arbitrary object of a type: `ClassName::instanceMethod` → `(obj, args) -> obj.method(args)`; (4) constructor: `ClassName::new` → `(args) -> new ClassName(args)`. ```java list.stream().map(String::toUpperCase).forEach(System.out::println); ```
`? extends T` (upper bounded wildcard) means "some unknown subtype of T" — you can read T values but not add (producer). `? super T` (lower bounded wildcard) means "some unknown supertype of T" — you can add T values but not safely read (consumer). This is the PECS rule: Producer Extends, Consumer Super. Bounded type parameters on methods (`<T extends Comparable<T>>`) constrain what types can be substituted. ```java void printAll(List<? extends Number> list) { // read-only list.forEach(System.out::println); } void addNumbers(List<? super Integer> list) { // write-ok list.add(42); } ```
At compile time the Java compiler removes all generic type parameters, replacing them with their bounds (or `Object` if unbounded) and inserting casts where needed. This is type erasure. At runtime, `List<String>` and `List<Integer>` are both just `List`. Consequences: you cannot use `instanceof` with a parameterized type, cannot create generic arrays (`new T[10]` is illegal), and cannot overload methods that differ only in generic type parameters. Use `Class<T>` tokens or `TypeToken` patterns when runtime type info is needed.
A `record` is a concise data carrier class. The compiler auto-generates a canonical constructor, `private final` fields, accessors named after the components, `equals()`, `hashCode()`, and `toString()`. Records are implicitly `final` and cannot extend other classes (but can implement interfaces). Use them for immutable value types like DTOs, coordinates, or configuration snapshots. Custom logic can be added via compact constructors. ```java record Point(int x, int y) {} Point p = new Point(3, 4); p.x(); // 3 — accessor, not field ```
A `sealed` class or interface restricts which classes may extend or implement it, using the `permits` clause. The permitted subclasses must be in the same compilation unit (or package). Sealed types improve domain modeling (e.g. an AST node that can only be `Literal`, `BinaryOp`, or `UnaryOp`) and enable exhaustive `switch` expressions — the compiler can verify all cases are handled. ```java sealed interface Shape permits Circle, Rectangle, Triangle {} final class Circle implements Shape { double radius; } final class Rectangle implements Shape { double width, height; } ```
Switch expressions (stable in Java 14) can return a value, use arrow labels (`case X ->`) that avoid fall-through, and can use `yield` to return from a multi-statement branch. They also enable exhaustiveness checking with sealed types and enums, making them safer than the old fall-through switch statement. Text blocks (Java 15) and pattern matching in switch (Java 21) build on top of this. ```java String label = switch (day) { case MONDAY, TUESDAY -> "early week"; case FRIDAY -> "end of week"; default -> "mid week"; }; ```
Text blocks (`"""..."""`) are multi-line string literals that preserve formatting and avoid escaping. The opening `"""` must be followed by a newline. Common leading whitespace is stripped based on the least-indented content line. They are ideal for embedding JSON, SQL, HTML, or XML strings. ```java String json = """ { "name": "Alice", "age": 30 } """; ```
`var` (Java 10+) triggers local variable type inference — the compiler infers the type from the initializer. It can only be used for local variables, for-loop variables, and try-with-resources variables. It cannot be used for method parameters, return types, or fields. It does not make Java dynamically typed; the inferred type is fixed at compile time. Avoid `var` when the inferred type is not obvious from context, as it can hurt readability.
Intermediate operations (e.g. `filter`, `map`, `sorted`) are lazy and return a new `Stream`. They build a pipeline without processing any data until a terminal operation is invoked. Terminal operations (`collect`, `reduce`, `forEach`, `count`, `findAny`) consume the stream. Parallel streams split the source across the common ForkJoinPool and process chunks concurrently; they can speed up CPU-intensive work on large data but add overhead for small datasets and are problematic when operations have side effects or non-associative reductions.
`flatMap` maps each element to a stream and then flattens all those streams into a single stream. It is the "map then flatten" pattern, useful when each element naturally expands to zero or more elements. ```java List<List<String>> nested = List.of(List.of("a","b"), List.of("c","d")); List<String> flat = nested.stream() .flatMap(Collection::stream) .collect(Collectors.toList()); // ["a","b","c","d"] ```
`Collectors` provides: `toList()`, `toSet()`, `toUnmodifiableList()`, `joining(delimiter)`, `groupingBy(classifier)`, `partitioningBy(predicate)`, `counting()`, `summingInt()`, `toMap()`, and `teeing()` (Java 12). A custom collector implements `Collector<T,A,R>` with `supplier` (creates mutable container), `accumulator` (folds element into container), `combiner` (merges parallel containers), `finisher` (transforms container to result), and `characteristics`. ```java Collector<String, StringBuilder, String> joiner = Collector.of(StringBuilder::new, StringBuilder::append, StringBuilder::append, StringBuilder::toString); ```
Optional chains allow composing multiple potentially-absent steps without explicit null checks. `map` transforms the value if present; `flatMap` is used when the mapper itself returns an `Optional`. This avoids nested `if (x != null)` blocks and makes the absent case explicit in the type system. The downside is that wrapping/unwrapping adds overhead — for hot paths, null checks may still be preferable. ```java Optional.ofNullable(order) .map(Order::getCustomer) .map(Customer::getAddress) .map(Address::getCity) .orElse("Unknown city"); ```
`synchronized` is built into the JVM (intrinsic lock on an object); it is simple but inflexible — you cannot try to acquire with a timeout, interrupt a waiting thread, or use multiple conditions. `ReentrantLock` (java.util.concurrent.locks) supports `tryLock(timeout)`, `lockInterruptibly()`, multiple `Condition` objects for fine-grained signaling, and fairness policies. Always unlock in a `finally` block to prevent deadlocks when using explicit locks.
`volatile` guarantees visibility: writes to a volatile variable are immediately flushed to main memory and reads always load from main memory, preventing threads from seeing stale cached values. It also establishes a happens-before relationship around reads and writes. However, `volatile` does not make compound operations (check-then-act, increment) atomic — use `AtomicInteger` or `synchronized` for that. ```java private volatile boolean stopRequested = false; // thread A writes: stopRequested = true; // thread B reads it correctly without synchronized ```
`java.util.concurrent.atomic` provides lock-free thread-safe wrappers: `AtomicInteger`, `AtomicLong`, `AtomicBoolean`, `AtomicReference`, `AtomicStampedReference`, `LongAdder` (better throughput under high contention for counters). They use CAS (compare-and-swap) hardware instructions. Use them for single-variable counters, flags, or reference swaps. `LongAdder` is preferable over `AtomicLong` for frequently updated counters because it reduces contention by maintaining multiple cells.
`ThreadLocal<T>` gives each thread its own independent copy of a variable, avoiding synchronization. Common uses: per-thread `SimpleDateFormat`, database connections, or transaction contexts in web frameworks. The critical risk is memory leaks in thread pools (like servlet containers): if `remove()` is never called, the value lives on the thread even after the task completes, potentially leaking large objects across requests.
`ExecutorService` decouples task submission from execution. `Executors.newFixedThreadPool(n)` maintains n threads; `newCachedThreadPool()` grows unboundedly and recycles idle threads (dangerous in practice); `newSingleThreadExecutor()` serializes tasks; `newScheduledThreadPool(n)` supports delayed/periodic tasks. For production use prefer `ThreadPoolExecutor` constructed directly to control core/max pool size, keep-alive, queue type, and rejection policy. Always shut down with `shutdown()` / `awaitTermination()`.
`CompletableFuture<T>` represents an asynchronous computation. `thenApply(Function)` transforms the result synchronously (like `Stream.map`). `thenCompose(Function)` chains a function that itself returns a `CompletableFuture` (flat-map semantics — avoids `CompletableFuture<CompletableFuture<T>>`). `allOf(cf1, cf2, ...)` returns a `CompletableFuture<Void>` that completes when all supplied futures complete, enabling fan-out patterns. Use `exceptionally()` or `handle()` for error recovery. ```java CompletableFuture.supplyAsync(() -> fetchUser(id)) .thenCompose(user -> fetchOrders(user.id())) .thenApply(orders -> orders.stream().count()) .exceptionally(ex -> -1L); ```
The Java Memory Model (JMM) defines how threads interact through memory, specifying when writes by one thread are guaranteed to be visible to reads by another. A happens-before (HB) relationship means all actions in the first set are guaranteed visible to the second. Key HB rules: monitor unlock HB monitor lock; volatile write HB volatile read; thread start HB all actions in that thread; `Thread.join()` HB code after the join. Without HB, the compiler and CPU are free to reorder operations in unexpected ways.
A deadlock requires four conditions (Coffman): mutual exclusion, hold-and-wait, no preemption, circular wait. Detection: JVM thread dumps (`jstack`, VisualVM, `ThreadMXBean.findDeadlockedThreads()`) show `BLOCKED` threads with a cycle. Prevention strategies: always acquire locks in a consistent global order, use `tryLock(timeout)` with `ReentrantLock`, prefer higher-level constructs (`ConcurrentHashMap`, `BlockingQueue`), or use lock-free data structures. Structured concurrency (Java 21) makes ownership clear and prevents many deadlock patterns.
`@Component` is the generic stereotype annotation; the others are specializations: `@Service` marks business logic, `@Repository` marks DAO classes (and enables exception translation). All four cause Spring to register the class as a bean in the application context. `@Bean` on a method inside a `@Configuration` class manually defines a bean — useful for third-party classes you cannot annotate. Spring resolves dependencies via `@Autowired` (constructor injection preferred), autowiring by type and then by name.
Eager loading (`FetchType.EAGER`) fetches associated entities immediately when the parent is loaded, which can cause N+1 query issues or load too much data. Lazy loading (`FetchType.LAZY`, default for collections) defers fetching until the association is first accessed, but accessing it outside an open `EntityManager`/`Session` causes `LazyInitializationException`. Solutions: use JOIN FETCH in JPQL/HQL, `@EntityGraph`, open-session-in-view (anti-pattern in production), or DTOs with projections. ```java @OneToMany(fetch = FetchType.LAZY) private List<Order> orders; // loaded only when accessed ```
The N+1 problem occurs when loading N parent entities and then issuing one additional query per entity to load a lazy association — 1 query for parents + N queries for children. Solutions: (1) JPQL JOIN FETCH: `SELECT u FROM User u JOIN FETCH u.orders`; (2) `@EntityGraph` on repository method; (3) batch fetching (`@BatchSize`); (4) projections/DTOs that fetch only needed columns. Avoid the open-session-in-view pattern in production as it masks the problem rather than solving it.
`reduce(identity, BinaryOperator)` folds all elements into one value starting from `identity`. The variant without identity returns `Optional<T>` (empty stream yields `Optional.empty()`). For parallel streams, the `BinaryOperator` must be associative and the identity element must be a true identity for the operator (applying it must not change the result). ```java int sum = IntStream.rangeClosed(1, 10) .reduce(0, Integer::sum); // 55 Optional<Integer> max = Stream.of(3,1,4,1,5) .reduce(Integer::max); // Optional[5] ```
A functional interface has exactly one abstract method (SAM — Single Abstract Method) and can therefore be used as the target type for a lambda or method reference. `@FunctionalInterface` is an optional annotation that causes a compile-time error if the interface has more or fewer than one abstract method, serving as documentation and a safety net. Default and static methods do not count against the SAM requirement.
`Runnable` executes code but returns void and cannot throw checked exceptions. `Callable<V>` is like `Runnable` but returns a value of type V and can throw checked exceptions. `Future<V>` represents the result of an asynchronous computation submitted to an `ExecutorService`; it provides `get()` (blocking), `cancel()`, `isDone()`, and `isCancelled()`. `CompletableFuture<V>` (Java 8+) extends `Future` with non-blocking callbacks and composition.
`LinkedHashMap` is a `HashMap` that additionally maintains a doubly-linked list through its entries, preserving insertion order (or optionally access order for LRU caches). Iteration order is predictable, unlike `HashMap`. The `removeEldestEntry()` hook enables a simple bounded LRU cache. Overhead is slightly higher than `HashMap` due to the extra pointers. ```java Map<String, Integer> lru = new LinkedHashMap<>(16, 0.75f, true) { protected boolean removeEldestEntry(Map.Entry e) { return size() > 100; } }; ```
`HashSet` offers O(1) average add/contains/remove but no ordering. `LinkedHashSet` maintains insertion order with nearly the same performance. `TreeSet` is a sorted set (Red-Black tree) with O(log n) operations and supports `headSet()`, `tailSet()`, `floor()`, `ceiling()` navigation. Choose `HashSet` by default; `TreeSet` when you need sorted iteration or range operations; `LinkedHashSet` when iteration order must match insertion order.
An object is eligible for GC when it is no longer reachable from any GC root. GC roots include local variables on active stack frames, static fields, JNI references, and active threads. The GC traces the object graph from roots; anything not reachable is garbage. Weak (`WeakReference`), soft (`SoftReference`), and phantom references let you hold references without preventing collection. Common leak patterns: static collections that grow unboundedly, listeners never unregistered, and `ThreadLocal` values not removed in thread pools.
Try-with-resources (`try (Resource r = new Resource()) { ... }`) automatically calls `r.close()` at the end of the block, even if an exception is thrown. The resource must implement `AutoCloseable`. Multiple resources can be declared and are closed in reverse order. If both the body and `close()` throw, the body exception is propagated and the close exception is suppressed (accessible via `Throwable.getSuppressed()`). ```java try (InputStream is = new FileInputStream(file); BufferedReader br = new BufferedReader(new InputStreamReader(is))) { return br.readLine(); } ```
Options: (1) eager init — static final field, initialized when class is loaded, always safe; (2) double-checked locking with `volatile` (Java 5+); (3) initialization-on-demand holder idiom — inner static class holds the singleton, leveraging class-loading guarantee; (4) enum singleton (Bloch, "Effective Java") — thread-safe, serialization-safe, reflection-safe. The initialization-on-demand holder is generally the cleanest. ```java class Singleton { private Singleton() {} private static class Holder { static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } } ```
`String.format(format, args)` uses `printf`-style specifiers (`%s`, `%d`, `%f`) and is locale-aware through `Locale` overload. `MessageFormat` uses numbered placeholders (`{0}`, `{1}`) with optional format types and is designed for internationalization — it handles pluralization patterns and locale-specific number/date formatting. For simple English strings `String.format` is cleaner; for i18n or complex plural rules use `MessageFormat` or a proper i18n library.
The JVM divides memory into: (1) Heap — all object instances live here; divided into young gen (Eden + two Survivor spaces) and old gen (tenured). GC primarily targets the heap. (2) Metaspace (replaced PermGen in Java 8) — class metadata, method bytecode, constant pools; grows on native memory, bounded by `-XX:MaxMetaspaceSize`. (3) Stack — per-thread, holds frames (local variables, operand stack, return address) for each method call; bounded by `-Xss`. (4) Off-heap / native memory — used by NIO DirectByteBuffer, memory-mapped files, and JVM internals. GC roots include: local variables in active frames, static fields, JNI global references, and synchronized monitors.
Serial GC: single-threaded mark-compact; only useful for tiny heaps or constrained environments. Parallel GC (default pre-Java 9): multi-threaded throughput-optimized; stop-the-world pauses during old-gen collection. G1 GC (default from Java 9): divides heap into equal-size regions; mostly-concurrent marking; targets pause-time goals via region prioritization; handles large heaps well. ZGC (production from Java 15): fully concurrent collector — all phases except a brief root scan are concurrent; sub-millisecond pauses regardless of heap size; uses colored pointers and load barriers. Shenandoah: similar to ZGC, developed by Red Hat; concurrent compaction using Brooks pointers. Use G1 for general-purpose; ZGC/Shenandoah for latency-sensitive applications.
Heap sizing: `-Xms` (initial heap), `-Xmx` (max heap), `-Xmn` (young gen size). G1 tuning: `-XX:+UseG1GC`, `-XX:MaxGCPauseMillis=200` (pause goal), `-XX:G1HeapRegionSize=16m`, `-XX:G1ReservePercent=10`. ZGC: `-XX:+UseZGC`, `-XX:SoftMaxHeapSize`. GC logging: `-Xlog:gc*:file=gc.log:time,uptime,level,tags`. Metaspace: `-XX:MaxMetaspaceSize=256m`. Ergonomics: `-XX:+UseContainerSupport` (critical in Docker to honor cgroup limits). GC tuning is empirical — baseline with JMH/production load, then measure with async-profiler or GC logs before changing flags.
Class loading uses a three-phase delegation model: the bootstrap loader (loads rt.jar/core), extension/platform loader, and application (classpath) loader. By default each loader delegates to its parent before attempting to load itself (parent-first). Custom ClassLoaders are needed for: hot-reloading classes at runtime (IDEs, app servers), loading classes from non-standard sources (databases, encrypted JARs), sandboxing/isolation (OSGi, plugin systems), or injecting bytecode transformations. Override `findClass()` (not `loadClass()`) to preserve the delegation model, and return a new `Class<?>` from `defineClass()` with the byte array.
A Java agent is a JAR with a `premain(String args, Instrumentation inst)` method, declared in `MANIFEST.MF` as `Premain-Class`. It is loaded by the JVM before the main class via `-javaagent:agent.jar`. The `Instrumentation` interface lets the agent register a `ClassFileTransformer` that intercepts class loading and can rewrite bytecode. Libraries like ASM, Javassist, or Byte Buddy are used to generate or modify bytecode. APM tools (New Relic, Datadog, OpenTelemetry Java agent) use this mechanism to auto-instrument frameworks. `agentmain` + `VirtualMachine.attach()` enables late attachment to a running JVM.
Reflection (`java.lang.reflect`) lets code inspect and invoke classes, methods, fields, and constructors at runtime without compile-time knowledge. `Class.forName()` loads a class; `getDeclaredMethods()` lists methods; `Method.invoke()` calls a method. Costs: reflection bypasses normal JIT inlining, involves security checks per invocation, and accesses require `setAccessible(true)` which may be restricted by the module system in Java 17+. Modern frameworks mitigate this with generated accessor classes (Byte Buddy), `MethodHandles` (faster, JIT-friendly), or annotation processing at compile time.
JDK dynamic proxies create a proxy at runtime that implements specified interfaces; they can only proxy interfaces. cglib (Code Generation Library) subclasses the target class and overrides methods, proxying concrete classes without interfaces. Spring AOP defaults to JDK proxies when the target implements an interface and cglib otherwise. With Spring Boot and `proxyTargetClass=true` (default when using `@SpringBootApplication`), cglib is always used. Both proxy types intercept method calls to apply advice (e.g. `@Transactional`, `@Cacheable`). Final methods and classes cannot be proxied by cglib; that is why Spring beans should not be final (use `@Configuration(proxyBeanMethods=false)` with care).
Introduced in Java 9, JPMS (`module-info.java`) adds a first-class module concept on top of packages. A module declares what packages it exports (`exports`), what modules it requires (`requires`), what services it provides/uses, and what packages are opened for reflection (`opens`). It solves classpath hell (accidental access to internal APIs, duplicate classes across JARs), enables reliable configuration, and allows the JVM to be slimmed down via `jlink` (only include needed modules). Java 17 enforces strong encapsulation: `--add-opens` is needed to access JDK internals, which breaks many pre-JPMS libraries.
Virtual threads are JVM-managed lightweight threads that can be created in millions without exhausting OS resources. They are scheduled by the JVM onto a small pool of carrier (platform) threads. When a virtual thread blocks (e.g. on I/O), the carrier thread is released and picks up another virtual thread — this is unmounting. Unlike reactive/async code, virtual threads let you write blocking code that scales to high concurrency. Key: avoid synchronized blocks with blocking I/O (use `ReentrantLock` instead) as they pin the carrier thread. Create with `Thread.ofVirtual().start(runnable)` or `Executors.newVirtualThreadPerTaskExecutor()`. ```java try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 10_000).forEach(i -> executor.submit(() -> handleRequest(i))); } ```
Structured concurrency (`StructuredTaskScope`) ensures that subtasks forked within a scope are completed or cancelled before the scope exits, creating a parent-child lifetime relationship for concurrent tasks. This eliminates task leaks, makes cancellation propagate correctly, and simplifies error handling. The two built-in policies: `ShutdownOnFailure` (cancel remaining tasks if any fails) and `ShutdownOnSuccess` (cancel remaining tasks when first succeeds — useful for racing calls). It pairs naturally with virtual threads. ```java try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Future<User> user = scope.fork(() -> fetchUser(id)); Future<Cart> cart = scope.fork(() -> fetchCart(id)); scope.join().throwIfFailed(); return new Page(user.resultNow(), cart.resultNow()); } ```
The FFM API (`java.lang.foreign`) provides safe, efficient access to native code and off-heap memory without JNI's verbosity or `sun.misc.Unsafe`'s unsafety. `MemorySegment` represents an off-heap memory region with lifetime management (allocated in an `Arena` that can be auto-closed). `MethodHandles.lookup()` with `Linker.nativeLinker()` directly calls C functions. The API is bounds-checked, lifecycle-safe, and JIT-friendly. It enables zero-copy I/O, native library interop, and is the foundation of Panama project improvements.
Record patterns extend pattern matching to destructure record components inline, eliminating manual accessor calls. Combined with `switch` expressions and sealed types, they enable exhaustive, concise deconstruction. Generic record patterns with type inference are also supported. ```java sealed interface Shape permits Circle, Rectangle {} record Circle(double radius) implements Shape {} record Rectangle(double w, double h) implements Shape {} double area = switch (shape) { case Circle(var r) -> Math.PI * r * r; case Rectangle(var w, var h) -> w * h; }; ```
Java 21 added three new interfaces to the Collections Framework: `SequencedCollection<E>` (adds `getFirst()`, `getLast()`, `addFirst()`, `addLast()`, `removeFirst()`, `removeLast()`, `reversed()`), `SequencedSet<E>`, and `SequencedMap<K,V>` (adds `firstEntry()`, `lastEntry()`, `reversed()`). These interfaces are retrofitted onto existing types: `List`, `Deque`, `LinkedHashSet`, `LinkedHashMap`, `SortedSet`, `SortedMap`, etc. They provide a uniform API for accessing ordered-collection endpoints without writing type-specific code.
HotSpot uses tiered compilation (default since Java 8). Tier 0: interpreter — collects profiling data. Tier 1-2: C1 (client compiler) — fast compilation with minimal optimization, used early. Tier 3: C1 with full profiling. Tier 4: C2 (server compiler) — aggressive optimizations (inlining, loop unrolling, escape analysis, scalar replacement) based on collected profile data. The JVM makes speculative assumptions (e.g. "this method has only one implementation"); if wrong, it deoptimizes back to the interpreter. `jstat -compiler <pid>` and `-XX:+PrintCompilation` reveal JIT activity.
Escape analysis determines whether an object allocated in a method can be accessed outside that method (i.e. "escapes" to the heap). If an object does not escape: (1) stack allocation — the JVM allocates it on the thread stack instead of the heap, reducing GC pressure; (2) scalar replacement — the object is decomposed into its primitive fields and those are kept in registers, eliminating the object altogether. This is why short-lived value objects (e.g. iterators, Optional wrappers) often have near-zero allocation cost in optimized code. ``` -XX:+DoEscapeAnalysis # enabled by default in JDK 8+ -XX:+EliminateAllocations # enable scalar replacement ```
Enabled with `-XX:+UseStringDeduplication` (G1 only), the GC identifies `String` objects on the heap that have identical char arrays and replaces all but one with a shared reference to the same backing array. This happens concurrently during GC pauses. It is useful for applications that hold large numbers of duplicate strings (e.g. XML/JSON parsing results). It deduplicates the backing `byte[]` but does NOT deduplicate the `String` wrapper objects themselves, so references remain distinct. Monitor with `-XX:+PrintStringDeduplicationStatistics`.
`ByteBuffer.allocateDirect(n)` allocates memory outside the Java heap (native memory), reducing GC overhead for large buffers used in I/O. The buffer is cleaned by `Cleaner` when the `DirectByteBuffer` becomes phantom-reachable. Memory-mapped files (`FileChannel.map()`) map a file region directly into virtual address space, allowing file I/O as fast as memory access and enabling large-file processing without loading everything into heap. Both approaches require careful lifecycle management to avoid native memory leaks; in Java 22+ the FFM API is preferred over direct `ByteBuffer`.
`Unsafe` provides backdoor access to JVM internals: direct memory allocation/deallocation, CAS operations, object field access without reflection overhead, array offsets, and class definition. Frameworks like Netty, Kryo, and Hazelcast use it for performance. Dangers: incorrect use causes JVM crashes (SIGSEGV), bypasses null/bounds checks, can corrupt the heap, and breaks security models. Java 9+ restricted access via JPMS; Java 17 enforces strong encapsulation. The FFM API and `VarHandle` are the modern, safe alternatives.
Java's built-in serialization (`ObjectInputStream.readObject()`) instantiates arbitrary classes from a byte stream. An attacker can craft a payload using "gadget chains" — sequences of classes (from the classpath: Apache Commons Collections, Spring, Groovy) whose `readObject()`/`readResolve()` methods chain together to execute arbitrary code (RCE) when deserialized. High-profile exploits: CVE-2015-4852 (WebLogic), CVE-2016-4438. Mitigations: use `ObjectInputFilter` (Java 9+) to allowlist classes, replace Java serialization with JSON (Jackson), Protobuf, or Avro for external data, and never deserialize untrusted data with `ObjectInputStream`.
Spring Boot scans all JARs on the classpath for `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` (Spring Boot 2.7+; previously `spring.factories`). Each listed `@AutoConfiguration` class is conditionally activated: `@ConditionalOnClass` (only if certain class is present), `@ConditionalOnMissingBean` (only if user has not defined their own), `@ConditionalOnProperty` (feature flag), etc. This creates an opt-out model — your own `@Bean` definitions override autoconfigured ones. The order of autoconfiguration is controlled by `@AutoConfigureBefore`/`@AutoConfigureAfter`.
Project Reactor (used by Spring WebFlux) provides `Flux<T>` (0..N elements) and `Mono<T>` (0..1 element) as reactive, non-blocking streams. Instead of one thread per request (blocking I/O), reactive uses a small thread pool and processes work in event-driven callbacks when data is ready. Backpressure is first-class — subscribers signal how many items they can handle. The programming model is functional/pipeline-based. Debugging stack traces are harder; `checkpoint()` and `log()` operators help. Virtual threads (Java 21) provide an alternative to reactive for simple concurrency without the callback complexity.
Native image compiles Java bytecode ahead-of-time (AOT) to a standalone native binary using static analysis starting from the main entry point. Benefits: near-instant startup (milliseconds vs seconds), low RSS memory footprint — ideal for serverless and microservices. Trade-offs: longer build time; closed-world assumption means reflective access, dynamic class loading, and JNI must be declared in config files (reflection-config.json, proxy-config.json) or the binary will fail at runtime. JIT is absent so peak throughput is lower than HotSpot. Many frameworks (Micronaut, Quarkus, Spring AOT in Spring Boot 3) generate native image configs automatically.
Java 9+ enforces strong encapsulation via JPMS: internal packages (e.g. `sun.misc`, `com.sun.reflect`) are not accessible by default even via reflection. `--add-opens module/package=ALL-UNNAMED` opens a package for deep reflection at runtime, bypassing encapsulation. Many legacy libraries and tools (Mockito, ByteBuddy, serialization frameworks) require this. Java 17 made violations warnings; Java 21 may make them errors. The proper fix is to migrate libraries to public APIs or use `VarHandle` / `MethodHandles.privateLookupIn()`.
JMH (Java Microbenchmark Harness) is the standard tool for reliable Java microbenchmarks. Annotate benchmark methods with `@Benchmark`; control warm-up with `@Warmup(iterations=5)`, measurement with `@Measurement(iterations=10)`, and time unit with `@BenchmarkMode(Mode.AverageTime)`. Key pitfalls JMH addresses: dead code elimination (use `Blackhole.consume()`), constant folding (pass inputs via `@State` objects), JIT biases from insufficient warmup. Run with `java -jar benchmarks.jar -f 1 -wi 5 -i 10`.
async-profiler is a low-overhead sampling profiler that uses `perf_events` / `AsyncGetCallTrace` to capture CPU, allocation, and lock contention profiles without the safepoint bias of older profilers. Attach to a running JVM: `./profiler.sh -d 30 -f flamegraph.html <pid>`. Key modes: `cpu` (default), `alloc` (heap allocation by allocation site), `lock` (monitor contention), `wall` (wall-clock — useful for diagnosing threads blocked on I/O). Flame graphs visualize the output: the width of a frame represents the proportion of time spent in that method and its callees. IntelliJ IDEA bundles async-profiler in its profiler view.
Eclipse Memory Analyzer (MAT) reads heap dumps (`.hprof`, generated by `-XX:+HeapDumpOnOutOfMemoryError` or `jcmd <pid> GC.heap_dump`). Key features: Dominator Tree shows which objects retain the most heap (retained heap = how much would be freed if object were collected); Leak Suspects report automatically identifies probable leaks; OQL (Object Query Language) allows SQL-like queries over the heap. Common leak patterns: large `char[]` retained by old `String` substrings (pre-Java 7u6), `ClassLoader` leaks (Metaspace), growing `HashMap` in static field.
Annotation processors run at compile time via `javac`'s pluggable annotation processing API. Implement `javax.annotation.processing.AbstractProcessor`, declare supported annotations with `@SupportedAnnotationTypes`, and override `process(Set<? extends TypeElement>, RoundEnvironment)`. Use `ProcessingEnvironment.getFiler()` to generate new source files. Processors enable compile-time code generation (Lombok, Dagger, MapStruct, Immutables) without reflection at runtime. Register processors in `META-INF/services/javax.annotation.processing.Processor` or via `@AutoService`. Multiple rounds occur until no new files are generated.
Key anti-patterns: (1) Premature optimization — micro-optimizing before profiling; (2) Checked exception abuse — declaring broad `throws Exception` or catching and swallowing exceptions silently; (3) Using raw types — `List list` instead of `List<String>`, losing compile-time safety; (4) Mutable static state — causes test pollution and concurrency bugs; (5) Returning null from collections — return empty collection instead; (6) `finalize()` for cleanup — unreliable, use `Cleaner` or try-with-resources; (7) Locking on `this` in library code — callers can deadlock by locking on the same object; (8) Over-synchronization — holding locks during I/O calls.
HotSpot makes speculative assumptions during C2 compilation: e.g. "method X has only one implementation" (monomorphic call site) or "field Y is always non-null". These are guarded by uncommon traps — special deoptimization points. When an assumption is violated at runtime (a second implementation is loaded, or Y is null), HotSpot deoptimizes: the compiled frame is replaced with interpreted frames and execution continues in the interpreter. Repeated deoptimizations of the same method are logged as "decompilations"; after too many the method may be blacklisted from tier-4 compilation. Monitor with `-XX:+TraceDeoptimization` or JDK Flight Recorder.
JFR is a low-overhead (typically < 1% overhead) production-safe profiling and event-recording framework built into the JVM. It records thousands of JVM and application events (GC pauses, class loading, thread lifecycle, I/O, lock contention, exception rates) into a ring-buffer `.jfr` file, analyzed with JDK Mission Control. Unlike sampling profilers, JFR captures exact events with timestamps and durations. It is always-on suitable, unlike async-profiler which is typically used for incident investigation. Custom events can be defined by extending `jdk.jfr.Event`. ```bash jcmd <pid> JFR.start duration=60s filename=recording.jfr jcmd <pid> JFR.dump filename=recording.jfr ```

Frequently Asked Questions

What Java version should I study?

Target Java 17 or 21 (LTS). Know records, sealed classes, pattern matching, and virtual threads.

Is Spring Boot a must?

For most enterprise Java roles, yes. Understand DI, REST controllers, JPA, transactions, and basic security.

How deep should I go on JVM internals?

Know memory model (heap vs stack, generational GC), class loading basics, and common tuning flags. Senior roles probe deeper.

What about concurrency?

Essential. Synchronized vs locks, volatile vs atomics, executors, CompletableFuture, and now virtual threads.

Do I need Kotlin too?

Only if the role lists it. It is a nice bonus but not a replacement for Java fundamentals.

Related Topics

Ready to apply?

TryApplyNow scores matches, tailors resumes, and tracks applications so you can focus on prep, not paperwork.

Try for free →