Sitemap

EPAM Technical Interview — Java Tech Lead

23 min readFeb 2, 2026

--

Press enter or click to view image in full size

Java 8 Interview Questions

Q.1 What is Synchronized?

synchronized is a keyword used to control access to shared resources in a multi-threaded environment.

synchronized prevents race conditions in shared mutable state and ensured data consistency across thread for all compound/atomic operation.

synchronized guarantees visibility by establishing a happens-before relationship between threads using lock acquire and lock release semantics.

synchronized provides visibility by flushing all writes to main memory on lock release and reloading them on lock acquisition, creating a happens-before relationship between threads.

JMM Rule → A release of a lock happens-before every subsequent acquisition of the same lock

It provides thread safety by restricting concurrent access to a critical section and ensures memory visibility, proper instruction ordering, and atomic operations.

one thread sees the value

Synchronized work internally

At runtime, every Java object has an intrinsic lock (monitor lock).

When a thread enters a synchronized block/method:

  1. It acquires the monitor lock
  2. Executes the critical section
  3. Releases the lock when exiting (even if an exception occurs)

Other threads:

  • Block until the lock is released

Types of synchronized

  • Synchronized Method (Object Level Lock) — Lock on current object (this)
  • Synchronized Block (Fine-grained Lock) — Reduced lock scope and provide better performance
  • Static synchronized Method (Class Level Lock) — Lock on class object and have only one thread access all instance
Thread A exits synchronized block
↓
Lock Release --> All writes flush into main memory
↓
Happens-Before
↓
Thread B enters synchronized block <-- reload from memory

synchronized keyword in Java is one of the simplest ways to achieve thread safety. It ensures mutual exclusion and visibility, making shared data safe in a multi-threaded environment. However, while synchronized is easy to use, it comes with several limitations that become evident in high-concurrency and performance-critical systems.

Synchronized Drawback:

1. Performance Overhead Under Contention

When a thread enters a synchronized block or method, it acquires a monitor lock. Any other thread attempting to acquire the same lock is forced to block.

synchronized does not scale well when many threads compete for the same lock.

Blocking leads to:

  • Context switching
  • CPU idle time
  • Reduced throughput

Under high thread contention, performance degrades significantly, making synchronized unsuitable for highly concurrent systems

2. Thread Blocking with No Timeout or Try Option

With synchronized, threads have no flexibility when acquiring a lock:

  • A thread must wait until the lock is released
  • There is no way to try acquiring the lock conditionally
  • No timeout mechanism is available

This means a thread may block indefinitely if the lock holder takes too long.

Impact:

  • Poor responsiveness
  • Not ideal for low-latency or real-time systems

3. Risk of Deadlock

Deadlocks can occur when multiple threads acquire locks in an inconsistent order.

Deadlocks are programming errors, and synchronized provides no built-in recovery mechanism.

For example:

  • Thread A holds Lock 1 and waits for Lock 2
  • Thread B holds Lock 2 and waits for Lock 1

The JVM does not detect or recover from deadlocks automatically. Once a deadlock occurs, the application may appear frozen.

4. Coarse-Grained Locking Reduces Parallelism

synchronized often results in coarse-grained locking:

  • Entire methods or large code blocks are locked
  • Even unrelated operations are blocked unnecessarily

This reduces:

  • Parallel execution
  • Application scalability
  • Efficient CPU utilization

In large systems, coarse-grained locking becomes a serious bottleneck.

5. No Interrupt Handling While Waiting for Lock

A thread waiting to acquire a synchronized lock cannot be interrupted. If a thread is blocked:

  • It remains blocked until the lock is released
  • Interrupt signals are ignored

This makes synchronized unsuitable for applications that rely on interruption for cancellation or graceful shutdown.

6. Limited Condition Coordination

synchronized supports only:

  • wait()
  • notify()
  • notifyAll()

Limitations include:

  • No support for multiple condition queues
  • Harder to manage complex thread coordination

Compared to modern concurrency utilities, this approach is less expressive and more error-prone.

7. No Fairness Guarantee

The JVM does not guarantee the order in which threads acquire a synchronized lock:

  • Threads are not served in FIFO order
  • Some threads may experience starvation

This lack of fairness can be problematic in systems requiring predictable execution order.

Q.2 ReentrantLock?

ReentrantLock is an explicit lock implementation that provides the same basic mutual exclusion behavior as synchronized, but with greater control and flexibility.

The term reentrant means:

A thread that already holds the lock can acquire it again without blocking itself.

ReentrantLock Features

Fine Grained Locking Control for both read and write operation

Try Lock without blocking

Timeout while acquiring lock

Interruptible Lock Acquisition

Fairness Policy

Multiple Condition Variable

ReentrantLock is a powerful alternative to synchronized that provides:

  • Better scalability
  • Advanced locking features
  • Greater control over thread behavio

Q.3 ArrayList vs LinkedList?

ArrayList and LinkedList are two commonly used implementations of the List interface in Java. While both allow ordered data storage and support duplicate elements, their internal data structures, performance characteristics, and use cases are very different.

ArrayList

  • Backed by a dynamic array
  • Elements are stored in contiguous memory locations
  • Automatically resizes when capacity is exceeded

LinkedList

Implemented as a doubly linked list

Each element (node) contains:

  • Data
  • Reference to previous node
  • Reference to next node

Memory is non-contiguous

Memory Usage

ArrayList → Memory overhead(Low) — Extra storage (Only Array)

LinkedList → Memory overhead(High) — Extra storage (Node + 2 references)

LinkedList consumes more memory due to node pointers.

Accessing Elements

  • get by index ArrayList O(1) and LinkedList O(n)
  • ArrayList supports direct index-based access
  • LinkedList must traverse from head or tail

Insertion / Deletion

  • Insertion at the end ArrayList O(1) and LinkedList O(1)
  • Insertion and deletion by index at the middle ArrayList O(n) and LinkedList O(n)
  • ArrayList may occasionally resize (costly operation)
  • LinkedList simply updates pointers
  • LinkedList removal is O(1) only if you already have the node reference
  • If accessed by index, traversal still costs O(n)

Cache Friendliness

  • ArrayList benefits from CPU cache locality
  • LinkedList suffers due to scattered memory access

Thread Safety

  • Neither ArrayList nor LinkedList is thread-safe
  • Must be synchronized externally or wrapped using:
  • Collections.synchronizedList()

For concurrency, prefer:

  • CopyOnWriteArrayList

When to Use ArrayList

Use ArrayList when: (Api Resp List, Caching Layers, Data Processing Pipelines)

  • Frequent read operations
  • Index-based access is required
  • Memory efficiency matters
  • Iteration performance is critical

When to Use LinkedList

Use LinkedList when: (Impl Queue, Undo/Redo Func)

  • Frequent insertions/deletions at head or tail
  • Deque or queue operations are required
  • You already have node references

In most real-world applications, ArrayList outperforms LinkedList, even for insertion-heavy workloads, due to better cache locality and lower memory overhead.

“ArrayList provides fast random access and better cache performance, while LinkedList offers efficient insertions only when node references are already available.”

Q.4 TreeSet vs TreeMap?

TreeSet is an implementation of the Set interface that stores unique elements in sorted order. It is typically used when both ordering and uniqueness are required. Internally, TreeSet is implemented using a Red-Black Tree, a self-balancing binary search tree that ensures the tree remains balanced after insertions and deletions.

Because of this structure, operations such as add, remove, and contains execute in O(log n) time. Sorting in a TreeSet is achieved either through the element’s natural ordering (by implementing Comparable) or via a custom Comparator passed at creation time. Since ordering relies on comparisons, TreeSet does not allow null elements.

An important internal detail is that TreeSet is actually backed by a TreeMap. Each element added to a TreeSet is stored as a key in an underlying TreeMap, with a dummy object used as the value.

TreeSet is not thread-safe and must be synchronized externally in concurrent environments. It is commonly used for maintaining sorted unique values, such as ranked IDs, sorted search suggestions, or ordered configuration values.

TreeMap is an implementation of the Map interface that stores key–value pairs sorted by keys. Like TreeSet, it is implemented using a Red-Black Tree, which guarantees balanced structure and O(log n) performance for operations such as put, get, and remove.

The ordering of keys in a TreeMap is determined either by natural ordering or by a custom Comparator. Because keys must be comparable, TreeMap does not allow null keys, although it allows multiple null values.

One of the major strengths of TreeMap is its support for navigational and range-based operations. Methods such as firstKey, lastKey, subMap, headMap, and tailMap make it extremely useful when working with ordered or time-based data.

Like TreeSet, TreeMap is not thread-safe by default and requires external synchronization in multi-threaded scenarios. It is widely used in applications such as leaderboards, sorted caches, scheduling systems, and scenarios where keys must be processed in order.

When to Use Which?

Use TreeSet when:

  • You need unique values
  • Elements must always be sorted
  • No key–value mapping is required

Use TreeMap when:

  • You need sorted key–value pairs
  • Range queries or ordered traversal are required
  • Keys must be processed in sequence

Use TreeSet for sorted uniqueness and TreeMap for sorted key–value relationships.

Q.5 Fixed Thread Pool vs Cached Thread Pool?

A Fixed Thread Pool creates a fixed number of threads and reuses them to execute submitted tasks. If all threads are busy, new tasks are placed in an unbounded queue and wait until a thread becomes available. This provides predictable resource usage and prevents thread explosion. It is best suited for CPU-bound or steady workloads where the number of concurrent tasks is known. However, if tasks are long-running, the queue may grow indefinitely, leading to memory pressure and increased latency.

Fixed thread pools offer stability and control but can suffer from queue buildup.

A Cached Thread Pool creates new threads as needed and reuses idle threads. Threads that remain idle for a certain time are terminated automatically. It uses no task queue, which allows fast execution of short-lived, asynchronous tasks. Cached thread pools scale well for bursty or I/O-bound workloads. However, since there is no upper limit on thread creation, it can lead to resource exhaustion under heavy load.

Cached thread pools prioritize throughput and responsiveness over resource predictability.

Q.6 Thread Safe Collection?

Vector is a legacy thread-safe implementation of a dynamic array. All its public methods are synchronized, meaning only one thread can access the vector at a time. While this guarantees thread safety, it introduces significant performance overhead due to coarse-grained locking. Every operation, including read operations, requires acquiring a lock, which limits scalability. Because of this, Vector is rarely used in modern applications and is mostly kept for backward compatibility. In practice, CopyOnWriteArrayList or synchronized ArrayList is preferred.

Hashtable is a legacy thread-safe implementation of the Map interface. Like Vector, all methods are synchronized, providing thread safety through coarse-grained locking. It does not allow null keys or values. Due to global locking, performance degrades severely under high contention. Modern applications avoid Hashtable in favor of ConcurrentHashMap, which provides better concurrency and scalability.

ConcurrentHashMap is the most widely used thread-safe map in Java. It allows multiple threads to read and write concurrently without blocking the entire map. Internally, it uses fine-grained locking and CAS operations, enabling high scalability. Reads are non-blocking, and updates lock only a small portion of the map. It allows null neither keys nor values. ConcurrentHashMap is ideal for high-concurrency systems such as caches, counters, and shared configuration storage.

CopyOnWriteArrayList is a thread-safe variant of ArrayList designed for read-heavy workloads. On every write operation, it creates a new copy of the underlying array, while readers continue accessing the old array without locking. This makes read operations extremely fast and lock-free. However, write operations are expensive in terms of memory and performance. It is best suited for scenarios with frequent reads and infrequent writes, such as event listeners or configuration lists.

CopyOnWriteArraySet is a thread-safe set built on top of CopyOnWriteArrayList. It provides thread safety using the same copy-on-write mechanism. All read operations are lock-free, while write operations involve copying the entire backing array. It ensures uniqueness of elements and is ideal for small sets with frequent iteration and rare updates. Due to memory overhead, it is not suitable for large or write-heavy datasets.

ConcurrentLinkedQueue is a non-blocking, thread-safe queue based on lock-free algorithms. It allows multiple threads to add and remove elements concurrently without locking. This makes it highly scalable and suitable for high-throughput systems. It is commonly used in task scheduling, messaging systems, and producer-consumer scenarios where blocking is undesirable.

ConcurrentLinkedDeque is a thread-safe, non-blocking double-ended queue. It allows concurrent insertion and removal from both ends. Like ConcurrentLinkedQueue, it uses lock-free algorithms for high scalability. It is useful when threads need flexible access from both ends, such as work-stealing algorithms or task prioritization systems. is a thread-safe, non-blocking double-ended queue. It allows concurrent insertion and removal from both ends. Like ConcurrentLinkedQueue, it uses lock-free algorithms for high scalability. It is useful when threads need flexible access from both ends, such as work-stealing algorithms or task prioritization systems.

ConcurrentSkipListMap is a thread-safe, sorted map that provides logarithmic-time operations. It supports concurrent access and maintains keys in sorted order. Unlike TreeMap, it is safe for concurrent access and allows scalable reads and writes. It is commonly used when sorted data is required in concurrent environments.

ConcurrentSkipListMap is a thread-safe, sorted map that provides logarithmic-time operations. It supports concurrent access and maintains keys in sorted order. Unlike TreeMap, it is safe for concurrent access and allows scalable reads and writes. It is commonly used when sorted data is required in concurrent environments.

BlockingQueue Implementations

Examples include:

  • ArrayBlockingQueue
  • LinkedBlockingQueue
  • PriorityBlockingQueue

These queues are thread-safe and designed for producer-consumer scenarios. They support blocking operations such as put() and take(), which wait when the queue is full or empty. Internally, they use locks and condition variables for coordination. Blocking queues are widely used in thread pools and task execution frameworks.

Why Not Just Use Collections.synchronizedXxx()

Collections.synchronizedList, synchronizedSet, and synchronizedMap provide thread safety by wrapping collections with a single global lock. While this ensures correctness, it has major drawbacks:

  • Poor scalability
  • Blocking reads
  • High contention
  • Risk of ConcurrentModificationException during iteration

In contrast, concurrent collections:

  • Use fine-grained or lock-free techniques
  • Allow concurrent reads and writes
  • Scale efficiently under heavy load

Synchronized wrappers ensure safety, but concurrent collections ensure scalability.

Q.7 ExecutorService?

ExecutorService is a high-level concurrency abstraction that manages a pool of threads and executes submitted tasks asynchronously.

ExecutorService decouples task submission from task execution.

ExecutorService is an interface in java.util.concurrent that extends Executor. It allows developers to:

  • Submit tasks for execution
  • Manage thread lifecycle
  • Handle asynchronous results
  • Gracefully shut down thread pools

Instead of managing threads directly, developers submit tasks (Runnable or Callable) and let the framework handle execution.

A ThreadFactory is responsible for creating threads used by an ExecutorService. Instead of the executor creating threads directly, it delegates thread creation to the ThreadFactory, giving developers control over thread characteristics.

ThreadFactory customizes how threads are created in a thread pool.

ThreadFactory role includes:

  • Creating new threads for the thread pool
  • Setting custom thread names (helps debugging & monitoring)
  • Setting daemon or non-daemon threads
  • Setting thread priority
  • Attaching UncaughtExceptionHandler
  • Applying security or logging policies

How ExecutorService Works

  1. Tasks are submitted using execute() or submit()
  2. Tasks are placed into an internal queue
  3. Worker threads pick tasks from the queue
  4. Threads are reused after task completion

This improves performance and scalability.

A Rejection Policy defines what happens when a task cannot be accepted by an ExecutorService. Rejection occurs when:

  • All core and maximum threads are busy
  • The task queue is full
  • The executor is shut down

Java provides four built-in rejection policies:

AbortPolicy (default)

  1. Throws RejectedExecutionException.
  2. Fail-fast; safest for critical systems.

CallerRunsPolicy

  1. Task runs in the calling thread.
  2. Provides back-pressure and slows task producers.

DiscardPolicy

  1. Silently discards the task.
  2. Risky — use only when task loss is acceptable.

DiscardOldestPolicy

  1. Drops the oldest queued task and retries submission.
  2. Useful for real-time or streaming systems.

Why Do We Need ExecutorService?

Manual thread management leads to:

  • Thread creation overhead
  • Poor resource utilization
  • Difficult lifecycle management
  • Risk of memory leaks and thread explosion

ExecutorService solves these problems by:

  • Reusing threads (thread pooling)
  • Limiting concurrent threads
  • Managing queues and scheduling
  • Supporting graceful shutdown

ExecutorService — How It Works

1. A task is submitted using execute() or submit() (submit() wraps it in a FutureTask).

2. The executor checks if the number of running threads is less than corePoolSize. If yes, it creates a new thread using the ThreadFactory and executes the task.

3. If core threads are busy, the task is placed into the BlockingQueue.

4. If the queue is full, the executor creates additional threads up to maximumPoolSize.

5. If both queue and maximum threads are full, the Rejection Policy is triggered (Abort, CallerRuns, Discard, or DiscardOldest).

6. Worker threads repeatedly take tasks from the queue, execute them, and return to the pool.

7. Threads above core size are terminated after keepAliveTime when idle.

8. On shutdown(), new tasks are rejected and queued tasks finish execution.

ExecutorService manages task execution using thread pooling, queuing, and configurable rejection and thread-creation policies.

Q.8 Future vs CompletableFuture?

Future

Future represents the result of an asynchronous computation. It is returned when a task is submitted to an ExecutorService.

Key Characteristics

• Task runs asynchronously

• Result is retrieved using get()

• get() is blocking

• Cannot chain tasks

• No built-in callback mechanism

• Limited error handling

Limitations (Why it’s weak)

• Blocking kills async benefits

• No way to say “when done, do this”

• Hard to compose multiple async tasks

• Poor exception handling

“Future is asynchronous in execution, but synchronous in result consumption.”

CompletableFuture

CompletableFuture is an advanced async abstraction introduced in Java 8.

It represents a promise that can be:

• Completed automatically

• Completed manually

• Chained with other async tasks

• Combined with multiple futures

Key Characteristics

• Fully non-blocking

Get Kaizen Chandra’s stories in your inbox

Join Medium for free to get updates from this writer.

• Supports callbacks

• Allows task chaining

• Powerful exception handling

• Can run without ExecutorService

Thread Model Difference

Future

• Always tied to ExecutorService

• Blocking call waits for worker thread to finish

CompletableFuture

• Uses:

• ForkJoinPool.commonPool() by default

• Custom Executor if provided

Future provides async execution but blocking results, while CompletableFuture enables fully non-blocking, composable, and reactive async programming.

Q.9 ParallelStream and How we can make stream ordered with using parallelStream?

A Parallel Stream is a stream that processes elements concurrently using multiple threads. Internally, it uses the ForkJoinPool.commonPool, splitting the data into chunks and processing them in parallel to improve performance.

“Parallel streams trade ordering guarantees for performance.”

Key Points

  • Uses multiple CPU cores
  • Best for CPU-bound, large data sets
  • Not suitable for small collections or I/O-bound tasks
  • Order is not guaranteed by default
list.parallelStream().forEach(System.out::println);

list.parallelStream()
.forEachOrdered(System.out::print);
// “forEachOrdered preserves order at the cost of parallel performance.”

list.parallelStream()
.map(x -> x * 2)
.sequential()
.forEach(System.out::println);
// This disables parallelism from that point onward.

Parallel streams use ForkJoinPool for concurrent processing, and ordering can be preserved using forEachOrdered or ordered terminal operations, with some performance trade-off

Q.10 ThreadLocal and What issue it has and how to provide alternative solution to deal with issue?

ThreadLocal provides thread-confined variables — each thread gets its own isolated copy of a variable, even though the variable is shared.

Use cases

  • User/session context
  • Transaction ID
  • Request-scoped data (logging, tracing)
  • Non-thread-safe objects per thread (e.g., SimpleDateFormat)

ThreadLocal avoids synchronization by giving each thread its own data copy.”

How ThreadLocal Works

  • Each Thread has a ThreadLocalMap
  • Keys are ThreadLocal objects
  • Values are thread-specific data
  • Keys are WeakReferences, values are StrongReferences

Issues with ThreadLocal

Memory Leak

  • In thread pools, threads live long
  • If ThreadLocal.remove() is not called:
  • Key may be GC’d
  • Value stays → leak

Thread Pool Reuse Problem

  • Same thread reused for multiple request
  • Old data may leak into next request

Hidden Dependency / Poor Design

  • Data flow becomes implicit
  • Hard to debug, test, or reason about
  • Violates clean architecture principles

Not Suitable for Async / Reactive Code

  • Thread may change during execution
  • ThreadLocal data does not propagate

Breaks in:

  • CompletableFuture
  • Reactive streams
  • Virtual threads (carefully)

How to Fix / Avoid ThreadLocal Issues

  • Always Call remove() — Mandatory in thread pools to prevent memory leaks
  • Prefer Method Parameters — Pass context explicitly instead of hiding it in ThreadLocal
  • Use Scoped Context Objects — Create a request-scoped object and pass it through layesr
  • Use Framework Context Propagation

— — For async/reactive systems:

— — MDC with proper cleanup

— — Context propagation libraries

— —Structured concurrency (Java 21+)

  • Use InheritableThreadLocal — Passes value to child threads (still unsafe with thread pools and not recommended for executors)

ThreadLocal provides thread confinement but can cause memory leaks, data leakage in thread pools, and design issues. Prefer explicit context passing and always call remove() if ThreadLocal is used.

StructuredScope (officially StructuredTaskScope) is a Java 21 feature that enables structured concurrency.

It lets you run multiple concurrent tasks as a single logical unit of work, with clear lifecycle, error handling, and cancellation rules.

StructuredScope ensures that child tasks cannot outlive their parent task.

Why StructuredScope Was Introduced

Traditional concurrency problems:

• Threads outlive request scope

• Hard to cancel sibling tasks

• Poor error propagation

• ThreadLocal breaks in async code

• Resource leaks

Structured concurrency fixes this by enforcing:

• Clear start and end

• Automatic cancellation

• Predictable error handling

How StructuredScope Works

1. A scope is created (usually in try-with-resources)

2. Multiple subtasks are forked

3. Parent waits for all subtasks

4. If one fails:

• Others are cancelled automatically

5. Scope closes → all tasks are done or cancelled

ExecutorService is unstructured; StructuredScope enforces structured concurrency.

Relationship with Virtual Threads

StructuredScope is designed to work perfectly with Virtual Threads:

  • Thousands of subtasks
  • Low memory overhead
  • Blocking is cheap
  • Cleaner async logic

When to Use StructuredScope

Use it when:

  • Handling a single request
  • Making multiple parallel calls
  • All tasks must finish together
  • Failure should cancel siblings

Avoid it for:

  • Long-running background jobs
  • Fire-and-forget tasks

Real-World Example

API Aggregation:

  • Call User Service
  • Call Order Service
  • Call Payment Service
  • If one fails → cancel all → return error fast

Q.11 Executor Service vs ForkJoin Pool?

ExecutorService is for general async task execution, while ForkJoinPool is optimized for CPU-bound, recursive parallelism using work stealing.

ExecutorService is ideal for general-purpose concurrency where tasks are independent and predictable resource control is required.

ExecutorService is a high-level concurrency framework used to execute and manage asynchronous tasks using a thread pool. It decouples task submission from task execution, allowing developers to submit Runnable or Callable tasks without managing threads manually. Internally, it is commonly backed by ThreadPoolExecutor, which uses a task queue, configurable core and maximum thread counts, ThreadFactory, and rejection policies.

ExecutorService is best suited for independent, long-running, or I/O-bound tasks, such as handling web requests, background jobs, or async API calls. It provides strong lifecycle control through shutdown() and shutdownNow(). However, tasks are largely independent, cancellation is manual, and there is no built-in support for task decomposition or work stealing.

ForkJoinPool excels at fine-grained parallel computation but is unsuitable for blocking or I/O-heavy workloads.

ForkJoinPool is a specialized thread pool designed for divide-and-conquer, CPU-bound parallelism. It works by recursively splitting tasks into smaller subtasks (fork) and then combining results (join). Internally, it uses a work-stealing algorithm, where idle threads steal work from busy threads’ queues, leading to better CPU utilization.

ForkJoinPool is the backbone of parallel streams and many modern Java parallel operations. It uses a limited number of threads (typically equal to CPU cores) and assumes tasks are short-lived and non-blocking. Blocking operations inside ForkJoinPool can severely degrade performance. Unlike ExecutorService, it is not queue-centric and does not focus on long-running or I/O tasks.

Q.12 When we should go for ForkJoinPool over ExecutorService

You should go for ForkJoinPool when your problem naturally fits a divide-and-conquer model and is CPU-bound, not I/O-bound.

Choose ForkJoinPool when:

• Tasks can be recursively split into smaller subtasks (fork) and results combined (join)

• Workload is CPU-intensive (computations, not blocking I/O)

• Tasks are short-lived and fine-grained

• You want maximum CPU utilization using work-stealing

• You need automatic load balancing between worker threads

• You are using parallel streams (they run on ForkJoinPool internally)

Examples:

• Recursive algorithms (merge sort, quick sort)

• Parallel array or collection processing

• Data aggregation and transformations

• Mathematical or analytics computations

Avoid ForkJoinPool when:

• Tasks perform blocking I/O

• Tasks are long-running

• You need strict task ordering or complex lifecycle control

Use ForkJoinPool for CPU-bound, recursive parallelism; use ExecutorService for general async or I/O-bound tasks.

Q.13 Checked vs Unchecked Exception, How we can create Custom Exception?

Checked Exceptions

Checked exceptions are checked at compile time. The compiler forces the developer to handle them using try-catch or declare them using throws.

Examples:

IOException, SQLException, FileNotFoundException

Characteristics

• Represent recoverable conditions

• Encourage explicit error handling

• Can make code verbose if overused

“Checked exceptions represent conditions a caller can reasonably recover from.”

Unchecked Exceptions

Unchecked exceptions occur at runtime and are not checked by the compiler. They extend RuntimeException.

Examples:

NullPointerException, IllegalArgumentException, ArithmeticException

Characteristics

• Represent programming errors

• No forced handling

• Cleaner APIs

Checked exceptions enforce compile-time handling for recoverable errors, while unchecked exceptions represent programming errors; custom exceptions are created by extending Exception or RuntimeException based on use case

Q.14 Functional Interface, Method Reference, Lambda Expression and Type of Functional Interface and map() vs flatMap()?

Functional interfaces provide a target type for lambda expressions. A Functional Interface is an interface with exactly one abstract method. It enables lambda expressions and functional programming in Java.

A Lambda Expression is an anonymous function that implements a functional interface, reducing boilerplate code. It is an anonymous function that implements a functional interface, reducing boilerplate code

A Method Reference is a shorthand for lambda expressions when an existing method matches the functional interface signature. Method references improve readability by reusing existing methods

Types of Functional Interfaces

1. Consumer — takes input, no output

2. Supplier — no input, returns output

3. Predicate — takes input, returns boolean

4. Function — takes input, returns output

map() vs flatMap() — map transforms, flatMap flattens

map() → Transforms each element into another object.

flatmap() → Flattens nested structures into a single stream.

Q15. Difference between == and equals()? When would you use each?

== compares references, while equals() compares logical equality (content), assuming it’s overridden. In interviews, I emphasize intent: use == when you want to check whether two references point to the same object, such as checking enum values or singleton instances. Use equals() when comparing business data, like usernames or IDs. A critical gotcha is that String overrides equals() but many custom objects don’t unless explicitly implemented. In Hash-based collections, equals() must be consistent with hashCode(). Interviewers often test whether candidates understand reference vs value semantics, especially in String Pool and heap scenarios.

Q.16 Coding Java 8 Task

Find the largest length string from a List<String>

List<String> list = List.of("Java", "SpringBoot", "Microservices", "API");

String longestString = list.stream()
.max(Comparator.comparingInt(String::length))
.orElseThrow(() -> new RuntimeException("empty list"));

System.out.println(longestString); // Microservices

Find the duplicates elements list from List<Integer>

List<Integer> numbers = List.of(1, 2, 3, 2, 4, 5, 1, 6);

Set<Integer> seen = new HashSet<>();

List<Integer> duplicates = numbers.stream()
.filter(n -> !seen.add(n))
.distinct()
.toList();

System.out.println(duplicates); // [2, 1]


List<Integer> duplicates = numbers.stream()
.collect(Collectors.groupingBy(n -> n, Collectors.counting()))
.entrySet()
.stream()
.filter(e -> e.getValue() > 1)
.map(Map.Entry::getKey)
.toList();
/*
Time Complexity
• O(n)
• Space: O(n)
*/

Sort the Employee by ID

record Employee(int id, String name) {}

List<Employee> sortedEmployees = employees.stream()
.sorted(Comparator.comparingInt(Employee::id))
.toList();

Spring Boot Interview Questions

Q.1 RestControllerAdvice annotation?

@RestControllerAdvice is a global exception-handling and response-modification annotation in Spring Framework. It combines @ControllerAdvice + @ResponseBody, meaning all handler methods return JSON/XML directly — perfect for REST APIs.

@RestControllerAdvice centralizes REST exception handling in Spring Boot, ensuring consistent JSON responses and cleaner controllers..

Why do we need

@RestControllerAdvice

  • Centralize exception handling
  • Avoid repetitive try-catch blocks in controllers
  • Ensure consistent error responses
  • Clean, maintainable REST APIs

Q.2 Qualifier vs Primary

@Primary defines a default bean, while @Qualifier explicitly selects a bean — and @Qualifier always takes precedence.

@Qualifier explicitly tells Spring which bean to inject by name or custom qualifier. @Primary defines the default bean.

@Primary marks one bean as the default choice when multiple candidates are present. @Qualifier removes ambiguity by explicit bean selection

  • If both are present, @Qualifier wins
  • Bean name itself can act as a qualifier
  • Multiple @Primary beans → error

Q.3 Spring Starter Dependency

In Spring Boot, a starter dependency is a predefined, curated set of dependencies bundled together to support a specific feature or use case. Instead of adding many individual libraries, you add one starter, and Spring Boot brings the right versions automatically.

Why Starter Dependencies Exist

  • Reduce dependency hell
  • Ensure version compatibility
  • Enable auto-configuration
  • Speed up project setup

Spring Boot starters simplify dependency management by providing opinionated defaults.

Q.4 Spring Boot Actuator?

Actuator adds built-in monitoring and management endpoints to Spring Boot apps.

Spring Boot Actuator provides production-ready features to monitor, manage, and observe your application at runtime — without writing custom code. It exposes endpoints for health checks, metrics, environment info, and more.

Why do we need Actuator?

  • Check application health (liveness/readiness)
  • Expose metrics (CPU, memory, requests)
  • Inspect config, beans, mappings
  • Enable ops visibility in production

Microservices Interview Questions

Q.1 How we can call one microservices to another?

Microservices can communicate synchronously or asynchronously.

The most common approach is REST over HTTP, using clients like RestTemplate, WebClient, or Feign Client. For high-performance internal communication, gRPC is used. For loosely coupled and scalable systems, microservices communicate asynchronously using message brokers such as Kafka or RabbitMQ. In large Kubernetes setups, communication may be managed by a service mesh (Istio/Linkerd). Database-level communication is considered an anti-pattern.

Q.2 RestTemplate vs Open Feign Client?

RestTemplate is a synchronous, blocking HTTP client used to call REST APIs programmatically. It requires manual URL construction and error handling and is now deprecated for new development.

OpenFeign Client is a declarative REST client used with Spring Cloud. APIs are defined as interfaces, reducing boilerplate and improving readability. It integrates seamlessly with service discovery, load balancing, and circuit breakers.

Unit Testing Questions

Q.1 Mock annotation vs InjectMock Annotation

In Mockito, @Mock and @InjectMocks are used together to isolate the class under test by replacing its dependencies with mocks.

@Mock

@Mock creates a mock object for a dependency.

It does not inject anything by itself.

@InjectMocks

@InjectMocks creates an instance of the class under test and injects mocks into it. @InjectMocks injects mocks into the class under test.

Q.2 ArgumentMatcher vs ArgumentCapture

ArgumentMatcher — any(), eq(), argThat()

Used to match arguments while stubbing or verifying behavior

“ArgumentMatcher is used to match inputs, not to capture them.”

You use ArgumentMatchers when:

  • Exact value is not important
  • You care about pattern or condition
  • You want flexible stubbing
when(service.process(anyString())).thenReturn("OK");

verify(service).process(eq("order123"));

ArgumentCaptor

Used to capture the actual argument passed and assert on it later.

You use ArgumentCaptor when:

  • You want to verify what exactly was passed
  • Object is complex
  • Internal state matters

ArgumentCaptor captures arguments for post-verification

ArgumentCaptor<Order> captor =
ArgumentCaptor.forClass(Order.class);

verify(repository).save(captor.capture());
Order savedOrder = captor.getValue();

assertEquals("PAID", savedOrder.getStatus());

ArgumentMatcher checks whether arguments match expectations, while ArgumentCaptor captures the actual arguments to assert on them late

Q.3 How to mock behaviour using Mockito?

In Mockito, mocking behavior means defining how a mock should behave when a method is called — what it returns, throws, or how it reacts to inputs.

Stubbing Return Values (when–thenReturn)

“Stubbing defines what a mock returns for a given input.”

Stubbing with Argument Matchers

When exact values don’t matter.

Throwing Exceptions (thenThrow)

Simulate failure

Dynamic Behavior (thenAnswer)

Return values based on runtime arguments.

Void Methods (doNothing / doThrow)

Mockito can’t use when() for void methods.

Partial Mocking (@Spy)

Real object + mocked behavior.

Verifying Behavior (Interaction Testing)

Ensure methods were called correctly.

Mockito mocks behavior by stubbing method calls using when–then, doThrow, or thenAnswer, and verifies interactions using verify()

Q.4 BeforeEach vs AfterEach

In JUnit, @BeforeEach and @AfterEach are lifecycle annotations used to run setup and cleanup logic around every test method.

@BeforeEach

Purpose:

Runs before every test method.

Used for:

• Initializing test data

• Creating mock objects

• Setting up common preconditions

@AfterEach

Purpose:

Runs after every test method.

Used for:

• Cleaning up resources

• Resetting static or shared state

• Closing connections or files

Coding Questions:

Q.1 Find the longest substring without Repeation Character?

 /    /**
* Time Complexity: O(n)
* - We traverse the string once with the right pointer (n iterations)
* - The left pointer also moves at most n times across the entire execution
* - Each character is visited at most twice (once by right, once by left)
* - Overall: O(n) where n is the length of the string
* <p>
* Space Complexity: O(1)
* - We use a fixed-size boolean array of 128 elements (for ASCII characters)
* - The array size doesn't grow with input size
* - All other variables (left, right, n, maxLen, c) use constant space
* - Overall: O(1) constant space (or O(128) = O(1))
* <p>
*/
public int lengthOfLongestSubstring1(String s) {
int left = 0, right = 0, n = s.length(), maxLen = 0;
boolean[] seen = new boolean[128];
while (right < n) {
char c = s.charAt(right);
if (seen[c]) seen[s.charAt(left++)] = false;
else {
seen[c] = true;
maxLen = Math.max(maxLen, right - left + 1);
right++;
}
}
return maxLen;
}

/**
* Time Complexity: O(n)
* - We traverse the string with the right pointer (n iterations in the worst case)
* - The left pointer also moves at most n times across the entire execution
* - Each character is visited at most twice (once by right, once by left)
* - HashSet operations (contains, add, remove) are O(1) average case
* - Overall: O(n) where n is the length of the string
* <p>
* Space Complexity: O(min(n, m))
* - We use a HashSet to store characters in the current window
* - In the worst case (all unique characters), the set size equals the string length: O(n)
* - If the character set is limited (e.g., ASCII), the set size is bounded by the alphabet size: O(m)
* - Overall: O(min(n, m)) where n is string length and m is the size of the character set
* <p>
*/
public int lengthOfLongestSubstring2(String s) {
Set<Character> set = new HashSet<>();
int left = 0, right = 0, maxLen = 0, n = s.length();
while (right < n) {
char c = s.charAt(right);
if (set.contains(c)) {
set.remove(s.charAt(left++));
} else {
set.add(c);
maxLen = Math.max(maxLen, right - left + 1);
right++;
}
}
return maxLen;
}

/**
* Time Complexity: O(n)
* - We traverse the string once with the right pointer (n iterations)
* - The left pointer also moves at most n times across the entire execution
* - Each character is visited at most twice (once by right, once by left)
* - Overall: O(n) where n is the length of the string
* <p>
* Space Complexity: O(1)
* - We use a fixed-size integer array of 128 elements (for ASCII characters)
* - The array size doesn't grow with input size
* - All other variables (count, left, right, n, maxLen, c) use constant space
* - Overall: O(1) constant space (or O(128) = O(1))
* <p>
*/
public int lengthOfLongestSubstring3(String s) {
int[] count = new int[128];
int left = 0, right = 0, maxLen = 0, n = s.length();
while (right < n) {
char c = s.charAt(right);
count[c]++;
right++;
while (count[c] > 1) {
count[s.charAt(left++)]--;
}
maxLen = Math.max(maxLen, right - left);
}
return maxLen;
}

--

--