1. Which method in Object must be overridden whenever equals() is overridden?
Answer: hashCode().
The rule is really a contract, and it lives in the documentation of Object.hashCode(): if two objects are equal according to equals(), then calling hashCode() on both must produce the same integer.
Why does this matter? Hash-based collections — HashMap, HashSet, HashTable — do not work by calling equals() on everything. They work in two steps. First they use hashCode() to decide which bucket to look in. Only then do they use equals() to compare the objects actually in that bucket.
Now consider what happens if you override equals() to treat two objects as equal but leave hashCode() inherited. Two objects that are equal land in different buckets (because their hash codes differ). A HashSet then sees them as distinct — both get stored, the set silently loses its uniqueness guarantee. Or a HashMap fails to find a key that is actually equal to a stored key. The whole collection misbehaves, and the bug is maddening to track down because nothing throws.
The contract also runs the other way, but with a softer rule: two unequal objects may share a hash code (that is just a collision, handled by equals() inside the bucket). Equal must imply equal hash; unequal may share.
So the interview answer is crisp: whenever you override equals(), you must override hashCode() too, so that equal objects always produce equal hash codes. Ignore either half and hash collections stop working correctly.
2. What happens when executing System.gc() in Java code?
Answer: System.gc() is only a hint. It suggests to the JVM that garbage collection might be useful — but the JVM is free to ignore it.
The name is misleading. It sounds like a command, but it is a request. The JVM decides whether, when, and how thoroughly to run a collection. There is no guarantee that a System.gc() call performs immediate or complete collection, and no guarantee that unreferenced objects are reclaimed before the call returns.
Why doesn’t the JVM just obey? Because garbage collection has a cost. A full collection pauses application threads. The JVM’s own heuristics are tuned to minimize those pauses, and it has far better information about when collection is genuinely needed than a single line of application code. Forcing collections on demand would fight the collector’s scheduling and hurt performance.
In modern JDKs, System.gc() is largely a relic. The JVM handles collection automatically. When people call it, it is usually a sign of tuning by guesswork rather than by measurement — and the classic advice is: don’t call it in production code. If you genuinely need to influence collection, prefer JVM flags.
The interview answer: System.gc() is a non-binding hint. It does not force anything. The JVM decides.
3. What is the output of the following arithmetic code?
System.out.println(Math.min(Double.MIN_VALUE, 0.0d));
Output: 0.0
This is the kind of question that punishes assumptions. Most people read MIN_VALUE and think “most negative number” — the integer intuition. Doubles don’t work that way.
For integers, Integer.MIN_VALUE is indeed the most negative value, around -2.1 billion. For double, the situation is different. A double can hold numbers with wildly different magnitudes, so “minimum” is defined as the smallest positive non-zero value the type can represent — about 4.9 × 10^-324, a number so close to zero it’s practically nothing.
So Double.MIN_VALUE is positive. And Math.min(a, b) returns the smaller of the two arguments. Since 4.9 × 10^-324 is greater than 0.0, the smaller value is 0.0.
The answer prints 0.0, not Double.MIN_VALUE. The lesson: in Java, Double.MIN_VALUE is the smallest positive double, not the most negative one.
4. What is the default initial capacity and load factor for a standard HashMap?
Answer: Initial capacity 16, load factor 0.75.
The HashMap has two tuning dials.
Initial capacity is the number of buckets (the size of the internal array) created when the map is first built. The default is 16.
Load factor is the measure of how full the map is allowed to get before it resizes. The default is 0.75, meaning: when the map holds 75% of its capacity in entries — 12 entries for a 16-bucket map — it grows. On resize, the map typically doubles its capacity, rehashing and redistributing existing entries into the new buckets.
The trade-off behind the default is the usual one. A lower load factor (say 0.5) means fewer collisions and faster lookups, but more memory wasted and more frequent resizing. A higher load factor (say 0.9) uses memory more tightly but increases collision rates and slows lookups. 0.75 is the standard compromise, and in practice the JVM’s own HashMap-based structures — like HashSet and HashTable — use the same default.
The interview answer: capacity 16, load factor 0.75, resizing by doubling when 75% full.
5. What will be the output of this code?
boolean b1 = true;
boolean b2 = false;
System.out.println(b1 | b2 & b2);
Output: true
Two things are being tested here: operator precedence, and the difference between the bitwise operators on booleans.
Precedence first. In Java, & binds more tightly than |. So b1 | b2 & b2 is evaluated as b1 | (b2 & b2), not as (b1 | b2) & b2. Getting this grouping backwards gives the wrong answer.
Now evaluate b2 & b2. b2 is false, so false & false is false.
Then the outer operation: b1 | false. b1 is true, so true | false is true.
The output is true.
One more thing worth noting for interviews: |, &, and ^ work on booleans as non-short-circuiting logical operators — they always evaluate both sides. The short-circuiting twins || and && evaluate the right side only when needed. Here it doesn’t change the result, but it’s a classic follow-up question.
6. What is the result of attempting to invoke Thread.start() twice on the exact same thread object?
Answer: It throws IllegalThreadStateException at runtime.
A thread object has a strict lifecycle. It is created, started once, runs, and eventually terminates. It cannot be restarted.
The start() method is what transitions a thread from the NEW state into a runnable state. When you call start() a second time on the same object, the JVM checks the thread’s state and finds it is no longer in the NEW state — it has already been started. The second call is invalid, and the JVM responds with IllegalThreadStateException.
The exception is thrown at runtime, not compile time. The compiler cannot know how many times you’ll call start() on a given thread, so nothing is flagged until execution.
The practical lesson: a thread is a one-shot object. If you need to run the same work again, don’t restart the thread — create a new thread object (or better, use an executor and submit the task again).
7. What is the outcome of compiling and running this catch block hierarchy?
try {
throw new ArithmeticException();
} catch (RuntimeException e) {
System.out.print("RuntimeException ");
} catch (Exception e) {
System.out.print("Exception ");
}
Output: RuntimeException
When an exception is thrown, Java walks the catch blocks from top to bottom and executes the first block whose parameter type can hold the thrown exception.
ArithmeticException extends RuntimeException. So the first catch (RuntimeException e) matches immediately — it can hold an ArithmeticException because of inheritance. Control enters that block, prints RuntimeException , and the whole try-catch is done. The second catch (Exception e) is never considered.
Order matters, and that’s exactly what the next question exploits.
8. What happens if you swap the catch blocks so catch (Exception e) comes FIRST?
try {
throw new ArithmeticException();
} catch (Exception e) {
System.out.print("Exception ");
} catch (RuntimeException e) {
System.out.print("RuntimeException ");
}
Answer: Compilation error: the second catch block is unreachable.
When the broader catch (Exception e) appears first, it can hold every checked and unchecked exception, including ArithmeticException. Any exception thrown in the try block would be caught there. Java’s rules require that a catch block only be allowed if there is still some exception it could catch.
The subsequent catch (RuntimeException e) block could never be reached — everything it could catch is already handled by the first block. So the compiler flags it as an unreachable catch block and refuses to compile the code.
The rule to remember: catch blocks are matched top-to-bottom, so the most specific catch must always come first, and broader catches go after. catch (Exception) swallowing everything is why you list RuntimeException (and any other specific types) before it.
9. What does the transient keyword do when applied to a class field?
Answer: It excludes the field from standard Java serialization — the field’s value is skipped when the object is written out and restored as default when read back.
Serialization is the mechanism by which an object’s state is written to a byte stream (ObjectOutputStream) and reconstructed later (ObjectInputStream). By default, every non-static, non-transient field is included. The transient keyword opts a field out.
When is that useful? Some fields simply shouldn’t survive serialization:
- Derived or cached values — a field recomputed from others, like a checksum or a lazily-initialized cache. Saving it is wasted space and risks saving stale data.
- Sensitive data — passwords, tokens, keys. If the object might be serialized, marking these
transientkeeps them out of the stream. - Non-serializable resources — a field holding a
Socket, aThread, or a JDBC connection cannot be meaningfully written to bytes. Marking ittransientis practically required.
When the object is deserialized, a transient field is simply left at its default value (null for objects, 0 for primitives, false for booleans). Your code is responsible for reconstructing it if needed — often in a readObject method.
The interview answer: transient marks a field to be skipped during serialization; it’s restored to its default value after deserialization.
10. What will be printed by this code?
List<String> list = List.of("anna", "bob", "alex");
long count = list.stream()
.filter(s -> s.startsWith("a"))
.count();
System.out.println(count);
Output: 2
The stream pipeline has two stages: an intermediate filter and a terminal count.
filter(s -> s.startsWith("a")) keeps only the elements that start with 'a'. Walking the list: "anna" starts with a — kept. "bob" does not — dropped. "alex" starts with a — kept. That leaves two elements.
The terminal operation count() then returns the number of elements left in the stream, which is 2.
Note that count() returns a long, and that the intermediate filter doesn’t run until a terminal operation is invoked — streams are lazy. But for counting purposes, that laziness is invisible: the result is simply 2.
11. What is the key functional difference between map and flatMap in Streams?
Answer: map transforms each element into exactly one result — a 1:1 mapping. flatMap transforms each element into a stream and then flattens all those streams into a single stream.
Both are intermediate operations that take a function. The difference is in what that function returns.
map takes a function that returns one value per input. Stream.of("a", "bb").map(s -> s.length()) gives [1, 2] — one output per input, input size preserved. The output is a Stream of those results.
flatMap takes a function that returns a Stream for each element, and then concatenates all those streams. Consider List.of("ab", "cd").stream().flatMap(s -> s.chars().mapToObj(c -> (char) c)). Each input string expands into a stream of its characters, and flatMap flattens the result into one stream of four characters.
Where does this matter in practice? The classic case is a list of lists — say, a List<List<String>>. A map would leave you with a Stream<List<String>> (same nesting). A flatMap with list -> list.stream() collapses it into a Stream<String>, giving you one flat sequence.
The memory hook: map maps 1→1; flatMap flattens 1→N into a single stream.
12. What is the scope of a variable stored inside a ThreadLocal<T>?
Answer: A ThreadLocal value is visible only to the thread that set it. Each thread that touches the same ThreadLocal object reads and writes its own isolated copy.
This is a mechanism for thread confinement — keeping per-thread data separate without synchronization.
Imagine several threads working through the same code. They all reference the same ThreadLocal instance, but the value each one reads or writes is private to it. Thread A sets a value; thread B, moments later, still sees its own default (or its own previously-set value). There is no shared state, no race condition — each thread’s copy lives in its own map of thread-locals held by that thread.
The classic use cases are per-request context in web servers (current user, request ID), and sharing a non-thread-safe object like SimpleDateFormat or a JDBC connection per thread, so each thread works with its own instance rather than contending over one shared one.
The critical caveat that trips people up: ThreadLocal values are not global, and they are not preserved across anything other than the same thread. If the work moves to a different thread — a thread pool, an executor, a virtual thread — the value does not follow. In a pooled-thread environment, a ThreadLocal can even leak stale state between tasks because the thread is reused. That is why the advice is to always remove() the value when done.
The interview answer: a ThreadLocal holds an independent copy per thread; only that thread can see it.
13. What is the result of using Executors.newFixedThreadPool(10) regarding its task queue capacity?
Answer: It uses an unbounded LinkedBlockingQueue. If tasks arrive faster than the 10 threads can process them, the queue grows without limit and can exhaust memory (OutOfMemoryError).
The fixed thread pool is created with a fixed number of worker threads — here, 10. The mechanics of submission follow the standard ThreadPoolExecutor logic: if a worker is free, the task is handed to it; otherwise the task goes into the work queue.
The key detail is what work queue is used. newFixedThreadPool uses a LinkedBlockingQueue with no capacity bound. That means under sustained load — tasks submitted faster than 10 threads drain them — the queue simply grows, holding more and more pending tasks. Nothing rejects the surplus.
Over time that accumulation is dangerous. Each queued Runnable holds references (and thus memory). Under a long enough burst, the queue can consume all available heap, and the JVM dies with OutOfMemoryError.
That is the practical argument for preferring a bounded pool in production: newFixedThreadPool never rejects tasks, which sounds generous but can be fatal. A ThreadPoolExecutor with a bounded queue and an explicit rejection policy protects you — it rejects (or otherwise handles) overflow instead of silently piling up work.
The interview answer: fixed thread pools use an unbounded LinkedBlockingQueue; overflow means unbounded memory growth and possible OutOfMemoryError.
14. What is the result of compiling and running this Record class snippet (Java 14+)?
public record User(String name, int age) {}
Answer: The compiler automatically generates the final fields, a canonical constructor, accessor methods (name(), age()), and implementations of equals(), hashCode(), and toString().
A record is a transparent, immutable carrier for data. The line of code you write replaces what would otherwise be a small wall of boilerplate.
What you get for free:
- Final fields for each component (
name,age), set in the constructor. - A canonical constructor taking all components, performing the assignments.
- Accessor methods named after the components —
name()andage(). Notice the naming: nogetName(), nogetprefix at all. equals(),hashCode(), andtoString()derived from all components. Two records are equal when all their components are equal; the string form shows the components.
There is no such thing as a setter — records are immutable by design, so the fields are final and the state is set once at construction. And records are implicitly final, so you cannot extend one. A subclass of a record is a compile error.
The interview answer: a record is a compact, immutable data holder that auto-generates the fields, constructor, accessors, and the standard Object methods — with no mutators and no inheritance allowed.
15. What will be the output of this code?
public class StringPool {
public static void main(String[] args) {
String s1 = "Java";
String s2 = "Ja" + "va";
System.out.println(s1 == s2);
}
}
Output: true
This is the mirror image of the "He" + new String("llo") question — and it’s the contrast that makes both of them click.
The difference is that "Ja" + "va" is a constant expression. Both operands are string literals, and the compiler is allowed to evaluate the concatenation at compile time. It folds the two literals into a single literal: "Java".
Now s1 = "Java" and the folded s2 — which is also "Java" — both point to the same entry in the String Constant Pool. The runtime resolves the same literal to the same pool object. So s1 == s2 compares two references to one object and returns true.
The general rule, stated cleanly: == on strings is true when both sides are compile-time constants resolving to the same pooled literal. It breaks the moment anything is built at runtime — new String(...), concatenation involving a variable or method call. The reliable comparison for content is always .equals(). This pair of questions is the classic demonstration of exactly where that line falls.
Premium Content
Unlock Top 50 - Part 1 and all premium lessons with a subscription.
From ₹199.99/year — See plans