Skip to content
Java 20 min read

Java Interview Questions 2026: Complete Guide with Code Examples

Top 50 Java interview questions covering OOP, Collections, Stream API, multithreading, and JVM internals.

By SDE Roadmap

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:

  1. Security: Class loading, network connections, file paths
  2. Hashing: Hash code cached, consistent across HashMap
  3. Thread Safety: No synchronization needed
  4. 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 interview Java questions OOP Collections multithreading JVM

Continue Your Prep

Apply what you learned with our structured roadmaps and practice problems.