These are the deep-cut questions that separate senior Java developers from mid-level engineers. Interviewers at senior/lead level don't ask "what is a HashMap" — they probe JVM internals, threading models, GC tuning, Java 21 virtual threads, and architectural decisions. Every answer here is crafted to be interview-ready.
-Xss).-XX:MaxMetaspaceSize.-XX:ReservedCodeCacheSize.ByteBuffer.allocateDirect) and frameworks like Netty. Not GC-managed.System.gc(), or metaspace overflow.Modern collectors like G1 and ZGC minimize STW pauses by doing most work concurrently. With ZGC on Java 21, pauses are sub-millisecond regardless of heap size.
The JIT (Just-In-Time) compiler converts bytecode to native machine code at runtime. It profiles code while running in interpreted mode and compiles hot spots.
Tiered compilation (default since Java 8) has 5 levels:
Methods start at level 0, quickly move to level 3 for profiling, then to level 4 once profiling data is collected. C2 applies aggressive optimisations: inlining, loop unrolling, dead code elimination, escape analysis.
A Java memory leak is when objects are no longer needed but are still reachable by GC roots — so GC cannot collect them. Common causes:
HashMap as a cache)How to find:
jmap -dump:live,format=b,file=heap.hprof <pid> or via /actuator/heapdumpFix: Remove the unintended reference — clear the collection, deregister the listener, use WeakHashMap or Caffeine cache with expiry.
WeakHashMap — entries disappear when the key is no longer strongly reachable. Good for canonical maps (interning).finalize(). Queried from a ReferenceQueue.Cleaner (built on PhantomReference) as the recommended replacement for finalize().The JMM defines how threads interact through memory — what values a thread is guaranteed to see when it reads a variable written by another thread.
Key rules:
volatile guarantees all threads see the latest write.static initialisation.// Without volatile, thread B may never see flag = true
volatile boolean flag = false;
// Thread A
flag = true;
// Thread B
while (!flag) { } // guaranteed to eventually see true
Class loading is the process of reading a .class file and creating a Class object in the JVM. The parent-delegation model:
Classloader hierarchy: Bootstrap (JDK core) → Platform (ext libs) → Application (your classpath) → custom classloaders.
This prevents user code from replacing core JDK classes (e.g. java.lang.String). Application servers use custom classloaders to isolate web apps — each app gets its own classloader instance, so two apps can use different versions of the same library without conflict.
The String pool (string intern pool) is a cache of String literals stored in the heap (moved from PermGen to Heap in Java 7). When you write "hello" as a literal, the JVM checks if that value already exists in the pool and reuses it.
String a = "hello";
String b = "hello";
System.out.println(a == b); // true — same pool object
String c = new String("hello");
System.out.println(a == c); // false — c is a new heap object
System.out.println(a == c.intern()); // true — intern() returns pool ref
String interning saves memory when many identical strings exist (e.g. database column names, enum-like values). But overusing intern() can fill the pool and cause GC pressure in older JVMs.
-Xss, default ~512KB–1MB). Almost always infinite recursion. Read the stack trace — the repeating pattern is the culprit. Fix: add a base case, or convert to iteration.-XX:MaxMetaspaceSize temporarily, then find the leak.-XX:MaxDirectMemorySize or find the NIO buffer leak.Escape analysis is a JIT optimisation that determines whether an object's reference can "escape" the method that created it (be stored in a field, returned, or passed to another thread).
If an object does not escape, the JIT can:
// The JIT may stack-allocate 'point' here if it doesn't escape
public double distance() {
Point point = new Point(3, 4); // may never touch the heap
return Math.sqrt(point.x * point.x + point.y * point.y);
}
-XX:+DoEscapeAnalysis (default on). Check with -XX:+PrintEscapeAnalysis.synchronized — mutual exclusion + visibility. One thread at a time inside the block. Simple but not interruptible, no tryLock, always reentrant.volatile — visibility only, no atomicity. Guarantees reads/writes go straight to main memory, not a CPU cache. Good for flags (running = false) but not for compound operations like count++.ReentrantLock — same mutual exclusion as synchronized but with more control: tryLock(timeout), lockInterruptibly(), fair ordering, multiple condition variables. Use when you need any of these features.ReentrantLock lock = new ReentrantLock();
try {
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try { /* critical section */ }
finally { lock.unlock(); }
}
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
A deadlock occurs when two or more threads each hold a lock that the other needs:
// Thread 1: lock A then B
// Thread 2: lock B then A → deadlock
Four conditions must all be true (Coffman conditions): mutual exclusion, hold-and-wait, no preemption, circular wait.
Prevention strategies:
tryLock(timeout); release held locks and retry if timeout expires.java.util.concurrent collections (ConcurrentHashMap, ConcurrentLinkedQueue) which use CAS instead of locks.Diagnose: take a thread dump (jstack <pid>). A deadlock shows a cycle in the "waiting on" chain.
Runnable — void run(). Cannot return a value or throw a checked exception.Callable<V> — V call() throws Exception. Returns a value and can throw checked exceptions.ExecutorService exec = Executors.newFixedThreadPool(4);
Future<Integer> future = exec.submit(() -> {
Thread.sleep(1000);
return 42;
});
// Do other work while the task runs...
Integer result = future.get(); // blocks until done, returns 42
Future lets you: check if done (isDone()), wait with timeout (get(timeout, unit)), or cancel (cancel(true)). CompletableFuture extends this with non-blocking callbacks and composable pipelines.
// Chain async tasks without blocking
CompletableFuture
.supplyAsync(() -> fetchUser(userId)) // async task
.thenApply(user -> enrichWithProfile(user)) // transform result
.thenCompose(user -> fetchOrders(user.getId())) // flat-map async
.thenCombine(fetchPreferences(userId), // merge two futures
(orders, prefs) -> build(orders, prefs))
.exceptionally(ex -> Response.error(ex.getMessage())) // error handling
.thenAccept(response -> sendToClient(response)); // terminal action
Key methods:
thenApply — synchronous transform (like Stream.map)thenCompose — flat-map: when the function returns another CompletableFuturethenCombine — combine two independent futures when both completeallOf — wait for all futures to completeanyOf — complete when any future completesBy default runs on ForkJoinPool.commonPool(). Pass a custom Executor as the second arg to any *Async variant for better control.
HashMap is not thread-safe. Concurrent modification causes data corruption, infinite loops (Java 7), or ConcurrentModificationException.
ConcurrentHashMap (Java 8+) uses segment-level locking — it locks only the bucket being modified, not the entire map. This allows:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Atomic compute — avoids get + put race condition:
map.compute("key", (k, v) -> v == null ? 1 : v + 1);
// Or: map.merge("key", 1, Integer::sum);
Collections.synchronizedMap() is different — it locks the entire map on every operation, giving lower concurrency than ConcurrentHashMap.ForkJoinPool is a specialised thread pool designed for divide-and-conquer tasks. Each thread has its own deque of tasks. Work stealing: when a thread's deque is empty, it "steals" tasks from the tail of another thread's deque. This keeps all threads busy with minimal contention.
ForkJoinPool pool = new ForkJoinPool(4);
long sum = pool.invoke(new SumTask(array, 0, array.length));
class SumTask extends RecursiveTask<Long> {
protected Long compute() {
if (length <= THRESHOLD) return sumSequentially();
SumTask left = new SumTask(array, start, mid);
SumTask right = new SumTask(array, mid, end);
left.fork(); // run left in parallel
return right.compute() + left.join(); // run right here, wait for left
}
}
The common ForkJoinPool (ForkJoinPool.commonPool()) backs parallel streams and CompletableFuture async methods. Its size = CPU cores - 1 by default.
A race condition occurs when a program's result depends on the relative timing of thread execution. Classic example: count++ is three operations (read, increment, write) and is not atomic:
// Thread A reads 5, Thread B reads 5, both write 6 → result is 6 not 7
int count = 0;
count++; // NOT thread-safe
java.util.concurrent.atomic classes use hardware CAS (Compare-And-Swap) for lock-free atomicity:
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // atomic, no lock needed
count.compareAndSet(expected, newValue); // CAS operation
LongAdder adder = new LongAdder(); // better under high contention
adder.increment(); // uses multiple cells to reduce CAS contention
LongAdder outperforms AtomicLong under high write contention by spreading updates across cells and summing on read.
ThreadLocal gives each thread its own independent copy of a variable — no synchronisation needed because threads never share it.
ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd"));
String formatted = sdf.get().format(new Date()); // thread-safe
Risks:
threadLocal.remove() in a finally block.ScopedValue instead.ArrayBlockingQueue — bounded, backed by array. Fair (FIFO) ordering. Good when you want backpressure — producers block when full.LinkedBlockingQueue — optionally bounded, backed by linked nodes. Separate locks for head and tail — higher throughput than ArrayBlockingQueue under contention.SynchronousQueue — no capacity. Each put must be paired with a take. The "hand-off" queue. Used in Executors.newCachedThreadPool().PriorityBlockingQueue — unbounded, orders elements by priority. Good for task scheduling.DelayQueue — elements become available only after a delay. Good for scheduled retries, cache expiry.Thread.sleep(ms) — pauses the current thread for at least ms. Does NOT release locks. Throws InterruptedException.Object.wait() — must hold the object's monitor. Releases the lock and waits until notify() / notifyAll() / timeout. Throws InterruptedException.Thread.yield() — hint to the scheduler to give other threads a chance. Rarely useful; scheduler may ignore it.thread.join() — the calling thread waits until the target thread completes. Used to wait for background threads to finish before proceeding.wait() in a loop: while (!condition) { wait(); } to handle spurious wakeups.Platform threads = OS threads. Each one consumes ~1MB stack and is expensive to create. Thread pools are needed to control cost.
Virtual threads (Project Loom, GA in Java 21) are JVM-managed, lightweight threads. They are multiplexed onto a small pool of OS carrier threads. When a virtual thread blocks on I/O, the carrier thread is freed to run other virtual threads.
// Create millions of virtual threads cheaply
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 1_000_000).forEach(i ->
exec.submit(() -> handleRequest(i)));
}
Key differences:
Thread API — no new programming modelScopedValueRecords (Java 16) are immutable data carriers. They auto-generate constructor, getters, equals(), hashCode(), and toString():
// Instead of 40 lines of boilerplate:
record Point(double x, double y) {}
record UserDTO(Long id, String name, String email) {
// Compact constructor for validation:
UserDTO {
Objects.requireNonNull(name, "name required");
name = name.trim();
}
}
Point p = new Point(3.0, 4.0);
System.out.println(p.x()); // getter
System.out.println(p); // Point[x=3.0, y=4.0]
Use records for: DTOs, value objects, database query results, API response payloads. Don't use for entities that need mutable state.
Sealed classes (Java 17) restrict which classes can extend or implement them:
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 height) implements Shape {}
// Pattern matching switch — compiler knows all cases are covered:
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.w() * r.h();
case Triangle t -> 0.5 * t.base() * t.height();
// No default needed — compiler knows it's exhaustive
};
This replaces the visitor pattern and instanceof chains. The compiler enforces that all permitted subtypes are handled — you can't forget a case at compile time.
// Before Java 16:
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
// Java 16+:
if (obj instanceof String s) {
System.out.println(s.length()); // s is already String, no cast
}
// With guards (Java 21 switch):
switch (obj) {
case Integer i when i > 0 -> System.out.println("Positive: " + i);
case Integer i -> System.out.println("Non-positive: " + i);
case String s -> System.out.println("String: " + s);
default -> System.out.println("Other");
}
// Before (Java 14-):
String json = "{\n" +
" \"name\": \"Alice\",\n" +
" \"email\": \"alice@example.com\"\n" +
"}";
// Text blocks (Java 15+):
String json = """
{
"name": "Alice",
"email": "alice@example.com"
}
""";
String sql = """
SELECT u.id, u.name, o.total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.active = true
""";
The indentation is automatically stripped based on the closing """ position. Use for SQL, JSON, HTML, XML embedded in Java code.
Java 21 added three new interfaces in the collections hierarchy to unify first/last element access which previously required different APIs per collection type:
SequencedCollection<E> — adds getFirst(), getLast(), addFirst(), addLast(), removeFirst(), removeLast(), reversed()SequencedSet<E> — extends SequencedCollection, no duplicatesSequencedMap<K,V> — adds firstEntry(), lastEntry(), sequencedKeySet(), reversed()List<String> list = new ArrayList<>(List.of("a","b","c"));
list.getFirst(); // "a" — no more get(0)
list.getLast(); // "c" — no more get(list.size()-1)
list.reversed(); // reversed view
Structured Concurrency treats concurrent tasks as a unit: if a parent task spawns child tasks, the parent doesn't complete until all children complete or the first fails:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<User> user = scope.fork(() -> fetchUser(id));
Future<List<Order>> orders = scope.fork(() -> fetchOrders(id));
scope.join().throwIfFailed(); // both must succeed
return new Response(user.resultNow(), orders.resultNow());
}
ScopedValue is an immutable, inheritable thread-local replacement — values flow into child tasks automatically without copy overhead:
static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
ScopedValue.where(CURRENT_USER, user).run(() -> handleRequest());
sun.* and com.sun.* packages is denied by default. Libraries that relied on internal APIs must be updated.Java 17 is also the LTS release that most production teams migrated to from Java 11. Java 21 is the next LTS.
HashMap uses an array of buckets. On put(key, value):
key.hashCode() is computed and spread with a secondary hash.hash & (capacity - 1).When the load factor (default 0.75) is exceeded, the map doubles capacity and rehashes all entries. This is expensive — pre-size maps when the final size is known: new HashMap<>(expectedSize / 0.75 + 1).
Default choice: ArrayList for List, ArrayDeque for stack/queue.
Generic type parameters are erased at compile time — List<String> and List<Integer> are the same List at runtime. The compiler inserts casts and checks but the JVM sees only raw types.
Implications:
instanceof with generic types: x instanceof List<String> is a compile error.new T[10] is illegal.List<String>.class doesn't exist, only List.class.Workaround: pass Class<T> as a constructor argument (type token), or use Jackson's TypeReference<List<User>>(){} which captures the type at construction via reflection on the anonymous subclass.
? extends T and ? super T?HardThe PECS principle: Producer Extends, Consumer Super.
List<? extends Animal> — the list produces Animals (you can read from it as Animal). But you cannot add to it (compiler doesn't know the exact subtype).List<? super Cat> — the list consumes Cats (you can add Cat or subtypes). But you can only read from it as Object.// Copy from source to dest:
public <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) dest.add(item);
}
// Collections.sort signature:
public static <T> void sort(List<T> list,
Comparator<? super T> c)
removeEldestEntry), ordered JSON output.subMap, headMap, tailMap), sorted iteration, finding floor/ceiling keys.TreeMap<Integer, String> map = new TreeMap<>();
map.put(3,"c"); map.put(1,"a"); map.put(2,"b");
map.floorKey(2); // 2
map.subMap(1, 3); // {1=a, 2=b}
List<String> list = List.of("a","b","c"); // Java 9+
Set<String> set = Set.of("x","y");
Map<String,Integer> map = Map.of("a",1,"b",2);
Map<String,Integer> map2 = Map.ofEntries( // for larger maps
Map.entry("a",1), Map.entry("b",2));
// Java 10+:
List<String> copy = List.copyOf(existingList); // immutable copy
Key characteristics of List.of / Set.of / Map.of:
UnsupportedOperationExceptionNullPointerExceptionThe best approach — enum singleton (guaranteed by JVM, handles serialisation and reflection attacks):
public enum DatabaseConnection {
INSTANCE;
public void query(String sql) { ... }
}
Alternative — initialization-on-demand holder (lazy, thread-safe, no synchronisation overhead):
public class Config {
private Config() {}
private static class Holder {
static final Config INSTANCE = new Config();
}
public static Config getInstance() { return Holder.INSTANCE; }
}
The class is only loaded when first accessed. The JVM class loading guarantee ensures thread safety without synchronized.
public class HttpRequest {
private final String url;
private final String method;
private final Map<String, String> headers;
private final int timeoutMs;
private HttpRequest(Builder b) {
this.url = b.url;
this.method = b.method;
this.headers = Collections.unmodifiableMap(b.headers);
this.timeoutMs = b.timeoutMs;
}
public static class Builder {
private final String url; // required
private String method = "GET"; // optional with default
private Map<String, String> headers = new HashMap<>();
private int timeoutMs = 5000;
public Builder(String url) { this.url = url; }
public Builder method(String m) { method = m; return this; }
public Builder header(String k, String v) { headers.put(k,v); return this; }
public Builder timeout(int ms) { timeoutMs = ms; return this; }
public HttpRequest build() { return new HttpRequest(this); }
}
}
// Usage:
HttpRequest req = new HttpRequest.Builder("https://api.example.com")
.method("POST").header("Auth","Bearer xyz").timeout(3000).build();
Strategy defines a family of interchangeable algorithms behind a common interface. The client chooses the algorithm at runtime.
@FunctionalInterface
interface SortStrategy {
void sort(int[] arr);
}
class Sorter {
private final SortStrategy strategy;
public Sorter(SortStrategy strategy) { this.strategy = strategy; }
public void sort(int[] arr) { strategy.sort(arr); }
}
// Usage with lambdas (lambdas ARE strategy pattern):
Sorter s = new Sorter(Arrays::sort);
s.sort(new int[]{3,1,2});
// Or swap strategy at runtime:
Sorter s2 = new Sorter(arr -> bubbleSort(arr));
Java 8+ functional interfaces make Strategy pattern trivial — most cases where you'd write a Strategy interface are now just a Function<T,R>, Comparator, or custom @FunctionalInterface.
Observer defines a one-to-many dependency: when a subject changes state, all registered observers are notified automatically.
// Modern approach with generics:
interface EventListener<T> { void onEvent(T event); }
class EventBus<T> {
private final List<EventListener<T>> listeners = new CopyOnWriteArrayList<>();
public void subscribe(EventListener<T> l) { listeners.add(l); }
public void publish(T event) { listeners.forEach(l -> l.onEvent(event)); }
}
EventBus<OrderEvent> bus = new EventBus<>();
bus.subscribe(event -> notificationService.send(event));
bus.subscribe(event -> auditLog.record(event));
bus.publish(new OrderEvent(orderId, "PLACED"));
In Spring, ApplicationEventPublisher + @EventListener is the built-in observer implementation. Spring Cloud Stream and Kafka provide the same pattern across service boundaries.
Decorator wraps an object to add behaviour without changing its interface or creating subclasses.
Java I/O is the classic example — each class wraps another:
// Layer by layer:
InputStream raw = new FileInputStream("data.gz");
InputStream buffered = new BufferedInputStream(raw); // add buffering
InputStream compressed = new GZIPInputStream(buffered); // add decompression
Reader reader = new InputStreamReader(compressed, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(reader); // add line reading
String line = br.readLine();
Each layer adds a single responsibility. You compose them as needed without a combinatorial explosion of subclasses. This is also the pattern behind Spring's HttpServletRequestWrapper and most filter chains.
Proxy provides a surrogate for another object, controlling access to it. Spring uses JDK dynamic proxies and CGLIB proxies to implement AOP:
// JDK Dynamic Proxy (interface-based):
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class[]{UserService.class},
(proxyObj, method, args) -> {
System.out.println("Before: " + method.getName());
Object result = method.invoke(target, args);
System.out.println("After: " + method.getName());
return result;
});
Spring uses this pattern for @Transactional, @Cacheable, @Async, @Secured — the actual bean you inject is a proxy that intercepts method calls and adds the cross-cutting behaviour. This is why self-invocation (this.method()) bypasses these annotations — the call skips the proxy.
Command encapsulates a request as an object, allowing parameterisation, queuing, logging, and undoable operations.
@FunctionalInterface
interface Command { void execute(); }
class CommandQueue {
private final Queue<Command> queue = new LinkedList<>();
public void enqueue(Command cmd) { queue.offer(cmd); }
public void run() { while (!queue.isEmpty()) queue.poll().execute(); }
}
// Usage:
queue.enqueue(() -> emailService.send(email));
queue.enqueue(() -> auditLog.record(action));
queue.run();
Used in: Spring Batch (Step, Tasklet), Undo/Redo systems, Task schedulers, CQRS command buses, Spring Integration MessageHandler.
UserService that handles auth, sends emails, AND generates reports.PaymentProcessor instead of a new implementation.Square extends Rectangle where setting width also sets height — breaks width/height independence.Animal interface with fly(), swim(), run() — Dog is forced to implement fly().OrderService instantiating new MySQLOrderRepository() instead of injecting an OrderRepository interface.Rule: G1 for most production workloads. ZGC when latency SLAs are strict (<1ms GC pause required).
Tools:
-XX:+FlightRecorder. Analyse with JDK Mission Control.# async-profiler 30s CPU profile to flamegraph HTML
./profiler.sh -d 30 -f flamegraph.html <pid>
# JFR: record 60s, download and open in Mission Control
jcmd <pid> JFR.start duration=60s filename=recording.jfr
CPU cache lines are typically 64 bytes. When two threads write to different variables that happen to be on the same cache line, each write invalidates the other thread's cache — causing unnecessary cache misses (false sharing).
// Bad: both counters may share a cache line
class Counters {
long counterA;
long counterB;
}
// Fix: pad to separate cache lines
@jdk.internal.vm.annotation.Contended // Java 8+ — adds padding automatically
class Counter {
long value;
}
// Or manual padding:
class Counter {
long p1,p2,p3,p4,p5,p6,p7; // 56 bytes padding
long value; // on its own 64-byte cache line
long q1,q2,q3,q4,q5,q6,q7;
}
Relevant in high-throughput ring buffers and concurrent counters. LMAX Disruptor's Sequence class uses this technique to achieve ~100M ops/sec.
Object pooling reuses expensive-to-create objects rather than creating and discarding them. Examples where it helps:
PooledByteBufAllocator.When not to pool normal objects: modern GC is very fast at allocating and collecting short-lived objects (Eden space is just a pointer bump). Pooling small plain objects often adds synchronisation overhead that outweighs the allocation savings. Measure first.
// Apache Commons Pool2 for custom pools:
GenericObjectPool<ExpensiveConnection> pool =
new GenericObjectPool<>(factory, config);
ExpensiveConnection conn = pool.borrowObject();
try { conn.doWork(); } finally { pool.returnObject(conn); }
top -H -p <pid> (Linux) — shows per-thread CPU. Note the thread ID in hex.jstack <pid> — find the thread matching the hex ID (nid=0x...).Common causes of unexpectedly high CPU:
jstat -gcutil <pid> 1000 — if GC time > 5%, GC is a problem)Traditional profilers (VisualVM CPU profiler) sample thread state only at JVM safepoints — points in code where it is safe to pause all threads (method returns, loop back-edges). CPU-intensive code that never reaches a safepoint is invisible.
This causes safepoint bias: you see methods that happen to safepoint, not the methods burning CPU.
async-profiler uses POSIX signals (SIGPROF) and the internal AsyncGetCallTrace API to interrupt threads at any arbitrary point — no safepoint required. This gives a true picture of where CPU time is spent, including tight loops and native code.
Parallel streams use the ForkJoinPool's common pool to split the stream's source, process sub-tasks in parallel, and combine results. They help when:
Avoid parallel streams when:
// Measure before using:
long result = LongStream.rangeClosed(1, 10_000_000)
.parallel()
.filter(n -> isPrime(n)) // CPU-bound: good candidate
.count();
JMH (Java Microbenchmark Harness) is the standard tool for measuring Java performance. It handles JVM warm-up, JIT compilation, dead code elimination, and measurement noise.
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class StringBenchmark {
private String value = "hello world";
@Benchmark
public boolean contains() {
return value.contains("world"); // result returned → not dead-code eliminated
}
@Benchmark
public int indexOf() {
return value.indexOf("world");
}
}
// Run: java -jar benchmarks.jar -wi 5 -i 10 -f 2
Key rules for valid benchmarks: always return or consume results (Blackhole.consume()), use @State to keep shared data, run warmup iterations, use multiple forks. Anything less gives numbers that the JIT can make meaningless.
Exception) — the compiler forces callers to handle or declare them. E.g. IOException, SQLException.RuntimeException) — no compiler enforcement. E.g. NullPointerException, IllegalArgumentException.Modern Java opinion: prefer unchecked exceptions for most application code. Checked exceptions were intended for recoverable conditions but in practice:
Use checked exceptions when: the caller can realistically recover (e.g. file not found → ask user for another path). Use unchecked when: it's a programming error or unrecoverable condition.
Reflection lets you inspect and manipulate classes, fields, and methods at runtime without compile-time knowledge:
Class<?> cls = Class.forName("com.example.UserService");
Method method = cls.getDeclaredMethod("findById", Long.class);
method.setAccessible(true); // bypass private
Object result = method.invoke(serviceInstance, 42L);
Performance: reflective invocation is 2–10x slower than direct call due to security checks, argument boxing, and no JIT inlining. In hot paths, use MethodHandle (Java 7+) or invoke dynamic — these can be inlined by the JIT.
Security: setAccessible(true) bypasses Java's access control. Java 17+ strong encapsulation blocks this for JDK internal classes. Modules (module-info.java) control which packages allow deep reflection.
Inheritance ("is-a"): a class extends another, inheriting state and behaviour. Creates tight coupling — subclass depends on superclass internals. Any change to the superclass can break subclasses (fragile base class problem).
Composition ("has-a"): a class holds references to objects providing the behaviour it needs. Much more flexible — you can swap implementations, combine multiple behaviours, and test in isolation.
// Prefer this (composition):
class UserService {
private final UserRepository repo; // composed
private final EmailService emailSvc; // composed
private final AuditLogger audit; // composed
}
// Over this (inheritance chain):
class UserService extends BaseService extends AbstractRepository { ... }
Rule: inherit when the relationship truly is "is-a" and the subclass is a specialisation (not just reuse). Compose when you need flexibility, multiple behaviours, or to avoid coupling to internals. Effective Java: "Favour composition over inheritance."
// Good use: represent optional return value from a method
Optional<User> findByEmail(String email);
// Chain without null checks:
findByEmail("alice@example.com")
.map(User::getProfile)
.filter(p -> p.isVerified())
.ifPresentOrElse(this::sendWelcome, this::sendVerificationEmail);
Common mistakes:
optional.get() without checking — throws NoSuchElementException. Use orElse, orElseGet, or orElseThrow.optional.isPresent() + optional.get() — just use optional.map() or ifPresent().Optional<Integer> — use OptionalInt, OptionalLong.orElse(expensiveCall()) — orElse always evaluates the argument. Use orElseGet(() -> expensiveCall()) for lazy evaluation.// Correct double-checked locking (requires volatile):
public class HeavyResource {
private static volatile HeavyResource instance;
public static HeavyResource getInstance() {
if (instance == null) { // first check (no lock)
synchronized (HeavyResource.class) {
if (instance == null) { // second check (with lock)
instance = new HeavyResource();
}
}
}
return instance;
}
}
volatile is essential. Without it, the JIT may reorder instance = new HeavyResource() — another thread may see a partially constructed object. Volatile establishes a happens-before relationship on the write.
In most cases the initialization-on-demand holder (Q35) is simpler and equally correct. Use DCL only when you need runtime-configurable lazy init that the holder pattern can't express.
Java serialisation converts an object graph to a byte stream (ObjectOutputStream) and back (ObjectInputStream). Enabled by implementing Serializable.
Problems:
serialVersionUID must be managed. Changing class fields breaks compatibility.Modern alternatives: Jackson JSON, Protocol Buffers, Avro, Kryo. Only use Java serialisation for JVM-internal caching (e.g. Ehcache) and only with deserialization filters (ObjectInputFilter).
Method inlining is when the JIT replaces a method call with the body of the callee — eliminating the call overhead and enabling further optimisations on the combined code.
// Before inlining:
int result = add(a, b);
int add(int x, int y) { return x + y; }
// After inlining (JIT does this):
int result = a + b; // call eliminated
Inlining triggers further optimisations: constant folding, dead code elimination, escape analysis on the inlined result.
Inlining limits: JIT won't inline methods over ~325 bytecodes (-XX:MaxInlineSize) or call chains deeper than ~9 levels (-XX:MaxInlineLevel). In hot code, keep methods small to stay inlineable. Diagnose with -XX:+PrintInlining.
Object.clone() and copy constructors produce shallow copies by default.// Deep copy approaches:
// 1. Manual copy constructor — most explicit and fastest
// 2. Serialization round-trip (slow, requires Serializable)
// 3. Jackson: objectMapper.readValue(objectMapper.writeValueAsString(obj), Class)
// 4. ModelMapper / MapStruct for DTOs
// Immutable records avoid the problem entirely:
record Point(int x, int y) {} // no copy needed, safe to share
Best design: make objects immutable (records, final fields). Then you never need to copy them — sharing is safe.
Introduced in Java 9, JPMS (Project Jigsaw) adds a module layer above packages. Each module declares what it exports and what it requires:
// module-info.java
module com.example.userservice {
requires spring.core;
requires spring.web;
exports com.example.userservice.api; // public API
// com.example.userservice.internal not exported → inaccessible
}
Why introduced:
jlink creates custom JREs with only required modules (important for containers)In practice most teams use the classpath still. JPMS is most valuable for library authors and applications that need small Docker images.
This is the classic senior-level "real world" question. A structured approach:
spring.jpa.properties.hibernate.session.events.log.LOG_QUERIES_SLOWER_THAN_MS=100EXPLAIN ANALYZE. Add an index if needed.@EntityGraph./actuator/metrics/executor.active. If threads are maxed out, the endpoint is queued.jstat -gcutil <pid> 1000.@Cacheable with Redis.Now that you have covered Java internals, continue with other interview topics: