Java Interview Questions
Java interviews test your depth of core language features, object-oriented design, concurrency understanding, and problem-solving abilities. Senior engineers are expected to not just know syntax but also reason about performance, memory, and best practices. This guide covers the most common and challenging Java interview questions.
What Java interviews cover
Core Java & OOP
Inheritance, polymorphism, encapsulation, abstraction, interfaces vs abstract classes, and object life cycle.
Collections Framework
HashMap internals, ConcurrentHashMap, ArrayList vs LinkedList, comparator vs comparable, and fail-fast vs fail-safe iterators.
Concurrency & Multithreading
Thread lifecycle, synchronized, volatile, locks (ReentrantLock, ReadWriteLock), executors, fork/join, and concurrent collections.
JVM Internals & Performance
Class loading, memory model (heap, stack, metaspace), garbage collection algorithms (G1, CMS, ZGC), profiling, and tuning.
Sample Java interview questions
- Explain the difference between abstract class and interface in Java. When would you use each?What a strong answer covers
- Abstract classes can have both abstract and concrete methods; interfaces originally had only abstract methods (Java 8+ allows default and static methods).
- A class can extend only one abstract class but can implement multiple interfaces.
- Abstract classes can have instance variables, constructors, and access modifiers other than public; interface fields are implicitly public static final.
- Use abstract class for related classes sharing state or behavior; use interface for defining capabilities or contracts across unrelated classes.
- Interfaces are preferred for polymorphism and loose coupling; abstract classes are better for code reuse in a class hierarchy.
View a sample answer
Abstract classes and interfaces are both used to achieve abstraction in Java, but they have key differences. An abstract class can contain both abstract and concrete methods, while an interface traditionally had only abstract methods, but since Java 8, interfaces can have default and static methods. A class can extend only one abstract class, but it can implement multiple interfaces, allowing for a form of multiple inheritance. Abstract classes can have constructors, instance variables, and access modifiers other than public, whereas interface fields are implicitly public, static, and final. You would use an abstract class when classes share a common base with state and behavior, such as in a class hierarchy where subclasses reuse code. Interfaces are used to define capabilities or contracts that can be implemented by unrelated classes, promoting loose coupling and polymorphism. A common pitfall is overusing abstract classes when an interface would suffice, leading to tight coupling. Conversely, using interfaces with default methods can lead to ambiguity if multiple interfaces define the same default method.
- How does HashMap work internally? How does resizing affect performance?What a strong answer covers
- HashMap stores key-value pairs in an array of Node objects (buckets), using key's hashCode() to compute the bucket index.
- Collisions are handled by chaining: each bucket is a linked list or tree (if bucket size > 8, becomes a red-black tree).
- Performance depends on hash distribution and load factor (default 0.75); when size exceeds capacity * load factor, capacity is doubled (resize).
- Resizing involves rehashing all existing entries, which is O(n) and can cause momentary latency spikes.
- After resize, buckets are redistributed; good hashCode reduces collisions and improves performance.
View a sample answer
HashMap internally uses an array of buckets, where each bucket is a linked list or tree structure. When a key is inserted, its hash code is computed using key.hashCode(), and then an index is derived by taking the hash AND (capacity-1) to fit within the array bounds. If multiple keys have the same hash, they are stored in the same bucket as a linked list. If a bucket grows too large (threshold 8), it is converted to a red-black tree for better search performance (O(log n) vs O(n)). Resizing occurs when the number of entries exceeds the product of capacity and load factor (default 0.75). During resize, the capacity is doubled, and all entries are rehashed and redistributed into new buckets. This is an expensive O(n) operation that can cause performance spikes in real-time systems. A common pitfall is using a key with a poor hash code (e.g., always returning a constant), which leads to all entries in one bucket, degrading performance to O(n). To mitigate, ensure keys have a well-distributed hashCode and consider initial capacity if the expected size is known.
- Write a thread-safe singleton in Java (double-checked locking, Bill Pugh method).What a strong answer covers
- Double-checked locking reduces synchronization overhead by first checking without locking, then synchronizing if needed.
- volatile keyword ensures visibility of the instance field across threads.
- Bill Pugh singleton uses a static inner helper class, which is loaded lazily and thread-safe without synchronization.
- Both methods are thread-safe and lazy; enum singleton is another simple alternative.
View a sample answer
A thread-safe singleton ensures only one instance is created in a multithreaded environment. Double-checked locking minimizes synchronization overhead: first check if instance is null without synchronization; if null, synchronize on the class, then check again before creating the instance. The instance field must be volatile to prevent instruction reordering and ensure visibility. The Bill Pugh method uses a private static inner class that holds the instance; it is loaded only when getInstance() is called, providing lazy initialization and thread safety without explicit synchronization. The enum singleton (Java 5+) is the simplest and safest, but may not allow lazy loading if you need it. A common pitfall in double-checked locking is forgetting the volatile keyword; without it, a partially constructed object may be visible, causing subtle bugs. The Bill Pugh method is preferred for its simplicity and performance.
Reference solutionjava // Double-checked locking singleton (Java 5+ with volatile) 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; } } // Bill Pugh singleton (static inner class) public class Singleton { private Singleton() {} private static class Holder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } } - What is the difference between synchronized, ReentrantLock, and ReadWriteLock? Provide a use case for each.What a strong answer covers
- synchronized is a keyword that provides intrinsic locking; it is simple but limited (cannot time out, cannot interrupt waiting).
- ReentrantLock is a class with same basic behavior as synchronized but offers features like tryLock, lockInterruptibly, and fairness policy.
- ReadWriteLock allows multiple readers to hold a lock concurrently but exclusive write access; improves concurrency for read-heavy workloads.
- Use synchronized for simple cases with low contention; use ReentrantLock when you need advanced features like fair ordering or timed waits.
- Use ReadWriteLock when reads significantly outnumber writes and the lock contention is high.
View a sample answer
synchronized is the simplest locking mechanism in Java, providing mutual exclusion and happens-before guarantees. It is reentrant and automatically releases the lock when the block exits. However, it has limitations: cannot attempt to lock with timeout, cannot interrupt a thread waiting for the lock, and block structure is inflexible. ReentrantLock addresses these with methods like tryLock, lockInterruptibly, and the ability to specify fairness. It also supports multiple Condition objects for fine-grained waiting. ReadWriteLock separates read and write locks; multiple threads can hold the read lock simultaneously, improving throughput when reads dominate. A pitfall of ReadWriteLock is that write starvation can occur under heavy read contention unless fairness is set. Use synchronized for local synchronization where simplicity matters, ReentrantLock for advanced control (e.g., implementing thread pools), and ReadWriteLock for caches or databases where reads vastly outnumber writes.
- Describe the Java memory model. How does volatile guarantee visibility?What a strong answer covers
- JMM defines how threads interact through memory and which ordering guarantees are provided.
- Key concepts: happens-before, visibility, atomicity, reordering.
- volatile ensures that a write to a volatile variable is visible to all subsequent reads of that variable across threads.
- Volatile prevents caching of the variable in thread-local memory; each read/write goes directly to main memory.
- Volatile does not provide atomicity for compound operations (e.g., i++).
View a sample answer
The Java Memory Model (JMM) specifies how threads interact via memory and defines the behavior of concurrent constructs. It is built on the happens-before relationship: if one action happens-before another, the first is visible to the second. The JMM allows compilers and processors to reorder instructions as long as the program's single-threaded semantics are preserved, which can cause subtle bugs in multithreaded code. The volatile keyword guarantees visibility: a write to a volatile variable establishes a happens-before relationship with subsequent reads of the same variable. It forces the thread to write the value to main memory and invalidates caches, so other threads see the latest value. However, volatile does not provide atomicity for compound actions like i++ (read-modify-write), so synchronization or atomic classes are needed. A common pitfall is assuming volatile makes an operation atomic; for example, without proper synchronization, two threads incrementing a volatile int can lose updates. Use volatile for flags and simple state indicators, but not for counters or accumulators.
- Write a method to find the first non-repeating character in a string using Java Streams or without them.What a strong answer covers
- Use a LinkedHashMap to preserve insertion order and find first non-repeating character efficiently.
- Traverse the string, count occurrences using a frequency map (e.g., int[256] for ASCII, or HashMap for Unicode).
- Without streams: use two passes – first to build frequency map, second to find first character with count 1.
- With streams: can use Collectors.groupingBy and filter, but may be less efficient due to multiple passes.
- Edge cases: empty string returns null or empty; no non-repeating character returns null.
View a sample answer
To find the first non-repeating character, an efficient approach is to use a frequency map (or array) in two passes. In the first pass, we iterate through the string and count the occurrences of each character. In the second pass, we iterate again and return the first character whose count is 1. This works in O(n) time and O(1) space if the character set is limited (e.g., ASCII), or O(k) where k is the character set size. Using Java streams, you can combine IntStream, Collectors.groupingBy, and filtering, but it often still requires two operations and may be less readable. LinkedHashMap can be used to track insertion order and find the first character with count 1 in one pass, but it adds overhead. A common pitfall is forgetting to handle Unicode or extended characters; using a HashMap<Character, Integer> is safer for full Unicode. Also consider case sensitivity as per requirements.
Reference solutionjava import java.util.*; public class FirstNonRepeating { public static Character firstNonRepeating(String s) { // Frequency map: O(n) time, O(k) space Map<Character, Integer> count = new HashMap<>(); for (char c : s.toCharArray()) { count.put(c, count.getOrDefault(c, 0) + 1); } // Second pass to find first with count 1 for (char c : s.toCharArray()) { if (count.get(c) == 1) { return c; } } return null; // no non-repeating character } // Using Java Streams (two passes internally) public static Character firstNonRepeatingStream(String s) { return s.chars() .mapToObj(c -> (char) c) .collect(Collectors.collectingAndThen( Collectors.groupingBy(c -> c, LinkedHashMap::new, Collectors.counting()), map -> map.entrySet().stream() .filter(e -> e.getValue() == 1) .map(Map.Entry::getKey) .findFirst() .orElse(null) )); } public static void main(String[] args) { System.out.println(firstNonRepeating("geeksforgeeks")); // f System.out.println(firstNonRepeatingStream("loveleetcode")); // v } } - How does garbage collection work in Java? Compare G1GC and ZGC.What a strong answer covers
- GC automatically reclaims memory by identifying objects that are no longer reachable from root references.
- Major phases: marking (find live objects), sweeping (remove dead objects), and compacting (reduce fragmentation).
- G1GC: generational, divides heap into regions, performs concurrent marking, aims for low pause times (default < 200ms).
- ZGC: non-generational (Java 21+ has generational), concurrent, uses colored pointers and load barriers, pause times < 1ms regardless of heap size.
- G1GC is suitable for large heaps (up to ~200GB) with moderate pause time requirements; ZGC for extremely large heaps and low latency.
View a sample answer
Garbage collection (GC) in Java automatically manages memory by reclaiming objects that are no longer referenced. G1GC (Garbage-First) is a generational, parallel, mostly concurrent collector that divides the heap into equal-sized regions. It performs concurrent marking and collects regions with the most garbage first to minimize pauses. G1GC aims to meet a configurable pause time goal (e.g., 200ms) and is suitable for heaps up to several hundred gigabytes. ZGC is a low-latency collector that uses colored pointers and load barriers to do most phases concurrently, resulting in pause times typically under 1 millisecond, independent of heap size. ZGC is designed for very large heaps (multiterabytes) and applications requiring consistent low latency. A key difference is that G1GC is generational, while ZGC was originally non-generational (but Java 21 added generational ZGC). G1GC may suffer from fragmentation and longer full GCs, whereas ZGC avoids most compaction overhead. The choice depends on requirements: G1GC for balanced throughput and pause times, ZGC for extreme low latency.
- Implement a custom blocking queue using wait-notify (or Lock conditions).What a strong answer covers
- A blocking queue is a queue that blocks when you try to dequeue from an empty queue or enqueue into a full queue.
- Use ReentrantLock with two Conditions: notEmpty and notFull for efficient waiting and signaling.
- Wait-notify (or notifyAll) with synchronized blocks also works but is less flexible and can cause missed signals.
- Use a circular buffer or a LinkedList internally to store elements.
- Implement methods: put(e) waits if full, then inserts and signals notEmpty; take() waits if empty, then removes and signals notFull.
View a sample answer
A custom blocking queue can be implemented using ReentrantLock and Condition objects for fine-grained control. The queue has a fixed capacity and uses a lock to protect internal state. Two conditions, notEmpty and notFull, allow threads to wait efficiently: a consumer thread waits on notEmpty when the queue is empty, and a producer thread waits on notFull when the queue is full. When an element is added, notEmpty.signal() is called to wake up a waiting consumer; when an element is removed, notFull.signal() wakes up a producer. Using wait-notify inside synchronized blocks is also possible but can lead to lost signals if notify is called when no threads are waiting, though wait-notify with a loop condition mitigates this. The ReentrantLock approach is more explicit and supports features like fairness and timed waits. A common pitfall is forgetting to use a while loop for condition checks (spurious wakeups). The implementation must be thread-safe and ensure proper signaling to avoid deadlock.
Reference solutionjava import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class BlockingQueue<T> { private final Queue<T> queue = new LinkedList<>(); private final int capacity; private final ReentrantLock lock = new ReentrantLock(); private final Condition notEmpty = lock.newCondition(); private final Condition notFull = lock.newCondition(); public BlockingQueue(int capacity) { this.capacity = capacity; } public void put(T item) throws InterruptedException { lock.lock(); try { while (queue.size() == capacity) { notFull.await(); // wait until not full } queue.add(item); notEmpty.signal(); // signal waiting consumers } finally { lock.unlock(); } } public T take() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); // wait until not empty } T item = queue.poll(); notFull.signal(); // signal waiting producers return item; } finally { lock.unlock(); } } }
How to prepare
- Practice writing clean, thread-safe code on a whiteboard or online editor.
- Understand JVM internals and be able to reason about memory and GC behavior.
- Master the Collections framework: know trade-offs of each implementation.
- Review Java 8+ features: streams, lambdas, optional, CompletableFuture, and new Date/Time API.
- Have a deep answer ready for 'How is a HashMap different from a ConcurrentHashMap?' with internal details.
Frequently asked questions
Are Java 8 features important for interviews?
Yes, most interviews assume knowledge of lambdas, streams, optionals, and CompletableFuture. They are heavily used in modern Java development.
Do I need to memorize JVM tuning flags?
You should be familiar with common GC flags like -XX:+UseG1GC, -Xms, -Xmx, and know how to adjust heap sizes. Understanding the reasoning is more important than memorization.
How often are design patterns asked in Java interviews?
Commonly, especially Singleton, Factory, Builder, and Observer. Be ready to implement a thread-safe Singleton or explain the Strategy pattern with Java 8 lambdas.
What is the difference between fail-fast and fail-safe iterators?
Fail-fast iterators throw ConcurrentModificationException if the collection is structurally modified while iterating (e.g., ArrayList). Fail-safe iterators operate on a clone (e.g., ConcurrentHashMap, CopyOnWriteArrayList).
Should I know Java 9-17 features for interviews?
Yes, expect questions on modules (Jigsaw), sealed classes, records, pattern matching for instanceof, and text blocks. At least be aware of key features.
Practice Java questions with instant AI feedback
Upload your resume, get a personalized mock interview, and see exactly what to improve — free to start.