gitGood.dev
Back to Blog

Top 50 Java Interview Questions for 2026

D
Dan
30 min read

Java isn't going anywhere. It's still the backbone of enterprise software, fintech, Android development, and massive distributed systems. If you're interviewing for a backend or full-stack role in 2026, there's a very good chance Java is on the table.

These are the 50 questions that actually come up in real interviews - not obscure trivia about serialization edge cases or deprecated APIs nobody uses. Each one includes a clear, direct answer you can study and internalize. Let's get into it.


Tier 1: Core Java Fundamentals

These are table stakes. If you can't nail these, interviewers will move on quickly.

1. What's the difference between ==, .equals(), and hashCode()?

== compares references - it checks whether two variables point to the exact same object in memory. .equals() compares logical equality - whether two objects are meaningfully the same. hashCode() returns an integer used for hash-based collections like HashMap and HashSet.

The contract: if two objects are equal according to .equals(), they must have the same hashCode(). The reverse isn't required - two unequal objects can share a hash code (that's a collision).

String a = new String("hello");
String b = new String("hello");

a == b;        // false - different objects in memory
a.equals(b);   // true - same content
a.hashCode() == b.hashCode(); // true - required by the contract

If you override .equals(), always override hashCode() too. Forgetting this is one of the most common bugs in Java code.

2. Why are Strings immutable? What is the String pool?

Strings in Java are immutable - once created, their value can't change. This enables the String pool (also called the intern pool), a special area in heap memory where Java caches string literals. When you write "hello" twice, both references point to the same object.

Why immutability matters:

  • Thread safety - immutable objects are inherently safe to share across threads
  • Caching - hashCode() can be computed once and cached
  • Security - strings used in class loading, network connections, and file paths can't be tampered with
String a = "hello";       // goes to String pool
String b = "hello";       // reuses same pool object
String c = new String("hello"); // creates new object on heap

a == b;  // true - same pool reference
a == c;  // false - c is a separate heap object

3. StringBuilder vs StringBuffer - when do you use each?

Both are mutable alternatives to String for building strings incrementally. The difference is thread safety.

  • StringBuilder - not synchronized, faster, use this 99% of the time
  • StringBuffer - synchronized, thread-safe, use only when multiple threads modify the same buffer (which is rare)
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" World");
String result = sb.toString(); // "Hello World"

In practice, if you need thread-safe string building, you're probably better off using local variables or other concurrency patterns rather than StringBuffer.

4. What are primitive types vs wrapper classes? What is autoboxing?

Java has 8 primitive types: byte, short, int, long, float, double, char, boolean. These live on the stack and hold values directly.

Wrapper classes (Integer, Double, Boolean, etc.) are objects that wrap primitives. They live on the heap and can be null.

Autoboxing is Java's automatic conversion between primitives and wrappers:

Integer x = 42;       // autoboxing: int -> Integer
int y = x;            // unboxing: Integer -> int

// Watch out for null unboxing
Integer z = null;
int w = z;            // NullPointerException!

Gotcha: Integer caches values from -128 to 127, so == works for small values but fails for larger ones:

Integer a = 127;
Integer b = 127;
a == b;  // true - cached

Integer c = 128;
Integer d = 128;
c == d;  // false - different objects

5. Explain final, finally, and finalize

Three completely different things that share a name:

  • final - keyword that means "can't change." A final variable can't be reassigned, a final method can't be overridden, a final class can't be extended.
  • finally - block in try-catch that always runs, regardless of whether an exception was thrown. Used for cleanup.
  • finalize - deprecated method called by the garbage collector before reclaiming an object. Don't use it. Use try-with-resources or Cleaner instead.
final int x = 10;
// x = 20; // compilation error

try {
    riskyOperation();
} catch (Exception e) {
    handleError(e);
} finally {
    cleanup(); // always runs
}

6. What does the static keyword do?

static means "belongs to the class, not to an instance." It has four uses:

  • Static variables - shared across all instances of a class
  • Static methods - called on the class itself, can't access instance members
  • Static blocks - run once when the class is loaded, used for initialization
  • Static inner classes - nested classes that don't hold a reference to the outer class
public class Counter {
    static int count = 0;          // shared across all instances

    static {                       // runs once at class load
        System.out.println("Class loaded");
    }

    static void increment() {      // called as Counter.increment()
        count++;
    }
}

7. What's the difference between method overloading and overriding?

Overloading - same method name, different parameters. Resolved at compile time (static polymorphism).

Overriding - subclass provides its own implementation of a parent's method. Resolved at runtime (dynamic polymorphism).

// Overloading - same class, different params
class Calculator {
    int add(int a, int b) { return a + b; }
    double add(double a, double b) { return a + b; }
}

// Overriding - subclass replaces parent method
class Animal {
    void speak() { System.out.println("..."); }
}
class Dog extends Animal {
    @Override
    void speak() { System.out.println("Woof!"); }
}

Key rules for overriding: same method signature, return type must be the same or a subtype (covariant return), access can't be more restrictive, and you can't override static or final methods.

8. Abstract class vs interface - what changed after Java 8?

Before Java 8, the rule was simple: interfaces had only abstract methods, abstract classes could have implementations. Java 8 changed the game with default and static methods in interfaces.

Use an abstract class when you need constructors, instance fields, or non-public methods. Classes can only extend one abstract class.

Use an interface when you're defining a capability or contract. Classes can implement multiple interfaces.

// Interface with default method (Java 8+)
interface Loggable {
    default void log(String msg) {
        System.out.println("[LOG] " + msg);
    }
    void process(); // abstract
}

// Abstract class with state
abstract class Vehicle {
    protected int speed;
    abstract void accelerate();
    void stop() { speed = 0; }
}

Post-Java 8, the line is blurrier. The practical rule: prefer interfaces for defining behavior contracts, and use abstract classes when you need shared state.

9. What are the four access modifiers in Java?

  • public - accessible from everywhere
  • protected - accessible within the same package and by subclasses
  • default (no modifier) - accessible within the same package only
  • private - accessible only within the same class

Think of it as a spectrum from most open to most restricted: public > protected > default > private.

10. Explain checked vs unchecked exceptions and try-with-resources

Checked exceptions - extend Exception, must be caught or declared in the method signature. The compiler enforces this. Examples: IOException, SQLException.

Unchecked exceptions - extend RuntimeException, don't require explicit handling. Examples: NullPointerException, IllegalArgumentException.

Try-with-resources (Java 7+) automatically closes resources that implement AutoCloseable:

// Old way - verbose and error-prone
BufferedReader br = null;
try {
    br = new BufferedReader(new FileReader("file.txt"));
    String line = br.readLine();
} finally {
    if (br != null) br.close();
}

// Modern way - clean and safe
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
    String line = br.readLine();
} // br.close() called automatically

11. How do generics work? What is type erasure?

Generics let you write type-safe code that works with different types. They're a compile-time feature - at runtime, generic type info is erased (replaced with Object or the upper bound).

List<String> names = new ArrayList<>();
names.add("Alice");
// names.add(42); // compile error - type safety

// Type erasure means at runtime, this is just List<Object>

This is why you can't do new T() or instanceof T - the type info doesn't exist at runtime. It also means List<String> and List<Integer> are the same class at runtime.

Bounded types let you constrain generics:

// T must be Comparable
public <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

12. When should you use enums?

Use enums when you have a fixed set of constants that belong together. They're type-safe, can have methods and fields, and work great in switch statements.

public enum Status {
    PENDING("Waiting"),
    ACTIVE("Running"),
    COMPLETED("Done");

    private final String description;

    Status(String description) {
        this.description = description;
    }

    public String getDescription() { return description; }
}

// Usage
Status s = Status.ACTIVE;
switch (s) {
    case PENDING -> handlePending();
    case ACTIVE -> handleActive();
    case COMPLETED -> handleCompleted();
}

Enums are singletons by design, implement Serializable, and are thread-safe. They're often the best way to implement the singleton pattern.

13. Is Java pass by value or pass by reference?

Java is always pass by value. Always. No exceptions.

The confusion comes from object references. When you pass an object to a method, you're passing a copy of the reference (the pointer) by value. You can modify the object through that reference, but you can't make the original variable point to a different object.

void changeValue(StringBuilder sb) {
    sb.append(" World");  // modifies the original object
    sb = new StringBuilder("New"); // only changes the local copy
}

StringBuilder original = new StringBuilder("Hello");
changeValue(original);
System.out.println(original); // "Hello World" - not "New"

14. How do you create an immutable object?

An immutable object can't be modified after creation. Here's the recipe:

  1. Make the class final (prevent subclassing)
  2. Make all fields private and final
  3. Don't provide setters
  4. Return defensive copies of mutable fields
  5. Initialize everything in the constructor
public final class Money {
    private final BigDecimal amount;
    private final String currency;

    public Money(BigDecimal amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public BigDecimal getAmount() { return amount; }
    public String getCurrency() { return currency; }
}

Immutable objects are thread-safe, cacheable, and make great map keys.

15. What are record classes (Java 14+)?

Records are a compact way to create immutable data carriers. The compiler generates the constructor, getters, equals(), hashCode(), and toString() for you.

public record Point(int x, int y) {}

// That's it. You get:
Point p = new Point(3, 4);
p.x();          // 3
p.y();          // 4
p.toString();   // "Point[x=3, y=4]"

Records can have custom constructors (compact form), static methods, and implement interfaces. They can't extend other classes or have mutable fields. Use them for DTOs, value objects, and anywhere you'd previously write a boilerplate POJO.


Tier 2: Collections and Data Structures

Collections questions test whether you actually understand the data structures you use every day.

16. ArrayList vs LinkedList - when do you use each?

ArrayList - backed by a dynamic array. O(1) random access, O(1) amortized append, O(n) insert/delete in the middle.

LinkedList - doubly-linked list. O(n) random access, O(1) insert/delete at known positions, higher memory overhead per element.

In practice, ArrayList wins almost always. Modern CPUs love sequential memory access (cache locality), which arrays provide. LinkedList's theoretical O(1) insertions rarely overcome the cache miss penalty.

Use LinkedList only when you're doing heavy insertion/removal at both ends (like a deque) and never accessing by index.

17. How does HashMap work internally?

This is a favorite interview question. Here's how it works:

  1. Hashing - key.hashCode() is computed and spread across buckets using bitwise operations
  2. Buckets - the hash determines which bucket (array index) the entry goes into
  3. Collisions - when multiple keys map to the same bucket, entries are stored in a linked list
  4. Treeification (Java 8+) - when a bucket has more than 8 entries, the linked list converts to a red-black tree (O(log n) lookup instead of O(n))
  5. Resizing - when the load factor (default 0.75) is exceeded, the array doubles and entries are rehashed
Map<String, Integer> map = new HashMap<>(16, 0.75f);
map.put("key", 42);
// hashCode("key") -> bucket index -> store entry

Key requirement: keys must have consistent hashCode() and equals() implementations. Mutable keys in a HashMap are a recipe for lost entries.

18. HashMap vs TreeMap vs LinkedHashMap

  • HashMap - O(1) average lookup, no ordering guarantee
  • TreeMap - O(log n) lookup, keys sorted in natural order (or by a Comparator)
  • LinkedHashMap - O(1) average lookup, maintains insertion order (or access order for LRU caches)
Map<String, Integer> hash = new HashMap<>();    // fast, unordered
Map<String, Integer> tree = new TreeMap<>();     // sorted by key
Map<String, Integer> linked = new LinkedHashMap<>(); // insertion order

Choose based on your needs: speed (HashMap), sorted keys (TreeMap), or predictable iteration (LinkedHashMap).

19. How does HashSet work under the hood?

HashSet is literally a HashMap where the values are a dummy constant object. When you call set.add(element), it calls map.put(element, PRESENT) internally.

This means HashSet inherits all of HashMap's properties: O(1) average add/remove/contains, requires proper hashCode() and equals(), and has no ordering guarantee.

20. ConcurrentHashMap vs Hashtable vs Collections.synchronizedMap

All three are thread-safe, but the mechanism differs:

  • Hashtable - every method is synchronized. One lock for the entire table. Very slow under contention. Legacy, don't use it.
  • Collections.synchronizedMap() - wraps a HashMap with a single lock. Same performance problem as Hashtable.
  • ConcurrentHashMap - uses fine-grained locking (lock striping in Java 7, CAS operations + synchronized blocks on individual bins in Java 8+). Much better concurrent performance.
// Don't do this
Map<String, String> old = new Hashtable<>();

// Do this instead
Map<String, String> modern = new ConcurrentHashMap<>();

ConcurrentHashMap doesn't allow null keys or values (unlike HashMap). This is intentional - null creates ambiguity in concurrent contexts.

21. Iterator vs ListIterator, fail-fast vs fail-safe

Iterator traverses forward only. ListIterator can go both directions and supports add/set operations during iteration.

Fail-fast iterators (ArrayList, HashMap) throw ConcurrentModificationException if the collection is modified during iteration. They detect this using a modification counter.

Fail-safe iterators (ConcurrentHashMap, CopyOnWriteArrayList) work on a copy or snapshot, so modifications during iteration don't cause exceptions - but you might not see the latest changes.

// Fail-fast - throws ConcurrentModificationException
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
    list.remove(s); // boom
}

// Safe way to remove during iteration
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (it.next().equals("b")) it.remove(); // safe
}

22. Comparable vs Comparator

Comparable - the object defines its own natural ordering by implementing compareTo(). One ordering per class.

Comparator - an external comparison strategy. You can define multiple different orderings.

// Comparable - natural ordering
public class Employee implements Comparable<Employee> {
    String name;
    int salary;

    @Override
    public int compareTo(Employee other) {
        return Integer.compare(this.salary, other.salary);
    }
}

// Comparator - custom ordering
Comparator<Employee> byName = Comparator.comparing(e -> e.name);
Comparator<Employee> bySalaryDesc = Comparator.comparingInt(Employee::getSalary).reversed();

employees.sort(byName);

23. What Queue and Deque implementations should you know?

  • LinkedList - implements both Queue and Deque. General purpose.
  • ArrayDeque - faster than LinkedList for stack and queue operations. Preferred choice.
  • PriorityQueue - elements ordered by priority (natural order or Comparator). Not FIFO.
  • BlockingQueue (LinkedBlockingQueue, ArrayBlockingQueue) - thread-safe, blocks on empty/full. Essential for producer-consumer patterns.
Queue<String> queue = new ArrayDeque<>();
queue.offer("first");
queue.offer("second");
queue.poll();  // "first"

Deque<String> stack = new ArrayDeque<>();
stack.push("bottom");
stack.push("top");
stack.pop();   // "top"

24. Stream API - map, filter, reduce, collect

Streams provide a functional approach to processing collections. They're lazy (operations are chained and only execute on a terminal operation) and can be parallelized.

List<String> names = List.of("Alice", "Bob", "Charlie", "David");

// filter + map + collect
List<String> result = names.stream()
    .filter(name -> name.length() > 3)
    .map(String::toUpperCase)
    .sorted()
    .collect(Collectors.toList());
// ["ALICE", "CHARLIE", "DAVID"]

// reduce
int sum = IntStream.rangeClosed(1, 10)
    .reduce(0, Integer::sum); // 55

// grouping
Map<Integer, List<String>> byLength = names.stream()
    .collect(Collectors.groupingBy(String::length));

Key rule: streams are single-use. You can't reuse a stream after a terminal operation.

25. Optional - when to use it and when not to

Optional is a container that may or may not hold a value. It's designed to be a return type for methods that might not have a result.

Optional<User> findUser(String id) {
    return Optional.ofNullable(userMap.get(id));
}

// Usage
String name = findUser("123")
    .map(User::getName)
    .orElse("Unknown");

Do use Optional for: method return types where "no result" is a valid outcome.

Don't use Optional for: fields, method parameters, collections (return empty collections instead), or when null is genuinely impossible. Don't use Optional.get() without checking - use orElse(), orElseGet(), or ifPresent().


Tier 3: Concurrency and Multithreading

Concurrency questions separate mid-level from senior candidates. These come up in almost every backend interview.

26. Thread vs Runnable vs Callable

Three ways to define work for a thread:

  • Thread - extend Thread class. Inflexible because Java doesn't support multiple inheritance.
  • Runnable - implement Runnable interface. Returns void, can't throw checked exceptions.
  • Callable - implement Callable<V> interface. Returns a value and can throw checked exceptions.
// Runnable - no return value
Runnable task = () -> System.out.println("Running");

// Callable - returns a value
Callable<Integer> computation = () -> {
    Thread.sleep(1000);
    return 42;
};

ExecutorService executor = Executors.newFixedThreadPool(4);
Future<Integer> future = executor.submit(computation);
int result = future.get(); // blocks until done, returns 42

Always prefer Runnable/Callable with an ExecutorService over extending Thread directly.

27. How does the synchronized keyword work?

synchronized provides mutual exclusion - only one thread can execute a synchronized block/method at a time. It works by acquiring a monitor lock on an object.

// Synchronized method - locks on 'this'
public synchronized void increment() {
    count++;
}

// Synchronized block - locks on specific object
public void increment() {
    synchronized (this) {
        count++;
    }
}

// Lock on a specific object for finer control
private final Object lock = new Object();
public void safeUpdate() {
    synchronized (lock) {
        // critical section
    }
}

synchronized also establishes a happens-before relationship, ensuring memory visibility between threads. For more flexible locking, consider ReentrantLock from java.util.concurrent.locks.

28. What does the volatile keyword do?

volatile guarantees visibility - when one thread writes to a volatile variable, all other threads immediately see the new value. Without volatile, threads might read stale cached values.

private volatile boolean running = true;

// Thread 1
public void stop() {
    running = false; // immediately visible to other threads
}

// Thread 2
public void run() {
    while (running) { // guaranteed to see the updated value
        doWork();
    }
}

volatile does not provide atomicity. count++ on a volatile variable is still not thread-safe because it's a read-modify-write operation. Use AtomicInteger for that.

29. What is a ThreadPool and how does ExecutorService work?

Thread pools reuse a fixed set of threads to execute tasks, avoiding the overhead of creating and destroying threads constantly.

// Fixed thread pool
ExecutorService pool = Executors.newFixedThreadPool(4);

// Submit tasks
pool.submit(() -> processOrder(order1));
pool.submit(() -> processOrder(order2));

// Shutdown gracefully
pool.shutdown();
pool.awaitTermination(30, TimeUnit.SECONDS);

Common pool types:

  • newFixedThreadPool(n) - fixed number of threads. Good default choice.
  • newCachedThreadPool() - grows/shrinks as needed. Good for short-lived tasks.
  • newSingleThreadExecutor() - one thread, tasks execute sequentially.
  • newScheduledThreadPool(n) - for delayed or periodic tasks.

In production, prefer ThreadPoolExecutor with explicit parameters over the Executors factory methods, so you control queue size and rejection policy.

30. How does CompletableFuture work?

CompletableFuture is Java's answer to async/await. It represents a future result that you can chain operations on, combine with other futures, and handle errors.

CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> fetchUserFromDB(userId))
    .thenApply(user -> user.getName())
    .thenApply(String::toUpperCase)
    .exceptionally(ex -> "UNKNOWN");

// Combining multiple futures
CompletableFuture<String> nameFuture = fetchNameAsync(id);
CompletableFuture<Integer> ageFuture = fetchAgeAsync(id);

CompletableFuture<String> combined = nameFuture
    .thenCombine(ageFuture, (name, age) -> name + " is " + age);

Use thenApply for synchronous transforms, thenCompose for chaining async operations (like flatMap), and thenCombine for combining independent futures.

31. What is a deadlock and how do you prevent it?

A deadlock occurs when two or more threads are each waiting for a lock held by the other, creating a circular wait where nobody makes progress.

// Classic deadlock
// Thread 1: locks A, then tries to lock B
// Thread 2: locks B, then tries to lock A

synchronized (lockA) {
    synchronized (lockB) { /* ... */ }
}

// Meanwhile, another thread does:
synchronized (lockB) {
    synchronized (lockA) { /* ... */ }  // deadlock!
}

Prevention strategies:

  • Lock ordering - always acquire locks in a consistent global order
  • Timeouts - use tryLock() with a timeout instead of blocking indefinitely
  • Lock-free algorithms - use atomic operations and concurrent collections
  • Reduce lock scope - hold locks for the shortest time possible

32. What are race conditions?

A race condition happens when the behavior of code depends on the timing of thread execution. The result is unpredictable and changes between runs.

// Race condition - count++ is not atomic
private int count = 0;

// Two threads running this simultaneously
public void increment() {
    count++; // read -> modify -> write (three operations)
}

Fixes:

  • synchronized block
  • AtomicInteger.incrementAndGet()
  • ReentrantLock

The key insight: any time multiple threads read and write shared mutable state without synchronization, you have a potential race condition.

33. CountDownLatch, CyclicBarrier, and Semaphore

Three coordination utilities from java.util.concurrent:

CountDownLatch - a one-shot gate. Threads wait until a counter reaches zero.

CountDownLatch latch = new CountDownLatch(3);

// Three worker threads each call latch.countDown() when done
// Main thread waits:
latch.await(); // blocks until count reaches 0

CyclicBarrier - reusable synchronization point. All threads wait until everyone arrives, then they all proceed together. Good for phased computations.

Semaphore - controls access to a limited number of resources. Like a bouncer with a counter.

Semaphore semaphore = new Semaphore(3); // 3 permits

semaphore.acquire(); // get a permit (blocks if none available)
try {
    accessLimitedResource();
} finally {
    semaphore.release(); // return the permit
}

34. What is ThreadLocal?

ThreadLocal gives each thread its own independent copy of a variable. No synchronization needed because threads never share the data.

private static final ThreadLocal<SimpleDateFormat> dateFormat =
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

// Each thread gets its own SimpleDateFormat instance
String date = dateFormat.get().format(new Date());

Common uses: per-thread database connections, user context in web apps, non-thread-safe objects like SimpleDateFormat.

Warning: ThreadLocal values can cause memory leaks in thread pools because threads are reused. Always call remove() when done, especially in web applications.

35. What are virtual threads (Java 21+)?

Virtual threads are lightweight threads managed by the JVM rather than the OS. You can create millions of them without exhausting system resources.

// Old way - limited by OS thread count
ExecutorService pool = Executors.newFixedThreadPool(200);

// New way - millions of virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 100_000; i++) {
        executor.submit(() -> {
            // Each task gets its own virtual thread
            String result = httpClient.send(request);
            process(result);
        });
    }
}

Virtual threads are ideal for I/O-bound workloads (HTTP calls, database queries) where threads spend most of their time waiting. They're mounted on platform (OS) threads and automatically unmount when blocked.

Key points: don't pool virtual threads (create new ones instead), avoid synchronized in favor of ReentrantLock for long-blocking operations, and they won't help with CPU-bound work.


Tier 4: JVM and Performance

These questions show up more in senior-level interviews, especially at companies that deal with scale.

36. Explain the JVM memory model

The JVM divides memory into several areas:

  • Heap - where objects live. Shared across all threads. Divided into Young Generation (Eden + Survivor spaces) and Old Generation.
  • Stack - each thread gets its own stack. Stores local variables, method call frames, and references.
  • Metaspace (replaced PermGen in Java 8) - stores class metadata, method bytecode, and the constant pool. Grows dynamically.
  • Program Counter - each thread has one, tracks the current instruction.
  • Native Method Stack - for native (JNI) method calls.

The key interview point: objects on the heap are shared between threads (synchronization needed), while stack data is thread-private (inherently safe).

37. How does garbage collection work? What are G1 and ZGC?

Garbage collection automatically reclaims memory from objects that are no longer reachable. The basic idea: start from GC roots (static fields, local variables, active threads), trace all reachable objects, and free everything else.

G1 (Garbage First) - the default since Java 9. Divides the heap into regions. Does mostly concurrent collection with predictable pause times. Good for heaps from a few hundred MB to several GB.

ZGC - ultra-low latency collector. Pause times under 1ms regardless of heap size. Handles multi-terabyte heaps. Great for latency-sensitive applications.

Generational hypothesis: most objects die young. Young generation collection (minor GC) is fast and frequent. Old generation collection (major GC) is slower and less frequent.

38. How does the ClassLoader hierarchy work?

Java uses a delegation model with three main classloaders:

  1. Bootstrap ClassLoader - loads core Java classes (java.lang, java.util) from the JDK
  2. Platform ClassLoader (was Extension ClassLoader) - loads platform modules
  3. Application ClassLoader - loads your application classes from the classpath

When a class needs to be loaded, the request goes up the chain (child delegates to parent). If the parent can't find it, the child tries. This prevents application code from replacing core Java classes.

Custom classloaders are used in app servers, plugin systems, and hot-reloading frameworks.

39. What is JIT compilation?

JIT (Just-In-Time) compilation is how the JVM optimizes code at runtime. Java bytecode starts out being interpreted, but the JVM identifies "hot" methods (frequently called) and compiles them to native machine code.

The JVM uses two compilers:

  • C1 (Client) - fast compilation, basic optimizations. Used for methods called a few times.
  • C2 (Server) - slower compilation, aggressive optimizations (inlining, escape analysis, loop unrolling). Used for very hot methods.

This is called tiered compilation, and it's why Java can sometimes match or beat C++ performance for long-running applications - the JIT has runtime profiling data that static compilers don't.

40. How do memory leaks happen in Java?

Java has garbage collection, but memory leaks still happen. They occur when objects are no longer needed but still have a reachable reference.

Common causes:

  • Static collections that grow without bounds
  • Listeners/callbacks that are registered but never removed
  • ThreadLocal values in thread pools (threads are reused, values persist)
  • Inner classes holding implicit references to outer class instances
  • Unclosed resources (streams, connections)
  • Custom caches without eviction policies
// Classic leak - static map grows forever
private static final Map<String, Object> cache = new HashMap<>();

public void processRequest(String key, Object data) {
    cache.put(key, data); // never removed
}

Use weak references (WeakHashMap), bounded caches (Caffeine, Guava), and profiling tools to find leaks.

41. What tools do you use for JVM profiling and monitoring?

Key tools every Java developer should know:

  • jstack - dumps thread stacks. Essential for diagnosing deadlocks and thread issues.
  • jmap - creates heap dumps. Use with memory analysis tools.
  • jconsole / VisualVM - graphical monitoring of heap, threads, CPU, and GC activity.
  • jstat - command-line GC statistics.
  • Java Flight Recorder (JFR) - low-overhead production profiling built into the JVM. Records events like method execution, GC pauses, and lock contention.
  • async-profiler - sampling profiler for CPU and allocation profiling with minimal overhead.

In production, JFR is the go-to because it has negligible overhead and can run continuously.

42. What is String deduplication?

String deduplication is a G1 GC feature that identifies String objects with identical char[] (or byte[]) arrays and makes them share the underlying array. This can significantly reduce memory usage since strings often make up 25-40% of a typical Java heap.

Enable it with: -XX:+UseStringDeduplication

It only works with G1 (and ZGC in newer versions). It's not the same as String.intern() - deduplication works on the internal array, not the String object itself, and it happens automatically during GC.


Tier 5: Spring and Modern Java

If you're interviewing for a backend Java role, Spring Boot questions are nearly guaranteed.

43. What is dependency injection and IoC?

Inversion of Control (IoC) means the framework controls object creation and lifecycle, not your code. You declare what you need, and the framework provides it.

Dependency Injection (DI) is how IoC works in practice - the framework "injects" dependencies into your objects instead of you creating them with new.

// Without DI - tightly coupled
public class OrderService {
    private PaymentGateway gateway = new StripeGateway(); // hard-coded
}

// With DI - loosely coupled
@Service
public class OrderService {
    private final PaymentGateway gateway;

    @Autowired // Spring injects the right implementation
    public OrderService(PaymentGateway gateway) {
        this.gateway = gateway;
    }
}

Constructor injection is preferred over field injection because it makes dependencies explicit, supports immutability, and works with testing.

44. How does Spring Boot auto-configuration work?

Spring Boot scans your classpath and automatically configures beans based on what libraries are present. Add spring-boot-starter-data-jpa to your dependencies, and Spring Boot automatically configures a DataSource, EntityManager, and transaction manager.

Under the hood:

  1. @SpringBootApplication includes @EnableAutoConfiguration
  2. Spring reads META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  3. Each auto-configuration class has @Conditional annotations that check for classes, beans, or properties
  4. Configurations only activate when conditions are met

You can override any auto-configured bean by defining your own. Use application.properties or application.yml to customize settings.

45. @Component vs @Service vs @Repository vs @Controller

Functionally, they're all the same - they mark a class as a Spring-managed bean. The difference is semantic:

  • @Component - generic bean. Use when nothing else fits.
  • @Service - business logic layer. No special behavior, just clarity.
  • @Repository - data access layer. Spring adds automatic exception translation (converting database-specific exceptions to Spring's DataAccessException).
  • @Controller - web layer. Handles HTTP requests when combined with @RequestMapping.
@Repository
public class UserRepository { /* data access */ }

@Service
public class UserService { /* business logic */ }

@RestController // @Controller + @ResponseBody
public class UserController { /* HTTP endpoints */ }

46. What are Spring Bean scopes and lifecycle?

Scopes:

  • singleton (default) - one instance per Spring container
  • prototype - new instance every time it's requested
  • request - one per HTTP request (web apps)
  • session - one per HTTP session (web apps)

Lifecycle:

  1. Bean instantiation
  2. Dependency injection
  3. @PostConstruct method called
  4. Bean is ready to use
  5. @PreDestroy method called on shutdown
@Service
@Scope("prototype") // new instance each time
public class ReportGenerator {

    @PostConstruct
    public void init() {
        // runs after all dependencies are injected
    }

    @PreDestroy
    public void cleanup() {
        // runs before bean is destroyed
    }
}

47. How do you build a REST API with Spring?

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        return userService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserRequest request) {
        User user = userService.create(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }

    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ErrorResponse> handleValidation(ValidationException ex) {
        return ResponseEntity.badRequest()
            .body(new ErrorResponse(ex.getMessage()));
    }
}

Key annotations: @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PathVariable, @RequestParam, @RequestBody, @Valid.

48. What are the basics of Spring Security?

Spring Security provides authentication (who are you?) and authorization (what can you do?) for Spring applications.

Core concepts:

  • SecurityFilterChain - configures HTTP security rules
  • UserDetailsService - loads user data for authentication
  • PasswordEncoder - hashes passwords (use BCrypt)
  • @PreAuthorize - method-level security
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .build();
    }
}

49. What microservices patterns should you know?

The big ones that come up in interviews:

  • Circuit Breaker - stops calling a failing service after a threshold, returns fallback. Prevents cascade failures. (Resilience4j is the standard library.)
  • Service Discovery - services register themselves and find each other dynamically (Eureka, Consul, or Kubernetes DNS).
  • API Gateway - single entry point that routes requests, handles auth, rate limiting (Spring Cloud Gateway, Kong).
  • Saga Pattern - manages distributed transactions across services using compensating actions instead of two-phase commit.
  • Event-Driven Architecture - services communicate via events/messages (Kafka, RabbitMQ) instead of synchronous HTTP.

Know when to use microservices (large teams, independent deployment needs) and when a modular monolith is simpler and better (most cases).

50. What recent Java features should you know?

Java has been releasing features every 6 months. Here are the ones that come up in interviews:

Sealed classes (Java 17) - restrict which classes can extend/implement them.

public sealed interface Shape permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}

Pattern matching for instanceof (Java 16):

if (obj instanceof String s) {
    System.out.println(s.toUpperCase()); // s is already cast
}

Switch expressions (Java 14):

String result = switch (day) {
    case MONDAY, FRIDAY -> "Work hard";
    case SATURDAY, SUNDAY -> "Relax";
    default -> "Midweek grind";
};

Text blocks (Java 15):

String json = """
    {
        "name": "Alice",
        "age": 30
    }
    """;

Java Platform Module System (JPMS, Java 9) - adds strong encapsulation to packages. In practice, most applications don't use modules directly yet, but libraries and the JDK itself do.


How to Approach Java Interviews

Knowing the answers is only half the battle. Here's how to actually perform well:

Think out loud. Interviewers want to see your reasoning process, not just the final answer. Walk through your thought process.

Know the trade-offs. "It depends" is often the right answer - as long as you can explain what it depends on. ArrayList vs LinkedList, synchronized vs ConcurrentHashMap, microservices vs monolith - always discuss trade-offs.

Write code, not paragraphs. When explaining something like HashMap internals or a concurrency pattern, sketch the code. It demonstrates real understanding.

Go deep on a few topics. It's better to have deep knowledge of collections, concurrency, and Spring than shallow knowledge of everything. Interviewers follow up, and surface-level answers fall apart under follow-ups.

Practice under pressure. Reading about these concepts is different from explaining them in a live interview. Practice answering out loud or on a whiteboard.

If you're looking for a place to practice Java questions in an interview-like setting, check out gitGood.dev. You can work through Java fundamentals, data structures, and backend concepts with instant feedback and track your progress over time.

Good luck out there. You've got this.