Java Developer Interview Questions
Core Overview
Master core Java concepts, object-oriented programming, concurrency, streams, JVM memory mechanics, and Spring Boot.
Ready to test your knowledge?
Launch a focused practice session to review questions without distraction.
What is the difference between the `==` operator and the `equals()` method in Java?
Direct Answer
The == operator compares memory references, while equals() compares object values for logical equality.
Detailed Explanation
Code Example
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false (different references in memory)
System.out.println(s1.equals(s2)); // true (logical content is identical)
String s3 = "hello";
String s4 = "hello";
System.out.println(s3 == s4); // true (String pool reference sharing)
Common Interview Pitfalls
- Using == to compare Strings instead of using String.equals().
- Forgetting to override hashCode() when overriding equals().
Is Java pass-by-value or pass-by-reference?
Direct Answer
Java is strictly pass-by-value. Primitives pass a copy of the value; objects pass a copy of the reference address.
Detailed Explanation
Code Example
public class PassTest {
public static void main(String[] args) {
int x = 10;
modifyPrimitive(x);
System.out.println(x); // Outputs 10
Dog d = new Dog("Max");
modifyObject(d);
System.out.println(d.name); // Outputs "Buddy" (mutated heap object)
reassignObject(d);
System.out.println(d.name); // Outputs "Buddy" (reassignment failed to change caller)
}
static void modifyPrimitive(int val) { val = 20; }
static void modifyObject(Dog dog) { dog.name = "Buddy"; }
static void reassignObject(Dog dog) { dog = new Dog("Fido"); }
}
Common Interview Pitfalls
- Believing that mutating an object parameter modifies the reference pointer itself.
- Claiming Java passes objects by reference.
What is the difference between an abstract class and an interface in Java?
Direct Answer
Abstract classes model inheritance and state (is-a). Interfaces model behavior contracts (can-do) without state.
Detailed Explanation
Code Example
public interface Flyable {
void fly(); // Abstract by default
default void land() {
System.out.println("Landing...");
}
}
public abstract class Animal {
protected String name;
public Animal(String name) { this.name = name; }
public abstract void makeSound();
}
public class Eagle extends Animal implements Flyable {
public Eagle(String name) { super(name); }
public void makeSound() { System.out.println("Eagle screech!"); }
public void fly() { System.out.println("Soaring high!"); }
}
Common Interview Pitfalls
- Assuming interfaces cannot have any method implementations (ignoring default/static methods).
- Thinking default methods allow interfaces to hold instance state.
What is polymorphism and how does Java implement compile-time vs. runtime polymorphism?
Direct Answer
Polymorphism lets objects take many forms. Compile-time uses overloading; runtime uses overriding via dynamic dispatch.
Detailed Explanation
Code Example
class Printer {
// Compile-time polymorphism (Overloading)
void print(String s) { System.out.println("String: " + s); }
void print(int i) { System.out.println("Int: " + i); }
}
class Animal {
void speak() { System.out.println("Generic Sound"); }
}
class Cat extends Animal {
// Runtime polymorphism (Overriding)
@Override
void speak() { System.out.println("Meow"); }
}
public class PolyTest {
public static void main(String[] args) {
Animal myAnimal = new Cat();
myAnimal.speak(); // Prints "Meow" (Runtime dynamic binding)
}
}
Common Interview Pitfalls
- Confusing method overloading (different arguments, same class) with overriding (same arguments, subclasses).
- Thinking instance variables can be overridden polymorphically (only methods are overridden, variables are shadowed).
How does HashMap work internally in Java?
Direct Answer
HashMap uses hashing to map keys. Collisions resolve via linked lists, or red-black trees in Java 8+ if count >= 8.
Detailed Explanation
Code Example
HashMap<String, Integer> map = new HashMap<>();
map.put("Apple", 1); // Key "Apple" hash computed, placed in bucket index
map.put("Banana", 2);
// Collision simulation: keys with different logical content but same hash
System.out.println(map.get("Apple")); // Retrieves value 1
Common Interview Pitfalls
- Believing HashMap is thread-safe (it is not; use ConcurrentHashMap in multi-threaded contexts).
- Forgetting that keys must override hashCode() and equals() correctly.
What is the difference between fail-fast and fail-safe (weakly consistent) iterators?
Direct Answer
Fail-fast iterators throw ConcurrentModificationException on modification. Fail-safe/weakly-consistent iterators do not.
Detailed Explanation
Code Example
// Fail-fast example
List<String> list = new ArrayList<>(List.of("A", "B"));
Iterator<String> it = list.iterator();
while(it.hasNext()) {
String val = it.next();
list.add("C"); // Throws ConcurrentModificationException!
}
// Fail-safe example
List<String> safeList = new CopyOnWriteArrayList<>(List.of("A", "B"));
for (String val : safeList) {
safeList.add("C"); // Allowed; runs without exceptions!
}
Common Interview Pitfalls
- Assuming ConcurrentModificationException only happens when multiple threads are involved (it can happen in a single thread).
- Using the collection add/remove methods inside a foreach loop instead of the Iterator.remove() method.
What is the difference between `map()` and `flatMap()` in Java Streams?
Direct Answer
map() performs 1-to-1 value transformation. flatMap() performs 1-to-many mapping and flattens nested streams.
Detailed Explanation
Code Example
List<List<String>> nestedList = List.of(
List.of("apple", "banana"),
List.of("orange", "cherry")
);
// Map produces a Stream<Stream<String>> or Stream<List<String>>
List<Integer> lengths = nestedList.stream()
.flatMap(List::stream) // Flattens to Stream<String>
.map(String::length) // Transforms Stream<String> to Stream<Integer>
.collect(Collectors.toList());
System.out.println(lengths); // [5, 6, 6, 6]
Common Interview Pitfalls
- Using map() when the mapper function returns a Collection or Stream, resulting in a nested stream (Stream<Stream<T>>).
- Forgetting that flatMap() requires the lambda function to return an explicit Stream instance, not a Collection.
Why are Java Streams described as lazily evaluated, and what are the performance implications?
Direct Answer
Stream intermediate operations only build a pipeline; execution triggers only when a terminal operation is called.
Detailed Explanation
Code Example
List<String> names = List.of("Alex", "David", "Brad", "Charlie");
Optional<String> firstFiltered = names.stream()
.filter(name -> {
System.out.println("Filter: " + name);
return name.startsWith("C");
})
.map(name -> {
System.out.println("Map: " + name);
return name.toUpperCase();
})
.findFirst(); // Terminal operation
// Console Output:
// Filter: Alex
// Filter: David
// Filter: Brad
// Filter: Charlie
// Map: Charlie
// Note: Brad and Alex were not mapped because findFirst short-circuited the pipeline!
Common Interview Pitfalls
- Adding intermediate operations without a terminal operation, which results in zero execution.
- Assuming that a stream processes all elements through the first step before passing them to the next step (elements are actually pulled through the pipeline one-by-one).
What is the purpose of the `volatile` keyword in Java, and how does it relate to cache coherence?
Direct Answer
volatile forces threads to read/write directly from main memory, ensuring variable visibility and ordering.
Detailed Explanation
Code Example
public class VolatileFlag implements Runnable {
// Volatile ensures updates are visible immediately to other threads
private volatile boolean running = true;
public void run() {
while (running) {
// Keep running until flag changes in main memory
}
System.out.println("Stopped.");
}
public void stopRunning() {
running = false; // Write flushed directly to main memory
}
}
Common Interview Pitfalls
- Assuming volatile variable increment operations (like vVar++) are thread-safe (they require atomic classes or synchronization).
- Using volatile instead of synchronized/locks for complex transactional operations.
What are Virtual Threads (Project Loom) in Java 21, and how do they differ from Platform Threads?
Direct Answer
Virtual threads are lightweight user-mode threads managed by the JVM to run millions of concurrent tasks efficiently.
Detailed Explanation
Code Example
// Creating a virtual thread
Thread vThread = Thread.ofVirtual().start(() -> {
System.out.println("Running in virtual thread: " + Thread.currentThread());
});
// Using a Virtual Thread Executor for concurrent tasks
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofMillis(100)); // Non-blocking context switch
return i;
});
});
} // Auto-closes after all virtual threads finish
Common Interview Pitfalls
- Pooling virtual threads (pooling is a mechanism to limit expensive OS threads; virtual threads are cheap and should be created per-task instead).
- Pinning carrier threads by performing synchronized blocks or native calls containing blocking operations inside a virtual thread.
What are the main runtime memory regions inside the JVM and what is stored in each?
Direct Answer
JVM memory includes Stack (local frames), Heap (shared objects), Metaspace (class metadata), and PC/Native stacks.
Detailed Explanation
Code Example
public class MemoryTest {
// Class metadata and methods are loaded in Metaspace
private int instanceVal = 10; // Stored in Heap inside the object
public static void main(String[] args) {
int x = 5; // Local primitive stored in Stack frame
MemoryTest obj = new MemoryTest(); // Reference "obj" on Stack, instance on Heap
}
}
Common Interview Pitfalls
- Assuming that Metaspace is part of the JVM heap (it is allocated in native off-heap memory since Java 8).
- Believing object references are stored on the heap (only the object instance resides on the heap; reference variables inside methods reside on the stack).
What is the difference between the G1 (Garbage First) and ZGC (Z Garbage Collector) collectors?
Direct Answer
G1 is a regional generational collector with tens-of-ms pauses. ZGC runs concurrently keeping pauses under 1ms.
Detailed Explanation
Code Example
# JVM Flags to enable ZGC in Java 17+
java -XX:+UseZGC -jar my-application.jar
# JVM Flags to enable Generational ZGC in Java 21
java -XX:+UseZGC -XX:+ZGenerational -jar my-application.jar
Common Interview Pitfalls
- Believing ZGC pause times increase with heap size (ZGC pauses are independent of heap size).
- Configuring aggressive GC tuning flags on ZGC when it is designed to auto-tune.
What are Records in Java 14+ and how do they differ from normal classes?
Direct Answer
Records are immutable data carriers. The compiler auto-generates constructors, accessors, equals, and hashCode.
Detailed Explanation
Code Example
// Simple record definition
public record UserDto(String username, String email) {}
// Usage
UserDto user = new UserDto("alice", "alice@example.com");
System.out.println(user.username()); // Getter (no "get" prefix)
System.out.println(user); // UserDto[username=alice, email=alice@example.com]
Common Interview Pitfalls
- Trying to define instance variables inside a record body (only static variables are permitted).
- Thinking records are deeply immutable (if a record has a mutable list field, the list contents can still be mutated).
What are Sealed Classes and Interfaces in Java 17, and what problems do they solve?
Direct Answer
Sealed classes restrict which subclasses can extend them, enabling precise API boundaries and exhaustive switches.
Detailed Explanation
Code Example
// Sealed interface restricting implementations
public sealed interface Shape permits Circle, Square {}
public final class Circle implements Shape {
public double radius() { return 5.0; }
}
public final class Square implements Shape {
public double side() { return 4.0; }
}
// Exhaustive switch in Java 21
public double getArea(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s -> s.side() * s.side();
// No default needed because compiler knows Circle and Square cover all cases!
};
}
Common Interview Pitfalls
- Forgetting that subclasses of a sealed class must explicitly declare one of the three modifiers: final, sealed, or non-sealed.
- Thinking sealed classes can permit subclasses outside their module or package.
What are the core lifecycle annotations in JUnit 5 and how do they differ from JUnit 4?
Direct Answer
JUnit 5 lifecycle annotations are @BeforeAll, @BeforeEach, @AfterEach, and @AfterAll, replacing JUnit 4 equivalents.
Detailed Explanation
Code Example
@TestInstance(TestInstance.Lifecycle.PER_METHOD) // Default lifecycle
public class LifecycleTest {
@BeforeAll
static void initAll() {
System.out.println("Starting test suite...");
}
@BeforeEach
void init() {
System.out.println("Setting up test parameters...");
}
@Test
void myTest() {
System.out.println("Executing test...");
}
@AfterEach
void tearDown() {
System.out.println("Cleaning up mock states...");
}
@AfterAll
static void tearDownAll() {
System.out.println("Shutdown test runner.");
}
}
Common Interview Pitfalls
- Forgetting that @BeforeAll and @AfterAll methods must be declared static under default PER_METHOD lifecycle settings.
- Mixing JUnit 4 annotations (like @Before) in a JUnit 5 (Jupiter) test project, causing tests to run without initialization.
What is Testcontainers and why is it used for database integration testing?
Direct Answer
Testcontainers spins up disposable Docker containers in JUnit to run integration tests against real databases.
Detailed Explanation
Code Example
@SpringBootTest
@Testcontainers
class IntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("testuser")
.withPassword("testpass");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Test
void testDatabaseConnection() {
assertTrue(postgres.isRunning());
}
}
Common Interview Pitfalls
- Hardcoding static ports inside Testcontainers setup, causing collisions in concurrent CI builds.
- Re-starting containers for every test file instead of using the Singleton Container pattern for shared speed.
How would you design a simple, thread-safe LRU (Least Recently Used) cache in Java?
Direct Answer
Build an LRU cache by extending LinkedHashMap, overriding removeEldestEntry, and wrapping in synchronizedMap.
Detailed Explanation
Code Example
public class LruCache<K, V> {
private final int capacity;
private final Map<K, V> cacheMap;
public LruCache(int capacity) {
this.capacity = capacity;
// Access-order set to true
this.cacheMap = Collections.synchronizedMap(new LinkedHashMap<K, V>(capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity; // Remove when size exceeds capacity
}
});
}
public V get(K key) { return cacheMap.get(key); }
public void put(K key, V value) { cacheMap.put(key, value); }
}
Common Interview Pitfalls
- Forgetting that access-order must be explicitly set (the default is insertion-order, which makes it an FIFO cache, not LRU).
- Failing to synchronize map operations in multi-threaded environments, leading to corrupted pointers in LinkedHashMap's doubly linked list.
How do you design a thread-safe counter in Java, and what are the performance trade-offs of synchronized, ReentrantLock, and AtomicLong?
Direct Answer
Thread-safe counters use locks or AtomicLong. Under high write contention, LongAdder is faster via striped cells.
Detailed Explanation
Code Example
// High Performance High Contention Counter
public class HighPerformanceCounter {
private final LongAdder adder = new LongAdder();
public void increment() {
adder.increment(); // Distributes writes over cells
}
public long getValue() {
return adder.sum(); // Merges cell counts
}
}
Common Interview Pitfalls
- Using volatile long for counter increments (volatile does not make incrementing atomic).
- Using AtomicLong under thousands of threads doing writes, where LongAdder would be dramatically faster.
Official Documentation & Specifications
Java Fundamentals
Object-Oriented Programming
Strings & Collections
Streams & Lambdas
Multithreading & Concurrency
JVM & Memory Management
Java 17+ Modern Features
Want to tailer your resume for Java Developer roles?
Import your resume, scan it for critical Java Developer keywords, and compare it against ATS standards instantly.