Java interviews test object-oriented design, the standard library, memory model, concurrency, and modern Java features. This guide covers the 50 most common questions — with concise answers and runnable code examples.
Quick reference
| Topic | Most asked questions |
|---|---|
| OOP | Inheritance, polymorphism, encapsulation, abstraction |
| Collections | List vs Set vs Map, HashMap internals, generics |
| Java 8+ | Streams, lambdas, Optional, default methods |
| Concurrency | synchronized, volatile, locks, thread pool |
| JVM | Heap vs stack, garbage collection, class loading |
| Exceptions | checked vs unchecked, try-with-resources |
| Design patterns | Singleton, Factory, Builder, Observer |
OOP fundamentals
1. What are the four pillars of OOP?
| Pillar | Definition | Java example |
|---|---|---|
| Encapsulation | Bundle data + behaviour; hide internals via private |
getter/setter, private fields |
| Inheritance | Subclass inherits state and behaviour from superclass | class Dog extends Animal |
| Polymorphism | Same interface, different runtime behaviour | method overriding, Animal a = new Dog() |
| Abstraction | Expose what, hide how | abstract class, interface |
// Polymorphism in action
abstract class Shape {
abstract double area();
}
class Circle extends Shape {
private final double r;
Circle(double r) { this.r = r; }
@Override
public double area() { return Math.PI * r * r; }
}
class Rectangle extends Shape {
private final double w, h;
Rectangle(double w, double h) { this.w = w; this.h = h; }
@Override
public double area() { return w * h; }
}
// Shape reference → runtime dispatch
Shape s = new Circle(5);
System.out.println(s.area()); // 78.54
2. What is the difference between abstract class and interface?
| Feature | abstract class |
interface |
|---|---|---|
| Multiple inheritance | No (single extends) | Yes (implements multiple) |
| Constructor | Yes | No |
| Fields | Any access modifier | public static final only |
| Method body | Allowed (any) | default/static since Java 8 |
| Use when | Shared state + partial impl | Contract / capability |
// Abstract class — shared state
abstract class Vehicle {
protected int speed;
abstract void accelerate();
void brake() { speed = 0; } // concrete method
}
// Interface — capability
interface Electric {
int batteryLevel();
default void charge() { System.out.println("Charging..."); }
}
class Tesla extends Vehicle implements Electric {
@Override public void accelerate() { speed += 20; }
@Override public int batteryLevel() { return 80; }
}
3. What is method overloading vs method overriding?
Overloading — same name, different parameter list, resolved at compile time.
Overriding — same signature in subclass, resolved at runtime (dynamic dispatch).
class Calculator {
// Overloading — compile-time polymorphism
int add(int a, int b) { return a + b; }
double add(double a, double b){ return a + b; }
int add(int a, int b, int c) { return a + b + c; }
}
class Animal {
String speak() { return "..."; }
}
class Dog extends Animal {
@Override
String speak() { return "Woof"; } // runtime polymorphism
}
Rules for overriding:
- Same name, same parameters, same (or covariant) return type.
- Cannot reduce visibility (
publicmethod cannot beprivatein subclass). - Cannot throw new checked exceptions broader than the parent's.
4. What is the difference between == and .equals()?
==compares references (identity) for objects; compares values for primitives..equals()compares logical equality (content), if properly overridden.
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b); // false — different objects
System.out.println(a.equals(b)); // true — same content
// String pool interning
String c = "hello";
String d = "hello";
System.out.println(c == d); // true — same pool reference
Always use .equals() for object comparison. For null-safety: Objects.equals(a, b).
5. What is final, finally, and finalize?
| Keyword | Where used | Meaning |
|---|---|---|
final |
variable, method, class | Variable: constant; method: cannot override; class: cannot extend |
finally |
try-catch block | Always executes (cleanup code) |
finalize |
method (deprecated) | Called by GC before object collection — avoid using it |
final int MAX = 100; // constant
try {
riskyOperation();
} catch (IOException e) {
handle(e);
} finally {
closeResource(); // always runs
}
finalize() is deprecated since Java 9 — use try-with-resources or Cleaner instead.
6. What is the difference between static and instance members?
static members belong to the class; instance members belong to each object.
class Counter {
private static int total = 0; // shared across all instances
private int id; // unique per instance
Counter() {
total++;
this.id = total;
}
static int getTotal() { return total; } // no 'this' available
int getId() { return id; }
}
Counter c1 = new Counter(); // total = 1, id = 1
Counter c2 = new Counter(); // total = 2, id = 2
System.out.println(Counter.getTotal()); // 2
Collections & Generics
7. What is the Java Collections hierarchy?
Iterable
└── Collection
├── List (ordered, duplicates allowed)
│ ├── ArrayList
│ └── LinkedList
├── Set (no duplicates)
│ ├── HashSet (unordered)
│ ├── LinkedHashSet (insertion order)
│ └── TreeSet (sorted)
└── Queue
├── PriorityQueue
└── ArrayDeque
Map (key-value, not Collection)
├── HashMap (unordered)
├── LinkedHashMap (insertion order)
├── TreeMap (sorted by key)
└── ConcurrentHashMap (thread-safe)
8. How does HashMap work internally?
HashMap uses an array of buckets (default 16). Each entry's bucket index is hash(key) & (n - 1).
put(k, v)— computehash(k), find bucket, add to linked list (or tree if ≥ 8 entries in bucket, Java 8+).get(k)— computehash(k), walk bucket's list/tree to find matchingequals(k).- Rehashing — when load factor (default 0.75) is exceeded, capacity doubles and all entries are rehashed.
Map<String, Integer> map = new HashMap<>();
map.put("alice", 1);
map.put("bob", 2);
// Iteration order is NOT guaranteed
for (Map.Entry<String, Integer> e : map.entrySet()) {
System.out.println(e.getKey() + " = " + e.getValue());
}
Why override both hashCode and equals?HashMap first compares hash codes (fast) then calls equals (slower). If you override only equals, equal objects may land in different buckets — lookup fails.
9. What is the difference between ArrayList and LinkedList?
| Operation | ArrayList |
LinkedList |
|---|---|---|
get(i) |
O(1) | O(n) |
add at end |
O(1) amortised | O(1) |
add/remove at middle |
O(n) | O(n) to find + O(1) to splice |
| Memory | Less (contiguous array) | More (node objects + pointers) |
| Cache locality | Excellent | Poor |
Default choice: ArrayList. Use LinkedList only when you do many insertions/deletions at both ends (deque use case) — and even then, ArrayDeque is usually faster.
10. What are Generics and why are they used?
Generics provide compile-time type safety and eliminate casts.
// Without generics — runtime ClassCastException risk
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // cast required
// With generics — checked at compile time
List<String> typed = new ArrayList<>();
typed.add("hello");
String s2 = typed.get(0); // no cast
// Generic method
public <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
// Bounded wildcards
void printAll(List<? extends Number> nums) { // producer — covariance
for (Number n : nums) System.out.println(n);
}
void addInts(List<? super Integer> list) { // consumer — contravariance
list.add(42);
}
Type erasure: Generics are a compile-time feature. At runtime List<String> and List<Integer> are both just List — generic type info is erased.
11. What is the difference between Iterator and ListIterator?
| Feature | Iterator |
ListIterator |
|---|---|---|
| Direction | Forward only | Forward and backward |
| Collections | All Collection |
List only |
| Add elements | No | Yes (add()) |
| Replace elements | No | Yes (set()) |
| Current index | No | Yes (nextIndex()) |
Java 8+ Features
12. What are lambda expressions?
Lambdas are anonymous functions that implement a functional interface (exactly one abstract method).
// Before Java 8
Comparator<String> comp = new Comparator<String>() {
@Override
public int compare(String a, String b) { return a.compareTo(b); }
};
// Java 8 lambda
Comparator<String> comp = (a, b) -> a.compareTo(b);
// Method reference (even shorter)
Comparator<String> comp = String::compareTo;
// Usage
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
names.sort((a, b) -> a.compareTo(b));
// or
names.sort(String::compareTo);
13. What is the Stream API? How is it different from a for loop?
Streams provide a declarative, pipeline-based way to process collections. They don't store data — they process it lazily.
List<String> names = List.of("Alice", "Bob", "Charlie", "Anna");
// Imperative
List<String> result = new ArrayList<>();
for (String name : names) {
if (name.startsWith("A")) result.add(name.toUpperCase());
}
result.sort(Comparator.naturalOrder());
// Declarative (Stream)
List<String> result = names.stream()
.filter(n -> n.startsWith("A"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());
// Parallel stream (use only for CPU-bound, large datasets)
long count = names.parallelStream()
.filter(n -> n.length() > 3)
.count();
Lazy evaluation: Intermediate operations (filter, map, sorted) build the pipeline. Terminal operations (collect, count, forEach) trigger execution.
14. What is Optional and when should you use it?
Optional<T> is a container that may or may not hold a value — it's a signal to callers that null is a possible outcome.
// Bad — null return is easy to miss
String findName(int id) { return null; }
// Good — explicit about nullability
Optional<String> findName(int id) {
if (id == 1) return Optional.of("Alice");
return Optional.empty();
}
// Usage
findName(1)
.map(String::toUpperCase)
.ifPresent(System.out::println); // "ALICE"
// Safe access with default
String name = findName(99).orElse("Unknown");
String name2 = findName(99).orElseGet(() -> computeDefault());
String name3 = findName(99).orElseThrow(() -> new NotFoundException(99));
Don't use Optional as a field or method parameter — it's designed as a return type.
15. What are default and static methods in interfaces?
Java 8 added these to allow interface evolution without breaking existing implementations.
interface Sorter {
void sort(List<?> list);
// Default method — subclasses may override
default void sortAndPrint(List<?> list) {
sort(list);
System.out.println(list);
}
// Static utility method
static Sorter natural() {
return list -> Collections.sort((List<Comparable>) list);
}
}
class QuickSorter implements Sorter {
@Override
public void sort(List<?> list) { /* custom impl */ }
// sortAndPrint is inherited
}
If two interfaces provide conflicting defaults, the implementing class must override the method.
16. What are the key Stream terminal operations?
List<Integer> nums = List.of(1, 2, 3, 4, 5);
// collect
List<Integer> evens = nums.stream().filter(n -> n % 2 == 0)
.collect(Collectors.toList());
// reduce
int sum = nums.stream().reduce(0, Integer::sum);
// findFirst / findAny
Optional<Integer> first = nums.stream().filter(n -> n > 3).findFirst();
// anyMatch / allMatch / noneMatch
boolean anyEven = nums.stream().anyMatch(n -> n % 2 == 0);
// count / min / max
long count = nums.stream().filter(n -> n > 2).count();
Optional<Integer> max = nums.stream().max(Integer::compare);
// groupingBy
Map<Boolean, List<Integer>> partitioned =
nums.stream().collect(Collectors.partitioningBy(n -> n % 2 == 0));
// joining
String joined = Stream.of("a","b","c")
.collect(Collectors.joining(", ", "[", "]")); // [a, b, c]
Concurrency
17. What is the difference between Thread and Runnable?
// Extends Thread — limits inheritance
class MyThread extends Thread {
@Override
public void run() { System.out.println("Running"); }
}
new MyThread().start();
// Implements Runnable — preferred (composition over inheritance)
Runnable task = () -> System.out.println("Running");
new Thread(task).start();
// ExecutorService — best practice for production
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> System.out.println("Task"));
executor.shutdown();
Always prefer ExecutorService over raw Thread — it provides pooling, lifecycle management, and Future results.
18. What is synchronized? What problems does it solve?
synchronized ensures mutual exclusion — only one thread can execute the block/method at a time. It also provides visibility (happens-before relationship).
class SafeCounter {
private int count = 0;
// Synchronized method — locks on 'this'
public synchronized void increment() {
count++;
}
// Synchronized block — narrower lock scope (more efficient)
public void decrement() {
synchronized (this) {
count--;
}
}
public synchronized int get() { return count; }
}
Problems without synchronisation:
- Race condition — two threads read-modify-write simultaneously.
- Visibility issue — one thread's write not seen by another (CPU cache).
19. What is volatile? When to use it?
volatile guarantees visibility (write is immediately visible to all threads) but not atomicity.
class Stopper {
private volatile boolean stop = false; // without volatile, loop may never stop
void stop() { stop = true; }
void run() {
while (!stop) {
doWork();
}
}
}
Use volatile for flags that are written by one thread and read by others. Do not use it for compound operations like count++ — use AtomicInteger or synchronized instead.
20. What is AtomicInteger and the java.util.concurrent.atomic package?
Atomic classes use compare-and-swap (CAS) hardware instructions — lock-free, thread-safe operations.
import java.util.concurrent.atomic.*;
AtomicInteger counter = new AtomicInteger(0);
// Thread-safe increment (no synchronized needed)
counter.incrementAndGet(); // ++i
counter.getAndIncrement(); // i++
counter.addAndGet(5); // += 5
// CAS — only update if current value matches expected
boolean swapped = counter.compareAndSet(10, 0); // set to 0 if current == 10
AtomicBoolean flag = new AtomicBoolean(false);
AtomicReference<String> ref = new AtomicReference<>("initial");
21. What is ReentrantLock? How does it differ from synchronized?
| Feature | synchronized |
ReentrantLock |
|---|---|---|
| Trylock (non-blocking) | No | tryLock() |
| Interruptible lock | No | lockInterruptibly() |
| Timed lock | No | tryLock(time, unit) |
| Fairness | No | new ReentrantLock(true) |
| Multiple conditions | No | newCondition() |
| Manual unlock needed | No | Yes — always in finally |
import java.util.concurrent.locks.*;
ReentrantLock lock = new ReentrantLock();
void transfer(Account from, Account to, int amount) {
lock.lock();
try {
from.debit(amount);
to.credit(amount);
} finally {
lock.unlock(); // ALWAYS in finally
}
}
// Try without blocking
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
try { /* work */ }
finally { lock.unlock(); }
}
22. What is a deadlock? How do you prevent it?
A deadlock occurs when two or more threads wait for each other's locks indefinitely.
// Deadlock scenario
Thread T1 acquires lock A → waits for lock B
Thread T2 acquires lock B → waits for lock A
// Both wait forever
Prevention strategies:
- Lock ordering — always acquire locks in the same global order.
- Timeout — use
tryLock(timeout)instead of blockinglock(). - Lock-free data structures — use
ConcurrentHashMap,AtomicInteger. - Reduce lock scope — hold locks for the shortest time possible.
- Single lock — merge data guarded by two locks into one structure.
23. What is the difference between Callable and Runnable?
Runnable |
Callable<V> |
|
|---|---|---|
| Return value | void |
V |
| Checked exceptions | Cannot throw | Can throw |
Future result |
Future<?> |
Future<V> |
Callable<Integer> task = () -> {
Thread.sleep(100);
return 42; // returns a value
};
ExecutorService exec = Executors.newSingleThreadExecutor();
Future<Integer> future = exec.submit(task);
// Block until result available
int result = future.get(); // 42
int result2 = future.get(1, SECONDS); // with timeout
exec.shutdown();
24. What are CountDownLatch and CyclicBarrier?
CountDownLatch — one or more threads wait until a count reaches zero. One-time use.
CountDownLatch latch = new CountDownLatch(3);
// 3 worker threads each call latch.countDown() when done
Runnable worker = () -> { doWork(); latch.countDown(); };
for (int i = 0; i < 3; i++) executor.submit(worker);
latch.await(); // main thread waits here until count == 0
System.out.println("All workers done");
CyclicBarrier — all threads wait at a barrier point before proceeding. Reusable.
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("Phase done"));
Runnable phase = () -> {
doPhaseWork();
barrier.await(); // wait until all 3 threads reach here
doNextPhase();
};
JVM & Memory
25. What is the difference between heap and stack memory?
| Stack | Heap | |
|---|---|---|
| Stores | Primitives, references, method frames | Objects, arrays, class instances |
| Lifetime | Until method returns | Until GC collects |
| Size | Small (few MB) | Large (configured by -Xmx) |
| Thread | Per-thread | Shared across threads |
| Error | StackOverflowError |
OutOfMemoryError |
void method() {
int x = 5; // x is on the stack
String s = new String("hi"); // reference s on stack; object on heap
}
// When method() returns: x, s are popped; object may be GCed
26. What is garbage collection in Java?
GC automatically reclaims heap memory occupied by unreachable objects.
Generational GC (default HotSpot):
Young Generation (Eden + S0 + S1) → Old Generation → Metaspace
Minor GC (fast, frequent) Major/Full GC (slower)
- Most objects die young (short-lived allocations).
- Objects that survive several GC cycles are promoted to Old Generation.
- Stop-the-world pauses are minimised by collectors like G1GC (default since Java 9) and ZGC/Shenandoah (low-latency).
JVM flags:
-Xms512m -Xmx2g # initial and max heap
-XX:+UseG1GC # G1 collector
-XX:+UseZGC # low-latency ZGC (Java 15+)
-verbose:gc # GC logging
27. What is the difference between String, StringBuilder, and StringBuffer?
String |
StringBuilder |
StringBuffer |
|
|---|---|---|---|
| Mutability | Immutable | Mutable | Mutable |
| Thread-safe | Yes (immutable) | No | Yes (synchronised) |
| Performance | Slowest for concat | Fastest | Slower than SB |
| Use when | Fixed strings | Single-thread concat | Multi-thread (rare) |
// String concatenation in loop — creates N+1 String objects
String result = "";
for (int i = 0; i < 1000; i++) result += i; // slow
// StringBuilder — single mutable buffer
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) sb.append(i); // fast
String result = sb.toString();
String pool: String literals are interned. "hello" == "hello" is true because both reference the same pool entry.
28. What is ClassLoader?
ClassLoader loads .class files into the JVM at runtime.
Delegation model:
- Bootstrap ClassLoader — loads
java.lang,java.utiletc. fromrt.jar/ modules. - Platform ClassLoader (ext) — loads extension libraries.
- Application ClassLoader — loads your app's classpath.
// Retrieve class loader
ClassLoader cl = MyClass.class.getClassLoader();
System.out.println(cl); // sun.misc.Launcher$AppClassLoader
// Custom class loader (for plugins, sandboxing)
class PluginLoader extends URLClassLoader {
PluginLoader(URL[] urls) { super(urls, null); } // null parent = isolated
}
Exception Handling
29. What is the difference between checked and unchecked exceptions?
| Checked | Unchecked | |
|---|---|---|
| Extends | Exception |
RuntimeException |
| Must handle | Yes (compile error) | No |
| Common examples | IOException, SQLException |
NullPointerException, IllegalArgumentException |
| Signals | External, recoverable failures | Programming bugs |
// Checked — caller must handle or declare
public String readFile(String path) throws IOException {
return Files.readString(Path.of(path));
}
// Unchecked — programming error, let it propagate
public int divide(int a, int b) {
if (b == 0) throw new IllegalArgumentException("b must not be zero");
return a / b;
}
30. What is try-with-resources?
Introduced in Java 7, it automatically closes AutoCloseable resources.
// Before Java 7 — verbose, easy to forget close()
Connection conn = null;
try {
conn = getConnection();
// use conn
} finally {
if (conn != null) conn.close();
}
// Java 7+ try-with-resources
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ResultSet rs = ps.executeQuery();
// process rs
} // conn and ps closed automatically, even on exception
Multiple resources are closed in reverse order. The resource's close() exception is suppressed if an exception was already thrown from the try block (accessible via Throwable.getSuppressed()).
31. What is exception chaining?
Wrapping a low-level exception in a higher-level one while preserving the cause.
// Service layer wraps repository exception
class UserService {
User findUser(int id) {
try {
return userRepo.findById(id);
} catch (SQLException e) {
throw new ServiceException("Failed to load user " + id, e); // chain
}
}
}
// Log full stack trace including cause
catch (ServiceException e) {
logger.error("Error", e); // prints wrapped cause too
e.getCause(); // the original SQLException
}
Design Patterns
32. How do you implement the Singleton pattern thread-safely?
// Lazy initialisation with double-checked locking (Java 5+)
public class DatabasePool {
private static volatile DatabasePool instance;
private DatabasePool() {}
public static DatabasePool getInstance() {
if (instance == null) {
synchronized (DatabasePool.class) {
if (instance == null) { // double-check
instance = new DatabasePool(); // volatile ensures visibility
}
}
}
return instance;
}
}
// Even simpler — Initialization-on-demand holder (preferred)
public class Config {
private Config() {}
private static class Holder {
static final Config INSTANCE = new Config(); // class loaded lazily
}
public static Config getInstance() { return Holder.INSTANCE; }
}
// Enum Singleton — serialisation-safe, reflection-safe
public enum Logger {
INSTANCE;
public void log(String msg) { System.out.println(msg); }
}
Logger.INSTANCE.log("Hello");
33. What is the Builder pattern?
Builder separates construction of a complex object from its representation.
public class HttpRequest {
private final String url;
private final String method;
private final Map<String, String> headers;
private final String body;
private final int timeoutMs;
private HttpRequest(Builder b) {
this.url = Objects.requireNonNull(b.url);
this.method = b.method;
this.headers = Map.copyOf(b.headers);
this.body = b.body;
this.timeoutMs = b.timeoutMs;
}
public static class Builder {
private final String url;
private String method = "GET";
private Map<String, String> headers = new HashMap<>();
private String body;
private int timeoutMs = 5000;
public Builder(String url) { this.url = url; }
public Builder method(String m) { this.method = m; return this; }
public Builder header(String k, String v){ headers.put(k, v); return this; }
public Builder body(String b) { this.body = b; return this; }
public Builder timeout(int ms) { this.timeoutMs = ms; return this; }
public HttpRequest build() { return new HttpRequest(this); }
}
}
// Usage
HttpRequest req = new HttpRequest.Builder("https://api.example.com/users")
.method("POST")
.header("Authorization", "Bearer token123")
.body("{\"name\":\"Alice\"}")
.timeout(3000)
.build();
34. What is the Factory Method pattern?
Factory Method defines an interface for creating an object but lets subclasses decide which class to instantiate.
interface Notification {
void send(String message);
}
class EmailNotification implements Notification {
@Override public void send(String msg) { System.out.println("Email: " + msg); }
}
class SmsNotification implements Notification {
@Override public void send(String msg) { System.out.println("SMS: " + msg); }
}
// Factory
class NotificationFactory {
public static Notification create(String type) {
return switch (type) {
case "email" -> new EmailNotification();
case "sms" -> new SmsNotification();
default -> throw new IllegalArgumentException("Unknown: " + type);
};
}
}
Notification n = NotificationFactory.create("email");
n.send("Hello!");
String & Data
35. How does Java handle String immutability?
Strings are immutable because:
- Security — class names, file paths, network connections use strings.
- Thread safety — immutable objects are safe to share without synchronisation.
- String pool — the JVM can intern literals safely.
- Hash caching —
hashCode()is computed once and cached.
String s = "hello";
s.toUpperCase(); // returns a new String — s is unchanged
System.out.println(s); // "hello"
String upper = s.toUpperCase(); // must reassign
36. What are the key differences between Comparable and Comparator?
Comparable<T> |
Comparator<T> |
|
|---|---|---|
| Method | compareTo(T o) |
compare(T o1, T o2) |
| Defined in | The class itself | Separate class / lambda |
| Sorting order | Natural (single) | Custom (multiple) |
| Modifying class | Required | Not required |
class Student implements Comparable<Student> {
String name;
double gpa;
@Override
public int compareTo(Student o) {
return Double.compare(this.gpa, o.gpa); // natural order by GPA
}
}
// Custom comparator — without changing Student
Comparator<Student> byName = Comparator.comparing(s -> s.name);
Comparator<Student> byGpaDesc = Comparator.comparingDouble(Student::gpa).reversed();
students.sort(byName.thenComparing(byGpaDesc));
Java Records, Sealed Classes & Modern Features
37. What are Java Records (Java 16+)?
Records are immutable data carriers that auto-generate constructor, getters, equals, hashCode, toString.
// Before — verbose DTO
public final class Point {
private final int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
public int x() { return x; }
public int y() { return y; }
@Override public boolean equals(Object o) { ... }
@Override public int hashCode() { ... }
@Override public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}
// Record — one line
record Point(int x, int y) {}
Point p = new Point(3, 4);
System.out.println(p.x()); // 3
System.out.println(p); // Point[x=3, y=4]
Records can have compact constructors for validation:
record Range(int min, int max) {
Range { // compact constructor — parameters already assigned
if (min > max) throw new IllegalArgumentException("min > max");
}
}
38. What are Sealed Classes (Java 17+)?
Sealed classes restrict which classes can extend them — useful for exhaustive pattern matching.
public sealed interface Shape
permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Triangle(double base, double h)implements Shape {}
// Pattern matching with exhaustive switch (Java 21+)
double area(Shape s) {
return switch (s) {
case Circle(var r) -> Math.PI * r * r;
case Rectangle(var w, var h) -> w * h;
case Triangle(var b, var h) -> 0.5 * b * h;
};
}
39. What is pattern matching for instanceof (Java 16+)?
// Before Java 16
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
// Java 16+ — pattern variable
if (obj instanceof String s) {
System.out.println(s.length()); // s is in scope here
}
// Java 21 — switch patterns
Object value = getPayload();
String result = switch (value) {
case Integer i -> "int: " + i;
case String s -> "str: " + s.toUpperCase();
case null -> "null";
default -> "other";
};
40. What are Virtual Threads (Java 21)?
Virtual threads (Project Loom) are lightweight JVM-managed threads — you can create millions of them without exhausting OS threads.
// Traditional thread pool — OS threads are scarce
ExecutorService platform = Executors.newFixedThreadPool(200);
// Virtual thread per task — scales to millions
ExecutorService virtual = Executors.newVirtualThreadPerTaskExecutor();
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
exec.submit(() -> {
Thread.sleep(Duration.ofMillis(100)); // blocking is fine
return "done";
});
}
}
Virtual threads are not faster for CPU-bound tasks — they shine for I/O-bound work (HTTP, database, file I/O) where threads spend most time waiting.
Spring Boot Basics
41. What is dependency injection? How does Spring implement it?
DI is a technique where an object's dependencies are provided externally rather than created internally (Inversion of Control).
// Without DI — tight coupling
class OrderService {
private PaymentGateway gateway = new StripeGateway(); // hardcoded
}
// With Spring DI — loose coupling
@Service
class OrderService {
private final PaymentGateway gateway;
@Autowired // Spring injects the implementation
OrderService(PaymentGateway gateway) {
this.gateway = gateway;
}
}
@Component
class StripeGateway implements PaymentGateway { ... }
Spring's IoC container wires beans together. Constructor injection (shown above) is preferred over field injection (@Autowired on field) — it makes dependencies explicit and testable.
42. What are the main Spring annotations?
| Annotation | Purpose |
|---|---|
@SpringBootApplication |
Entry point (@Configuration + @EnableAutoConfiguration + @ComponentScan) |
@RestController |
@Controller + @ResponseBody |
@GetMapping, @PostMapping |
HTTP method shortcuts |
@Service |
Business layer bean |
@Repository |
Data layer bean (wraps persistence exceptions) |
@Component |
Generic bean |
@Autowired |
Inject dependency |
@Value("${prop}") |
Inject property |
@Transactional |
Wrap method in transaction |
@Entity, @Table |
JPA entity mapping |
@Bean |
Register a method's return value as a bean |
43. What is @Transactional? What are its pitfalls?
@Service
class TransferService {
@Transactional // commits on success, rolls back on RuntimeException
public void transfer(Account from, Account to, BigDecimal amount) {
from.debit(amount);
to.credit(amount);
// if exception thrown here, BOTH operations roll back
}
}
Common pitfalls:
- Self-invocation —
this.transfer()bypasses the proxy, so@Transactionalhas no effect. Injectselfor restructure. - Checked exceptions — by default only
RuntimeExceptiontriggers rollback. Use@Transactional(rollbackFor = Exception.class). @Transactionalonprivatemethods — proxies can't intercept private methods.- Lazy loading outside transaction —
LazyInitializationExceptionwhen accessing collections after transaction ends.
Miscellaneous & Tricky Questions
44. What is the difference between == for Integer objects?
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true — cached (-128 to 127)
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false — different objects beyond cache range
System.out.println(c.equals(d)); // true — always use equals()
Java caches Integer values from -128 to 127 (and Boolean, small Long, Short, Byte). Comparing with == outside this range gives unexpected results.
45. What is covariant return type?
An overriding method can return a more specific type than the parent's return type.
class Animal {
Animal clone() { return new Animal(); }
}
class Dog extends Animal {
@Override
Dog clone() { return new Dog(); } // covariant return — Dog is a subtype of Animal
}
Animal a = new Dog();
Dog d = ((Dog) a).clone(); // returns Dog, no cast needed via Dog reference
46. What is the contract between hashCode and equals?
- If
a.equals(b)is true, thena.hashCode() == b.hashCode()must be true. - If
a.hashCode() != b.hashCode(), thena.equals(b)must be false. - If
a.hashCode() == b.hashCode(),a.equals(b)may be true or false (collision).
Breaking this contract causes HashMap/HashSet to malfunction — objects become unretrievable.
class Point {
int x, y;
@Override
public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y); // consistent with equals
}
}
47. What are var (local variable type inference, Java 10+)?
var lets the compiler infer the type of a local variable from the right-hand side expression.
// Before
Map<String, List<Integer>> map = new HashMap<String, List<Integer>>();
// With var
var map = new HashMap<String, List<Integer>>();
// Useful in for-each
for (var entry : map.entrySet()) {
System.out.println(entry.getKey() + " → " + entry.getValue());
}
// NOT allowed: return types, fields, method params, null literal
// var x = null; // compile error — type cannot be inferred
// private var name = ""; // compile error — not local variable
48. What are CompletableFuture key methods?
// Async chain
CompletableFuture.supplyAsync(() -> fetchUser(id)) // runs on ForkJoinPool
.thenApply(user -> fetchOrders(user.id)) // transform
.thenAccept(orders -> sendEmail(orders)) // consume
.exceptionally(ex -> { log(ex); return null; }); // error handler
// Combine two futures
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> "World");
cf1.thenCombine(cf2, (a, b) -> a + " " + b)
.thenAccept(System.out::println); // "Hello World"
// Wait for all
CompletableFuture.allOf(cf1, cf2).join();
// Wait for any
CompletableFuture.anyOf(cf1, cf2).thenAccept(System.out::println);
49. What is the difference between Iterator.remove() and Collection.remove()?
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
// WRONG — ConcurrentModificationException
for (String s : list) {
if (s.equals("b")) list.remove(s); // modifying while iterating
}
// Correct — Iterator.remove()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("b")) it.remove(); // safe
}
// Or use removeIf (Java 8+)
list.removeIf(s -> s.equals("b"));
50. What is String.intern()? When would you use it?
intern() returns the canonical representation of a String from the JVM's String pool.
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false — heap objects
String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s3 == s4); // true — same pool reference
When to use: Processing millions of repeated strings (e.g., parsing log files with many repeated field names) can save memory via interning. Modern JVMs handle many common cases automatically (literals, class names). Profile before using — interning has overhead and can stress the old generation.
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
== on Strings / Integer |
Reference comparison, not value | Use .equals() or Objects.equals() |
Catching Exception everywhere |
Hides bugs | Catch specific exception types |
e.printStackTrace() in production |
No structured logging | Use a logger (SLF4J + Logback) |
Ignoring InterruptedException |
Swallows cancellation signal | Restore interrupt: Thread.currentThread().interrupt() |
| Accessing lazy JPA collection outside transaction | LazyInitializationException |
Use @Transactional or fetch eagerly |
Mutable static fields |
Hidden global state, thread safety issues | Use final fields; inject via DI |
| Not closing streams / connections | Resource leak | Use try-with-resources |
null return instead of Optional |
Unexpected NPEs | Return Optional<T> from methods |
FAQ
Q: What Java version should I target for interviews?
Most questions target Java 8-11 (streams, lambdas, Optional) but knowing Java 17+ features (records, sealed classes, pattern matching) stands out. Java 21 (virtual threads, sequenced collections) is increasingly relevant.
Q: Is Java pass-by-value or pass-by-reference?
Always pass-by-value. For objects, the value passed is the reference (memory address), not the object itself. You can mutate the object through the reference, but you can't make the caller's variable point to a different object.
void change(StringBuilder sb) {
sb.append(" world"); // mutates the object — visible to caller
sb = new StringBuilder("replaced"); // rebinds local var only — NOT visible
}
Q: What is the difference between throw and throws?throw is a statement that throws an exception. throws is a method declaration that declares what checked exceptions a method may throw.
void risky() throws IOException { // declares
throw new IOException("failed"); // throws
}
Q: Can you override a static method?
No. static methods belong to the class, not an instance — they are hidden, not overridden. Calling via a superclass reference invokes the superclass's version regardless of the actual runtime type.
Q: What happens if main() throws an exception?
The JVM prints the stack trace to stderr and exits with a non-zero exit code. The thread's uncaught exception handler is invoked first (Thread.getDefaultUncaughtExceptionHandler()).
Q: What is the difference between String.format() and text blocks (Java 15+)?
// String.format
String sql = String.format("SELECT * FROM %s WHERE id = %d", table, id);
// Text block (Java 15+) — preserves indentation, no escape needed
String json = """
{
"name": "Alice",
"age": 30
}
""";
Text blocks handle multiline strings cleanly. Use String.format() or formatted() for dynamic substitution within text blocks.