Java Interview Questions — Anti-Cheat Edition
Designed to test genuine understanding, not pattern matching. Each snippet produces a surprising or non-obvious output. Ask the candidate to explain why before running it.
Ground Rules for the Interviewer
- Ask the candidate to predict the output first, then explain why.
- Follow up with "What would you change to fix / alter the behaviour?"
- If they copy-paste to AI, the answer alone won't save them — the follow-up will expose gaps.
Difficulty Progression
- Basics: language fundamentals, equality, object lifecycle, collections
- Intermediate: generics, control flow edge cases, interface defaults, stream pitfalls
- Advanced: concurrency, memory model, API design contracts, Effective Java style trade-offs
Basics
1. String Immutability
public class Test1 {
public static void main(String[] args) {
String s = "Hello";
s.concat(" World");
System.out.println(s);
}
}
Output: Hello
Question: Why doesn't it print Hello World?
Key Points:
- String is immutable; concat() returns a new String — the original reference s is unchanged.
- Fix: s = s.concat(" World");
Follow-up: What is the String pool / interning? How does new String("Hello") differ from "Hello"?
Concepts: String immutability, String pool / interning, reference vs value
2. String == vs .equals()
public class Test2 {
public static void main(String[] args) {
String a = "Java";
String b = "Java";
String c = new String("Java");
System.out.println(a == b);
System.out.println(a == c);
System.out.println(a.equals(c));
}
}
Output:
Question: Why is a == b true but a == c false, even though all three contain "Java"?
Key Points:
- String literals are interned — a and b point to the same pool object.
- new String(...) always creates a new heap object, bypassing the pool.
- == compares references; .equals() compares content.
Follow-up: How do you force c into the pool? (c.intern())
Concepts: String pool, reference equality, intern()
3. Overloading vs Overriding (Surprising Dispatch)
class Parent {
void show(Number n) { System.out.println("Parent Number"); }
}
class Child extends Parent {
void show(Integer n) { System.out.println("Child Integer"); }
}
public class Test3 {
public static void main(String[] args) {
Parent p = new Child();
p.show(10);
}
}
Output: Parent Number
Question: The runtime object is a Child, so why does it print Parent Number instead of Child Integer?
Key Points:
- Method overloading is resolved at compile time based on the declared (static) type of the reference (Parent).
- Parent has no show(Integer) — only show(Number). So the compiler binds to show(Number).
- Dynamic dispatch (polymorphism) only applies to overridden methods (same signature).
Follow-up: What if Child instead overrode show(Number n)? What would print then?
Concepts: Static vs dynamic dispatch, overloading, overriding, compile-time binding
4. Autoboxing & Integer Cache
public class Test4 {
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
Integer c = 128;
Integer d = 128;
System.out.println(a == b);
System.out.println(c == d);
}
}
Output:
Question: Both pairs are autoboxed from int literals — why does == give different results?
Key Points:
- JVM caches Integer objects for values -128 to 127. a and b share the cached instance.
- Values outside this range create new objects, so c != d by reference.
- Safe comparison always uses .equals() or unboxing.
Follow-up: What range is cached? Is this guaranteed by the JVM spec?
Concepts: Autoboxing, Integer cache, reference vs value equality
5. Generics & Invariance (Why List<Integer> isn't a List<Number>)
import java.util.*;
public class Test5 {
public static void main(String[] args) {
Integer i = 10;
Number iNum = i; // ✅ works — Integer IS-A Number
List<Integer> ints = Arrays.asList(1, 2, 3);
// List<Number> nums = ints; // ❌ compile error
List<? extends Number> nums2 = ints; // ✅ works
System.out.println(nums2.get(0));
}
}
Output: 1
Question: Why does List<Integer> not assign to List<Number> even though Integer extends Number?
Key Points:
- Generics are invariant in Java. List<Integer> is NOT a subtype of List<Number>.
- If it were allowed, you could add a Double through a List<Number> reference, corrupting a List<Integer>.
- ? extends Number is a bounded wildcard — read-only (no add()).
Follow-up: What does ? super Integer allow? What is PECS (Producer Extends, Consumer Super)?
Concepts: Generics invariance, wildcards, PECS, type safety
6. static Initialiser & Constructor Order
public class Test6 {
static int x = initX();
static int initX() {
System.out.println("static init");
return 5;
}
Test6() {
System.out.println("constructor");
}
public static void main(String[] args) {
System.out.println("main start");
Test6 t1 = new Test6();
Test6 t2 = new Test6();
}
}
Output:
Question: Why does static init print before main start?
Key Points:
- Static initialisers run once when the class is first loaded by the JVM, before main() executes.
- Instance constructors run each time new is called.
Follow-up: What if Test6 had a parent class with a static block? What runs first?
Concepts: Class loading, static vs instance initialisation, JVM lifecycle
Intermediate
7. finally vs return
public class Test7 {
static int getValue() {
try {
return 1;
} finally {
return 2;
}
}
public static void main(String[] args) {
System.out.println(getValue());
}
}
Output: 2
Question: The try block explicitly returns 1. Why does the method return 2?
Key Points:
- finally always executes, even after a return in try.
- A return inside finally overrides the return in try.
- This is considered bad practice — it silently swallows the try return value.
Follow-up: What if finally threw an exception instead of returning? What happens to the original return value?
Concepts: try-finally, control flow, exception handling pitfalls
8. == on null and instanceof
public class Test8 {
public static void main(String[] args) {
String s = null;
System.out.println(s instanceof String);
System.out.println(s == null);
// s.equals("hello"); // what happens here?
}
}
Output:
Question: instanceof returns false for null — is that a bug or by design?
Key Points:
- null instanceof T is always false by spec — null has no type.
- This is useful: if (obj instanceof String) is null-safe without a separate null check.
- s.equals(...) would throw NullPointerException.
Follow-up: How does Java 16+ pattern matching instanceof (if (obj instanceof String str)) change usage?
Concepts: instanceof, null safety, pattern matching (Java 16+)
9. Interface default Method Diamond Problem
interface A {
default String hello() { return "A"; }
}
interface B extends A {
default String hello() { return "B"; }
}
class C implements A, B {
// what must C do?
}
public class Test9 {
public static void main(String[] args) {
System.out.println(new C().hello());
}
}
Question: Does this compile? If not, how do you fix it?
Key Points:
- This is a compile error — C inherits conflicting default methods from A and B.
- C must override hello() to resolve the ambiguity.
- Can delegate explicitly: return B.super.hello();
Follow-up: If C extends a class that also implements hello(), which wins — the class or the interface default?
Concepts: Default methods, diamond problem, interface vs class hierarchy
10. Varargs Ambiguity
public class Test10 {
static void print(int... nums) {
System.out.println("varargs int");
}
static void print(Integer... nums) {
System.out.println("varargs Integer");
}
public static void main(String[] args) {
print(1, 2, 3);
}
}
Question: Does this compile? If yes, which overload is called?
Key Points:
- This is a compile error — the compiler cannot choose between int... and Integer... when given int literals (autoboxing makes both equally valid).
- Varargs overloads with autoboxing candidates are ambiguous.
Follow-up: How would you resolve this? (Explicit cast: print((int[]) new int[]{1,2,3}) or rename the methods.)
Concepts: Varargs, overload resolution, autoboxing ambiguity
11. HashMap and null Keys
import java.util.*;
public class Test11 {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put(null, 1);
map.put(null, 2);
System.out.println(map.size());
System.out.println(map.get(null));
}
}
Output:
Question: Can a HashMap have null as a key? What happens when you insert it twice?
Key Points:
- HashMap allows one null key; the second put overwrites the first.
- Hashtable and ConcurrentHashMap do not allow null keys — throws NullPointerException.
Follow-up: How many null values can a HashMap have?
Concepts: HashMap internals, null handling, Hashtable vs ConcurrentHashMap
12. volatile vs synchronized
public class Test12 {
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!flag) { /* spin */ }
System.out.println("Thread saw flag = true");
});
t.start();
Thread.sleep(100);
flag = true;
}
}
Question: What does volatile guarantee here? Would this work without volatile?
Key Points:
- volatile ensures visibility — writes are immediately flushed to main memory; reads always fetch from main memory.
- Without volatile, the JIT may cache flag in a CPU register — the thread might spin forever.
- volatile does not guarantee atomicity (e.g., flag++ is still a race condition).
Follow-up: When would you need synchronized instead? What is the difference between volatile and AtomicBoolean?
Concepts: Java Memory Model, visibility, volatile, synchronized, atomicity
Side-effect Trap: parallelStream() + shared mutable state
List<String> words = getData(url);//Add more data
List<String> result = new ArrayList<>();//Shared Mutable Variable
words
.parallelStream() //Gives problem
//.stream
.map(String::toUpperCase)
.forEach(name -> result.add(name));//Shared Mutability is BAD
Why this is dangerous: mutating shared state from a parallel stream introduces data races and inconsistent results.
Safer approach: collect into a new immutable result (words.parallelStream().map(String::toUpperCase).toList()).
Quick Reference: Concepts Coverage
| # | Question Topic | Concepts |
|---|---|---|
| 1 | String concat |
Immutability, pool |
| 2 | String == vs equals |
Pool, interning, reference equality |
| 3 | Overloading dispatch | Static vs dynamic binding |
| 4 | Integer cache | Autoboxing, JVM cache range |
| 5 | Generics invariance | Wildcards, PECS |
| 6 | Static init order | Class loading, JVM lifecycle |
| 7 | finally return |
Control flow, exception pitfalls |
| 8 | null instanceof |
Null safety, pattern matching |
| 9 | Default method diamond | Interface hierarchy |
| 10 | Varargs ambiguity | Overload resolution, autoboxing |
| 11 | HashMap null key |
Map internals, null handling |
| 12 | volatile flag |
Java Memory Model, visibility |
Practice Pack (Basics to Intermediate)
Essential Java — Interview Snippet Pack (Study Notes)
Purpose: small, interview-style Java puzzles aligned to Essential Java / core Java topics: exceptions, collections, lambdas, generics, streams, threads, regex, and file I/O.
How to practice: For each snippet, do 3 steps: 1) Predict output/behavior 2) Explain why 3) State the best-practice fix / what an interviewer wants to hear
1) Exceptions: finally overrides return
public class Q1 {
static int f() {
try { return 1; }
finally { return 2; }
}
public static void main(String[] args) {
System.out.println(f());
}
}
✅ Expected output:
Key point:finally always runs; a return inside finally replaces any earlier return.
2) Try-with-resources: close order is reverse
class R implements AutoCloseable {
private final String name;
R(String name) { this.name = name; }
public void close() { System.out.print("close-" + name + " "); }
}
public class Q2 {
public static void main(String[] args) {
try (R a = new R("A"); R b = new R("B")) {
System.out.print("try ");
}
}
}
✅ Expected output:
Key point: Resources close in reverse order of creation.3) Streams: laziness + short-circuit (findFirst)
import java.util.*;
public class Q3 {
public static void main(String[] args) {
List<Integer> xs = Arrays.asList(1,2,3,4,5);
int v = xs.stream()
.filter(x -> { System.out.print("f" + x + " "); return x % 2 == 0; })
.map(x -> { System.out.print("m" + x + " "); return x * 10; })
.findFirst()
.orElse(-1);
System.out.println("=> " + v);
}
}
✅ Typical output:
Key point: Streams are lazy; terminal opfindFirst() short-circuits once it finds the first match.
4) Lambdas: “effectively final” capture
import java.util.*;
public class Q4 {
public static void main(String[] args) {
int base = 10; // effectively final
List<Integer> xs = List.of(1,2,3);
xs.forEach(x -> System.out.println(x + base));
// base++; // Uncomment -> compile error
}
}
base++ cause a compile error?
Key point: A lambda can only capture local variables that are final or effectively final.
5) Generics: invariance + wildcard (? extends Number)
import java.util.*;
public class Q5 {
static double sum(List<? extends Number> xs) {
double s = 0;
for (Number n : xs) s += n.doubleValue();
return s;
}
public static void main(String[] args) {
List<Integer> a = List.of(1,2,3);
System.out.println(sum(a));
// List<Number> b = a; // compile error (invariance)
}
}
List<Integer> not assign to List<Number>?
Key point: Generics are invariant. Use ? extends Number when you only need to read.
6) Maps: computeIfAbsent runs once per missing key
import java.util.*;
public class Q6 {
public static void main(String[] args) {
Map<String, Integer> m = new HashMap<>();
int a = m.computeIfAbsent("k", k -> {
System.out.print("compute ");
return 42;
});
int b = m.computeIfAbsent("k", k -> 99);
System.out.println(a + " " + b + " " + m.get("k"));
}
}
✅ Expected output:
Key point: Only computes when key is absent.7) HashMap: mutating a key after put() breaks retrieval
import java.util.*;
class Key {
int id;
Key(int id) { this.id = id; }
@Override public int hashCode() { return id; }
@Override public boolean equals(Object o) {
return (o instanceof Key k) && k.id == id;
}
}
public class Q7 {
public static void main(String[] args) {
Map<Key, String> map = new HashMap<>();
Key k = new Key(1);
map.put(k, "one");
k.id = 2; // mutation breaks hashing
System.out.println(map.get(k));
System.out.println(map.size());
}
}
✅ Typical output:
Key point: Hash-based collections assume keys have stablehashCode/equals while stored.
8) Threads: race condition (count++ is not atomic)
public class Q8 {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> { for (int i=0;i<100_000;i++) count++; });
Thread t2 = new Thread(() -> { for (int i=0;i<100_000;i++) count++; });
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(count);
}
}
200000? Fix?
Key point: count++ is a read-modify-write sequence; updates get lost.
Fix options: synchronized, AtomicInteger, LongAdder, locks.
9) Threads: run() vs start()
public class Q9 {
public static void main(String[] args) {
Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName()));
t.run(); // runs on main thread
t.start(); // runs on a new thread
}
}
✅ Typical output:
Key point:run() is a normal method call; start() launches a new thread.
10) File I/O: chars vs bytes (encoding matters)
import java.nio.charset.StandardCharsets;
public class Q10 {
public static void main(String[] args) {
String s = "€"; // one character
System.out.println(s.length());
System.out.println(s.getBytes(StandardCharsets.UTF_8).length);
}
}
✅ Typical output:
Key point:length() counts UTF-16 code units; UTF‑8 encoding uses variable bytes.
11) Regex: capture groups
import java.util.regex.*;
public class Q11 {
public static void main(String[] args) {
Matcher m = Pattern.compile("(\\w+)-(\\d+)").matcher("item-42");
if (m.matches()) {
System.out.println(m.group(1));
System.out.println(m.group(2));
}
}
}
group(0)?
✅ Expected output:
Key point:group(0) is the entire match; groups start at 1.
12) Optional: orElse vs orElseGet (eager vs lazy)
import java.util.*;
public class Q12 {
static String expensive() {
System.out.print("exp ");
return "X";
}
public static void main(String[] args) {
Optional<String> a = Optional.of("A");
System.out.println(a.orElse(expensive())); // eager
System.out.println(a.orElseGet(Q12::expensive)); // lazy
}
}
Key point: orElse() evaluates its argument even if Optional is present; orElseGet() only calls supplier when empty.
Extra practice prompts (no code)
- Explain the difference between checked and unchecked exceptions.
- Explain the contract between
equals()andhashCode(). - When would you prefer
ConcurrentHashMapoverCollections.synchronizedMap? - Difference between
ArrayListandLinkedListin real workloads.
Suggested daily routine
- Day 1–2: Exceptions + Collections
- Day 3–4: Streams + Lambdas + Optional
- Day 5: Threads + concurrency pitfalls
- Repeat with your own variations.
Advanced
Effective Java — Interview Snippet Pack (Study Notes)
Purpose: quick, interview-style code puzzles based on the principles popularized in Effective Java (3rd ed.).
Note: This is not a copy of the book. It’s an original practice pack.
How to use this pack
- For each snippet: predict the output, then explain why, then propose the Effective Java–style fix.
- Practice answering in 60–90 seconds per question.
1) Prefer static factories to constructors (naming + caching)
final class Score {
private static final Score ZERO = new Score(0);
private final int value;
private Score(int value) { this.value = value; }
public static Score of(int value) {
return value == 0 ? ZERO : new Score(value);
}
}
of() enable that a constructor can’t easily?
- Named creation, caching, returning subtypes, hiding implementation.
2) Builder when many params (avoid telescoping)
public final class User {
private final String id;
private final String email;
private final String phone;
private User(Builder b) {
this.id = b.id;
this.email = b.email;
this.phone = b.phone;
}
public static class Builder {
private final String id;
private String email;
private String phone;
public Builder(String id) { this.id = id; }
public Builder email(String email) { this.email = email; return this; }
public Builder phone(String phone) { this.phone = phone; return this; }
public User build() { return new User(this); }
}
}
// usage:
// User u = new User.Builder("u1").email("a@b.com").build();
3) Enums for singletons (safe serialization / reflection)
Ask: Why is this preferred overpublic static final Config INSTANCE = new Config()?
4) Don’t use finalize() / cleaners for critical resources
class Leaky {
@Override protected void finalize() throws Throwable {
System.out.println("finalize");
}
}
5) Prefer try-with-resources
import java.io.*;
public class Q5 {
public static void main(String[] args) throws Exception {
try (ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{1,2,3})) {
System.out.println(in.read());
}
}
}
close() throws and the body throws too? (suppressed exceptions)
6) equals() pitfalls: symmetry with inheritance
class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
}
class ColoredPoint extends Point {
final String color;
ColoredPoint(int x,int y,String c){ super(x,y); color=c; }
// naive equals:
@Override public boolean equals(Object o) {
if (!(o instanceof ColoredPoint cp)) return false;
return super.equals(cp) && color.equals(cp.color);
}
}
public class Q6 {
public static void main(String[] args) {
Point p = new Point(1,2);
Point cp = new ColoredPoint(1,2,"red");
System.out.println(p.equals(cp));
System.out.println(cp.equals(p));
}
}
true then false (symmetry violation).
Fix: favor composition, or make Point final, or use a well-defined equality strategy.
7) Always override hashCode when overriding equals
import java.util.*;
final class Money {
final int amount;
final String currency;
Money(int a, String c){ amount=a; currency=c; }
@Override public boolean equals(Object o){
return (o instanceof Money m)
&& amount == m.amount
&& Objects.equals(currency, m.currency);
}
// hashCode intentionally omitted
}
public class Q7 {
public static void main(String[] args) {
Set<Money> s = new HashSet<>();
s.add(new Money(5,"USD"));
System.out.println(s.contains(new Money(5,"USD")));
}
}
false? What’s the fix?
8) toString() as a debugging API
record OrderId(String value) {}
public class Q8 {
public static void main(String[] args) {
System.out.println(new OrderId("o-123"));
}
}
9) Comparable: be consistent with equals
import java.util.*;
final class Person implements Comparable<Person> {
final String name;
final int age;
Person(String n, int a){ name=n; age=a; }
@Override public int compareTo(Person o) {
return name.compareTo(o.name); // age ignored
}
@Override public boolean equals(Object o){
return (o instanceof Person p) && age==p.age && name.equals(p.name);
}
}
public class Q9 {
public static void main(String[] args) {
Set<Person> set = new TreeSet<>();
set.add(new Person("A", 20));
set.add(new Person("A", 30));
System.out.println(set.size());
}
}
1? What rule is being broken?
10) Minimize mutability
import java.util.*;
final class Bag {
private final List<String> items;
Bag(List<String> items) {
this.items = items; // BUG
}
List<String> items(){ return items; }
}
public class Q10 {
public static void main(String[] args) {
var src = new ArrayList<>(List.of("a"));
var b = new Bag(src);
src.add("b");
System.out.println(b.items());
}
}
Bag truly immutable?
Fix: defensive copy + unmodifiable view.
11) Favor composition over inheritance
import java.util.*;
class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
@Override public boolean add(E e){ addCount++; return super.add(e); }
@Override public boolean addAll(Collection<? extends E> c){
addCount += c.size();
return super.addAll(c);
}
int getAddCount(){ return addCount; }
}
public class Q11 {
public static void main(String[] args) {
var s = new InstrumentedHashSet<String>();
s.addAll(List.of("a","b","c"));
System.out.println(s.getAddCount());
}
}
6? How does composition fix it?
12) Generic varargs + heap pollution
import java.util.*;
public class Q12 {
static <T> List<T> flatten(List<T>... lists) {
List<T> out = new ArrayList<>();
for (var l : lists) out.addAll(l);
return out;
}
public static void main(String[] args) {
List<String> a = List.of("x");
List<String> b = List.of("y");
System.out.println(flatten(a,b));
}
}
@SafeVarargs appropriate?
13) Prefer interfaces to reflection
public class Q13 {
public static void main(String[] args) throws Exception {
Object x = Class.forName("java.lang.String").getConstructor(String.class)
.newInstance("hi");
System.out.println(((String)x).toUpperCase());
}
}
14) Streams: keep them side-effect free
import java.util.*;
public class Q14 {
public static void main(String[] args) {
List<Integer> xs = List.of(1,2,3,4);
int[] sum = {0};
xs.stream().map(x -> sum[0] += x).forEach(System.out::println);
System.out.println("sum=" + sum[0]);
}
}
int s = xs.stream().mapToInt(Integer::intValue).sum();
15) Exceptions only for exceptional conditions
public class Q15 {
static int indexOf(int[] a, int target) {
try {
for (int i = 0;; i++) if (a[i] == target) return i;
} catch (ArrayIndexOutOfBoundsException e) {
return -1;
}
}
}
16) Concurrency: prefer executors to raw threads
import java.util.concurrent.*;
public class Q16 {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> f = pool.submit(() -> 40 + 2);
System.out.println(f.get());
pool.shutdown();
}
}
17) Concurrency: publication + visibility
public class Q17 {
static boolean ready;
static int number;
public static void main(String[] args) {
new Thread(() -> {
while (!ready) { /* spin */ }
System.out.println(number);
}).start();
number = 42;
ready = true;
}
}
0? How to fix?
Fix: volatile, synchronized, or higher-level concurrency constructs.
18) Serialization: prefer alternatives
// Interview prompt (no code):
// Explain why Java native serialization is risky and what you use instead.
Mini-checklist (what interviewers listen for)
- You mention contracts (equals/hashCode/compareTo), immutability, encapsulation, composition, generics safety, and concurrency visibility.
- You propose fixes that improve readability, correctness, and maintainability.
Suggested practice routine
- Pick 5 snippets/day.
- Explain the bug, the principle, the fix, the tradeoff.
- Re-implement the fix from memory.
References (for further reading)
- Effective Java (Joshua Bloch), 3rd edition.
- Oracle JavaDocs:
Object.equals,Object.hashCode,AutoCloseable.