Java Interview Questions 2026: Complete Guide with Code Examples
Top 50 Java interview questions covering OOP, Collections, Stream API, multithreading, and JVM internals.
Why Java Dominates Backend Interviews
Java remains the #1 language for backend engineering interviews at FAANG companies. Its strong typing, mature ecosystem, and performance characteristics make it ideal for large-scale systems. Java's garbage collection, strong memory management, and extensive standard library make it perfect for building reliable, high-performance applications. Companies like Amazon, Google, and Netflix rely heavily on Java for their backend services because of its stability and scalability.
Core Java Questions
Q1: HashMap vs Hashtable
| Feature | HashMap | Hashtable |
|---|---|---|
| Thread Safety | Not synchronized | Synchronized |
| Null Keys/Values | Allows one null key | No nulls allowed |
| Performance | Faster | Slower due to synchronization |
| Legacy | Modern (Java 1.2) | Legacy (Java 1.0) |
Best Answer: Use ConcurrentHashMap for thread-safe scenarios. HashMap is preferred for single-threaded contexts. Hashtable is considered legacy and should rarely be used in modern Java applications.
Q2: fail-fast vs fail-safe Iterators
// fail-fast: throws ConcurrentModificationException
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) {
list.remove(s); // Exception!
}
// fail-safe: works on clone
List<String> list = new CopyOnWriteArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) {
list.remove(s); // No exception
}
Key Insight: fail-fast iterators use a modification count (modCount) to detect concurrent changes. fail-safe iterators work on a copy of the collection, so they never throw exceptions but may not reflect recent changes.
Q3: String Pool and Immutable Strings
Java maintains a String pool for memory optimization. Strings are immutable because:
- Security: Class loading, network connections, file paths
- Hashing: Hash code cached, consistent across HashMap
- Thread Safety: No synchronization needed
- Immutability: Enables string interning
String a = "hello";
String b = "hello";
String c = new String("hello");
System.out.println(a == b); // true (same pool reference)
System.out.println(a == c); // false (different objects)
System.out.println(a.equals(c)); // true (same content)
Interview Tip: Always use .equals() for string comparison, never ==. The == operator checks reference equality, not content equality.
Collections Framework Deep Dive
Q4: ArrayList vs LinkedList
| Operation | ArrayList | LinkedList |
|---|---|---|
| get(index) | O(1) | O(n) |
| add(end) | O(1) amortized | O(1) |
| add(index) | O(n) | O(n) |
| remove(index) | O(n) | O(n) |
| Memory | Compact | Extra pointers |
Real-world Example: Use ArrayList when you need random access (like reading by index). Use LinkedList when you're frequently adding/removing from the beginning or end, like implementing a queue or stack.
Q5: ConcurrentHashMap Internals
Java 8+ uses CAS + synchronized blocks on individual nodes:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1); // Thread-safe
map.compute("key", (k, v) -> v + 1); // Atomic operation
// Real-world example: Thread-safe word counter
public class WordCounter {
private final ConcurrentHashMap<String, AtomicInteger> wordCounts = new ConcurrentHashMap<>();
public void addWord(String word) {
wordCounts.computeIfAbsent(word, k -> new AtomicInteger(0)).incrementAndGet();
}
public int getCount(String word) {
return wordCounts.getOrDefault(word, new AtomicInteger(0)).get();
}
}
Q6: TreeMap vs HashMap vs LinkedHashMap
| Feature | HashMap | TreeMap | LinkedHashMap |
|---|---|---|---|
| Ordering | No order | Sorted by key | Insertion order |
| Time Complexity | O(1) | O(log n) | O(1) |
| Null Keys | One null | No nulls | One null |
| Use Case | General purpose | Sorted data | Maintaining insertion order |
Best Practice: Use LinkedHashMap when you need to maintain insertion order, like building an LRU cache. Use TreeMap when you need sorted keys, like in a phone directory application.
Q7: Queue and Deque Implementations
// Queue implementations
Queue<String> queue = new LinkedList<>(); // Unbounded queue
Queue<String> boundedQueue = new ArrayDeque<>(10); // Bounded queue
Queue<String> priorityQueue = new PriorityQueue<>(); // Priority-based queue
// Deque implementations
Deque<String> deque = new ArrayDeque<>(); // Better than Stack
deque.push("first"); // Add to front
deque.push("second"); // Add to front
deque.pop(); // Remove from front
// Real-world example: Task scheduler
public class TaskScheduler {
private final Deque<Runnable> taskQueue = new ArrayDeque<>();
public void addTask(Runnable task) {
taskQueue.offerLast(task);
}
public Runnable getNextTask() {
return taskQueue.pollFirst();
}
public Runnable getHighestPriorityTask() {
return taskQueue.pollFirst();
}
}
Q8: Queue Implementations Comparison
| Queue Type | Characteristics | Use Case |
|---|---|---|
| LinkedList | Unbounded, FIFO | General purpose |
| ArrayDeque | Resizable array, faster than LinkedList | Stack/Queue implementation |
| PriorityQueue | Heap-based, min-ordered | Priority scheduling |
| BlockingQueue | Thread-safe, blocking operations | Producer-consumer patterns |
Multithreading Questions
Q9: synchronized vs ReentrantLock
// synchronized: implicit lock
synchronized void method() { }
// ReentrantLock: explicit lock with features
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}
When to use ReentrantLock:
- TryLock with timeout
- Fair ordering
- Multiple condition variables
- Interruptible lock acquisition
Real-world Example: Database connection pool management:
public class ConnectionPool {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
private final Queue<Connection> connections = new LinkedList<>();
private final int maxSize;
public Connection getConnection(long timeout, TimeUnit unit) throws InterruptedException {
lock.lock();
try {
long deadline = System.nanoTime() + unit.toNanos(timeout);
while (connections.isEmpty()) {
if (!notEmpty.await(timeout, unit)) {
throw new TimeoutException("No connection available");
}
}
return connections.poll();
} finally {
lock.unlock();
}
}
public void releaseConnection(Connection conn) {
lock.lock();
try {
connections.offer(conn);
notEmpty.signal();
} finally {
lock.unlock();
}
}
}
Q10: volatile vs synchronized
| Feature | volatile | synchronized |
|---|---|---|
| Atomicity | No | Yes |
| Visibility | Yes | Yes |
| Mutual Exclusion | No | Yes |
| Blocking | No | Yes |
| Performance | Faster | Slower |
Key Insight: volatile only guarantees visibility, not atomicity. For example, volatile int count++; is not atomic because it's actually three operations: read, increment, write.
Q11: Thread Pool Configuration
ExecutorService executor = new ThreadPoolExecutor(
4, // core pool size
8, // max pool size
60L, TimeUnit.SECONDS, // keep alive
new LinkedBlockingQueue<>(100) // work queue
);
Formula: Number of CPU cores + 1 for CPU-bound tasks. For I/O-bound: number of CPU cores * 2.
Real-world Example: Web server thread pool configuration:
// For CPU-intensive tasks (like data processing)
int cpuCores = Runtime.getRuntime().availableProcessors();
ExecutorService cpuPool = Executors.newFixedThreadPool(cpuCores + 1);
// For I/O-bound tasks (like database queries)
ExecutorService ioPool = Executors.newFixedThreadPool(cpuCores * 2);
// For mixed workloads
ThreadPoolExecutor mixedPool = new ThreadPoolExecutor(
cpuCores, // core threads
cpuCores * 2, // max threads
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy() // rejection policy
);
Q12: Deadlock Prevention
// Deadlock example - DON'T DO THIS
public void transferMoney(Account from, Account to, int amount) {
synchronized (from) {
synchronized (to) {
// Transfer logic
}
}
}
// Solution: Always acquire locks in consistent order
public void transferMoneySafe(Account from, Account to, int amount) {
Account first = from.getId() < to.getId() ? from : to;
Account second = from.getId() < to.getId() ? to : from;
synchronized (first) {
synchronized (second) {
// Transfer logic
}
}
}
JVM Internals
Q13: Memory Model
+-------------------+
| Method Area | (Class metadata, static variables)
+-------------------+
| Heap | (Objects, arrays)
| +-------------+ |
| | Young Gen | | (Eden + Survivor spaces)
| +-------------+ |
| | Old Gen | | (Long-lived objects)
+-------------------+
| Stack (per thread) | (Local variables, method calls)
+-------------------+
Q14: Garbage Collection Algorithms
| Algorithm | Best For | Trade-off |
|---|---|---|
| Serial | Small apps | Stop-the-world |
| Parallel | Throughput | Stop-the-world |
| G1 | Balanced | Predictable pauses |
| ZGC | Ultra-low latency | More memory |
Real-world Example: JVM tuning for different scenarios:
# Low latency application
java -XX:+UseZGC -XX:MaxGCPauseMillis=10 -Xmx4g MyApp
# High throughput application
java -XX:+UseParallelGC -XX:MaxGCPauseMillis=500 -Xmx8g MyApp
# Balanced configuration
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Xmx4g MyApp
Q15: Class Loading and Memory Leaks
// Memory leak example - static collection growing forever
public class DataCache {
private static final Map<String, Object> cache = new HashMap<>();
public static void addData(String key, Object value) {
cache.put(key, value); // Never cleared!
}
}
// Solution: Use WeakHashMap or bounded cache
public class DataCache {
private static final Map<String, Object> cache =
Collections.synchronizedMap(new WeakHashMap<>());
public static void addData(String key, Object value) {
cache.put(key, value);
}
}
Spring Boot Questions
Q16: @Autowired vs Constructor Injection
// Field injection (avoid)
@Autowired
private UserService userService;
// Constructor injection (preferred)
@Service
public class OrderService {
private final UserService userService;
public OrderService(UserService userService) {
this.userService = userService;
}
}
Why constructor injection: Immutability, testability, explicit dependencies.
Real-world Example: Service with multiple dependencies:
@Service
public class PaymentService {
private final PaymentRepository paymentRepository;
private final UserService userService;
private final NotificationService notificationService;
// Constructor injection with all dependencies
public PaymentService(PaymentRepository paymentRepository,
UserService userService,
NotificationService notificationService) {
this.paymentRepository = paymentRepository;
this.userService = userService;
this.notificationService = notificationService;
}
// Easy to test - just mock the dependencies
// No need for reflection or Spring context
}
Q17: @Transactional Propagation
| Propagation | Description |
|---|---|
| REQUIRED | Join existing or create new |
| REQUIRES_NEW | Always create new |
| NESTED | Nested transaction with savepoints |
| SUPPORTS | Join if exists, non-tx otherwise |
| NOT_SUPPORTED | Suspend current transaction |
Real-world Example: Order processing with nested transactions:
@Service
public class OrderService {
@Transactional
public void processOrder(Order order) {
// Main transaction
orderRepository.save(order);
// Nested transaction - can be rolled back independently
processPayment(order.getPayment());
// If payment fails, order is still saved
sendConfirmation(order);
}
@Transactional(propagation = Propagation.NESTED)
public void processPayment(Payment payment) {
paymentRepository.save(payment);
// If this fails, only payment is rolled back
}
}
Q18: REST API Best Practices
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
if (user == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(user);
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody UserDto userDto) {
User user = userService.create(userDto);
URI location = URI.create("/api/users/" + user.getId());
return ResponseEntity.created(location).body(user);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidation(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.badRequest().body(errors);
}
}
Java 8+ Features
Q19: Stream API
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// Filter and collect
List<String> filtered = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
// Map and reduce
int totalLength = names.stream()
.map(String::length)
.reduce(0, Integer::sum);
// Grouping
Map<Integer, List<String>> grouped = names.stream()
.collect(Collectors.groupingBy(String::length));
// Real-world example: Processing employee data
public class EmployeeAnalyzer {
public Map<String, Double> calculateAverageSalaryByDepartment(List<Employee> employees) {
return employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
}
public List<Employee> findTopEarners(List<Employee> employees, int count) {
return employees.stream()
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
.limit(count)
.collect(Collectors.toList());
}
}
Q20: Lambda Expressions
// Traditional approach
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
// Lambda approach
Runnable lambdaRunnable = () -> System.out.println("Hello");
// Method reference
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(System.out::println);
// Functional interfaces
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;
Q21: Optional Class
// Avoiding NullPointerException
public String getUserName(Long userId) {
return Optional.ofNullable(userRepository.findById(userId))
.map(User::getName)
.orElse("Unknown User");
}
// With default value
public String getUserNameWithDefault(Long userId) {
return Optional.ofNullable(userRepository.findById(userId))
.map(User::getName)
.orElseGet(() -> "User " + userId);
}
// Throwing exception
public User getUserOrThrow(Long userId) {
return Optional.ofNullable(userRepository.findById(userId))
.orElseThrow(() -> new UserNotFoundException("User not found: " + userId));
}
// Real-world example: Safe navigation
public class OrderService {
public String getOrderCustomerEmail(Long orderId) {
return Optional.ofNullable(orderRepository.findById(orderId))
.map(Order::getCustomer)
.map(Customer::getEmail)
.orElse(null);
}
}
Common Java Anti-Patterns
Q22: String Concatenation in Loops
// BAD: O(n^2) time complexity
String result = "";
for (String s : list) {
result += s; // Creates new String object each time
}
// GOOD: O(n) time complexity
StringBuilder sb = new StringBuilder();
for (String s : list) {
sb.append(s);
}
String result = sb.toString();
Q23: Catching Generic Exceptions
// BAD: Too broad
try {
// code
} catch (Exception e) {
// Handles everything
}
// GOOD: Specific exceptions
try {
// code
} catch (FileNotFoundException e) {
// Handle file not found
} catch (IOException e) {
// Handle other IO errors
} catch (SQLException e) {
// Handle database errors
}
Q24: Mutable Static State
// BAD: Thread unsafe
public class UserCache {
private static Map<String, User> cache = new HashMap<>();
public static User getUser(String id) {
return cache.get(id); // Not thread safe
}
}
// GOOD: Thread safe
public class UserCache {
private static final ConcurrentHashMap<String, User> cache = new ConcurrentHashMap<>();
public static User getUser(String id) {
return cache.get(id); // Thread safe
}
}
Practice Problems with Solutions
Problem 1: Implement LRU Cache
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true); // Access order
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
// Usage
LRUCache<String, Integer> cache = new LRUCache<>(3);
cache.put("a", 1);
cache.put("b", 2);
cache.put("c", 3);
cache.get("a"); // Access "a", making it most recently used
cache.put("d", 4); // Evicts "b" (least recently used)
Problem 2: Producer-Consumer Pattern
public class ProducerConsumer {
private final BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10);
public void produce() throws InterruptedException {
int value = 0;
while (true) {
queue.put(value++);
System.out.println("Produced: " + value);
Thread.sleep(100);
}
}
public void consume() throws InterruptedException {
while (true) {
int value = queue.take();
System.out.println("Consumed: " + value);
Thread.sleep(200);
}
}
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
Thread producer = Thread.startVirtualThread(() -> {
try {
pc.produce();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = Thread.startVirtualThread(() -> {
try {
pc.consume();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
Problem 3: Thread-safe Singleton
// Option 1: Enum (preferred)
public enum Singleton {
INSTANCE;
public void doSomething() {
// Business logic
}
}
// Option 2: Double-checked locking
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Problem 4: Custom Stream Operations
public class StreamUtils {
public static <T> Collector<T, ?, Optional<T>> toSingleton() {
return Collector.of(
() -> new AtomicReference<>(),
(ref, t) -> ref.set(t),
(ref1, ref2) -> {
ref1.set(ref2.get());
return ref1;
},
ref -> Optional.ofNullable(ref.get()),
Collector.Characteristics.UNORDERED
);
}
// Usage
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> sum = numbers.stream()
.collect(Collectors.summingInt(Integer::intValue));
}
Problem 5: Custom Predicate Composition
public class PredicateUtils {
public static <T> Predicate<T> and(Predicate<T>... predicates) {
return Arrays.stream(predicates)
.reduce(Predicate::and)
.orElse(t -> true);
}
public static <T> Predicate<T> or(Predicate<T>... predicates) {
return Arrays.stream(predicates)
.reduce(Predicate::or)
.orElse(t -> false);
}
public static <T> Predicate<T> not(Predicate<T> predicate) {
return predicate.negate();
}
// Usage
Predicate<String> isNotNull = Objects::nonNull;
Predicate<String> isNotEmpty = s -> !s.isEmpty();
Predicate<String> startsWithA = s -> s.startsWith("A");
Predicate<String> combined = and(isNotNull, isNotEmpty, startsWithA);
List<String> names = Arrays.asList("Alice", "Bob", null, "", "Charlie");
List<String> result = names.stream()
.filter(combined)
.collect(Collectors.toList());
// Result: ["Alice"]
}
Resources
- Java Mastery Roadmap - Complete Java learning path
- Java Topics - Deep dive into Java concepts
- Practice Problems - Coding practice
- SQL Interview Questions - Database preparation
- System Design Guide - Architecture rounds
- Senior Java Developer - Advanced Java path
Continue Your Prep
Apply what you learned with our structured roadmaps and practice problems.