MediumThe Complete Java Interview Guide — 220+ Questions From Mid-Level to Staff Engineer Series Part 1 | by Basuki Nath | Medium
Sitemap

The Complete Java Interview Guide — 220+ Questions From Mid-Level to Staff Engineer Series Part 1

8 min readJun 17, 2026

--

How to Use This Guide

  • 🟢 Mid-Level (2–4 YOE) — You should know these cold
  • 🟡 Senior (5+ YOE) — Expected at senior interviews
  • 🔴 Staff (8+ YOE) — Deep internals, trade-offs, “explain the why”

Jump to your section:

[Hyperlink will be added in few days for better readability and access]

  1. Core Java Fundamentals
  2. OOP & Design Principles
  3. Generics & Type System
  4. Collections Framework
  5. Streams & Functional Programming
  6. Exception Handling
  7. Concurrency & Multithreading
  8. Modern Java (8 → 21)
  9. Memory Management & GC
  10. JVM Internals

1. Core Java Fundamentals

Q1: Why is String immutable in Java? 🟢

Three reasons:

  1. Security — Strings used in class loading, network connections, file paths. Mutation would be a security hole.
  2. Thread safety — Immutable objects are inherently thread-safe. No synchronization needed.
  3. String pool — JVM caches string literals. If strings were mutable, changing one would affect all references.
String s = "hello";
s.concat(" world"); // Returns NEW string, s is still "hello"
String s2 = "hello"; // Same reference as s (pooled)

Follow-up: What about StringBuilder vs StringBuffer? StringBuilder is not thread-safe (faster). StringBuffer is synchronized (slower). Use StringBuilder unless sharing across threads.

Q2: Explain equals() vs == 🟢

  • == compares references (memory addresses)
  • equals() compares logical equality (content)
String a = new String("hello");
String b = new String("hello");
a == b; // false — different objects
a.equals(b); // true — same content
// Gotcha with Integer cache:
Integer x = 127;
Integer y = 127;
x == y; // true - cached range [-128, 127]
Integer m = 128;
Integer n = 128;
m == n; // false - outside cache, different objects

Q3: What is the equals() and hashCode() contract? 🟢

Contract:

  • If a.equals(b) is true → a.hashCode() == b.hashCode() MUST be true
  • If hashCodes differ → equals() MUST be false
  • Same hashCode does NOT guarantee equals (collisions are allowed)

What breaks if violated:

// BAD: equals without hashCode
class Employee {
String id;
public boolean equals(Object o) { return id.equals(((Employee)o).id); }
// No hashCode override!
}
Set<Employee> set = new HashSet<>();
set.add(new Employee("123"));
set.contains(new Employee("123")); // false! Different hashCode → different bucket

Q4: What is final, finally, and finalize? 🟢

  • final — variable can't be reassigned, method can't be overridden, class can't be extended
  • finally — block that always executes after try/catch (cleanup)
  • finalize — deprecated method called by GC before object destruction (never use it)
final int x = 10;
// x = 20; // Compile error
final List<String> list = new ArrayList<>();
list.add("item"); // OK! final means the reference, not the contents
// list = new ArrayList<>(); // Compile error - can't reassign reference

Follow-up: Can finally NOT execute? Yes — System.exit(), infinite loop in try, thread killed, JVM crash.

Q5: How does static work — variables, methods, blocks, inner classes? 🟡

  • Static variable — shared across all instances (one copy per class)
  • Static method — belongs to class, not instance. Can’t access this.
  • Static block — executes once when class is loaded
  • Static inner class — doesn’t hold reference to outer class
class PayrollEngine {
private static int instanceCount = 0; // shared
private static final Logger log = getLogger(); // constant
static { // Runs once on class load
System.out.println("PayrollEngine loaded");
}
public static PayrollEngine create() { // Factory method
instanceCount++;
return new PayrollEngine();
}
// Static inner class - no implicit outer reference
static class Config {
String region;
}
}

Follow-up: Why prefer static inner class over non-static? Non-static holds hidden reference to outer instance → memory leak if inner outlives outer.

Q6: Explain Java’s pass-by-value mechanism 🟢

Java is ALWAYS pass-by-value. But for objects, the “value” is the reference (memory address).

void modify(List<String> list) {
list.add("added"); // Modifies the ORIGINAL list (same reference)
list = new ArrayList<>(); // Reassigns local copy of reference — no effect on caller
}
List<String> myList = new ArrayList<>();
modify(myList);
// myList contains "added" - the add worked
// But myList was NOT replaced - reassignment was local
  • Primitives: value is copied. Changes inside method don’t affect caller.
  • Objects: reference is copied. You can modify the object through it, but can’t make the caller point to a different object.

Q7: What is autoboxing/unboxing and its pitfalls? 🟢

Autoboxing: primitive → wrapper (int → Integer). Unboxing: wrapper → primitive.

Integer a = 5;           // autoboxing: Integer.valueOf(5)
int b = a; // unboxing: a.intValue()
// Pitfall 1: NullPointerException
Integer x = null;
int y = x; // NPE! Unboxing null
// Pitfall 2: Performance in loops
Long sum = 0L;
for (int i = 0; i < 1_000_000; i++) {
sum += i; // Creates a new Long object EVERY iteration
}
// Fix: use primitive long
// Pitfall 3: == vs equals
Integer m = 200, n = 200;
m == n; // false! Outside [-128, 127] cache
m.equals(n); // true

Q8: What is the initialization order in Java? 🟡

1. Static fields & static blocks (parent first, in source order)
2. Instance fields & instance initializer blocks (in source order)
3. Constructor

class Parent {
static { System.out.println("1. Parent static"); }
{ System.out.println("4. Parent instance"); }
Parent() { System.out.println("5. Parent constructor"); }
}
class Child extends Parent {
static { System.out.println("2. Child static"); }
{ System.out.println("6. Child instance"); }
Child() { System.out.println("7. Child constructor"); }
}
new Child();
// Output: 1, 2, 4, 5, 6, 7
// Static blocks only run ONCE (on class load)

Q9: Explain the transient keyword 🟡

Marks a field to be excluded from serialization:

class UserSession implements Serializable {
String username;
transient String password; // Not serialized — security
transient Socket connection; // Not serialized — non-serializable
}

Use when: sensitive data, derived/cached values, non-serializable fields, or fields that should be recomputed on deserialization.

Q10: What is the difference between Comparable and Comparator? 🟢

  • Comparable — natural ordering defined IN the class (compareTo)
  • Comparator — external ordering defined OUTSIDE the class (compare)
// Comparable: one natural order
class Employee implements Comparable<Employee> {
public int compareTo(Employee o) {
return this.name.compareTo(o.name); // Natural order = by name
}
}
// Comparator: multiple sort strategies
Comparator<Employee> bySalary = Comparator.comparing(Employee::salary);
Comparator<Employee> byDeptThenName = Comparator
.comparing(Employee::department)
.thenComparing(Employee::name);
employees.sort(bySalary.reversed()); // Sort by salary descending

Q11: What is type casting and when does ClassCastException occur? 🟢

// Upcasting (implicit, always safe):
Object obj = "hello"; // String → Object
// Downcasting (explicit, can fail):
String s = (String) obj; // OK - obj actually IS a String
Object num = Integer.valueOf(5);
String bad = (String) num; // ClassCastException at runtime!
// Safe downcasting with instanceof:
if (obj instanceof String str) { // Java 16+ pattern matching
System.out.println(str.length());
}

Q12: What are marker interfaces and why do they exist? 🟡

Interfaces with zero methods — they “mark” a class as having a capability:

  • Serializable — JVM can serialize this object
  • Cloneable — Object.clone() won't throw
  • RandomAccess — signals O(1) indexed access (ArrayList has it, LinkedList doesn't)
// Algorithms can check:
if (list instanceof RandomAccess) {
// Use index-based loop (fast for ArrayList)
} else {
// Use iterator (fast for LinkedList)
}

Modern alternative: annotations (@Entity, @Service) — more flexible, can carry metadata.

Q13: Explain this and super keywords 🟢

class Animal {
String name;
Animal(String name) { this.name = name; }
void speak() { System.out.println("..."); }
}
class Dog extends Animal {
Dog(String name) {
super(name); // Must be first line - calls parent constructor
}
void speak() {
super.speak(); // Call parent's version
System.out.println("Woof!");
}
Dog self() {
return this; // Reference to current instance
}
}

this() and super() must be the first statement in a constructor. You can't use both.

Q14: What is the difference between abstract class and interface? 🟢

AspectAbstract ClassInterfaceMultiple inheritanceNo (single extends)Yes (multiple implements)ConstructorsYesNoState (fields)Instance fieldsOnly static final constantsAccess modifiersAnypublic (default since Java 9: private)When to useIS-A + shared implementationCAN-DO capability

Get Basuki Nath’s stories in your inbox

Join Medium for free to get updates from this writer.

Since Java 8+ interfaces have default and static methods, blurring the line. But abstract classes still win when you need state or constructors.

Q15: What happens when you override equals() but not hashCode()? 🟡

HashMap/HashSet break. They use hashCode to find the bucket first, then equals to find the exact match.

class Key {
int id;
public boolean equals(Object o) { return id == ((Key)o).id; }
// hashCode NOT overridden — uses Object.hashCode() (memory address)
}
Map<Key, String> map = new HashMap<>();
map.put(new Key(1), "one");
map.get(new Key(1)); // null! Different hashCode → different bucket → never finds it
Rule: ALWAYS override both together. Use same fields for both.

Q16: What is the volatile keyword? 🟡

Guarantees visibility and ordering across threads:

private volatile boolean running = true;
// Thread 1:
running = false; // Immediately visible to all threads
// Thread 2:
while (running) { } // Sees updated value (without volatile, might loop forever)

Does NOT guarantee atomicity: volatile int count; count++ is still a race condition (read-modify-write). Use AtomicInteger for compound operations.

Q17: Explain immutability — how to create an immutable class 🟡

public final class Money {                    // final — can't extend
private final BigDecimal amount; // final — can't reassign
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
this.amount = amount; // Defensive copy if mutable
this.currency = currency;
}
public BigDecimal getAmount() {
return amount; // BigDecimal is immutable - safe
}
// No setters. Return new instances for operations:
public Money add(Money other) {
return new Money(amount.add(other.amount), currency);
}
}

Rules: final class, final fields, no setters, defensive copies of mutable fields in constructor and getters.

Since Java 16: just use record — immutable by default.

Q18: What are checked vs unchecked exceptions? 🟢

  • Checked (extends Exception): Compiler forces you to handle or declare them. Example: IOException, SQLException.
  • Unchecked (extends RuntimeException): No compile-time enforcement. Example: NullPointerException, IllegalArgumentException.
// Checked — must declare or catch
void readFile() throws IOException {
Files.readString(Path.of("x.txt"));
}
// Unchecked - no declaration needed
void validate(String input) {
if (input == null) throw new IllegalArgumentException("null input");
}

Follow-up: Which should you use for domain exceptions? Unchecked. Spring uses unchecked exclusively. Checked exceptions create coupling between layers.

Q19: What is try-with-resources? 🟢

Auto-closes resources that implement AutoCloseable:

// Before Java 7 — verbose and error-prone
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("file.txt"));
return br.readLine();
} finally {
if (br != null) br.close(); // What if close() also throws?
}

// Java 7+ - clean and safe
try (var br = new BufferedReader(new FileReader("file.txt"))) {
return br.readLine();
} // Automatically closed, even on exception

Multiple resources closed in reverse declaration order. If both try and close throw, the close exception is suppressed.

Q20: What is reflection and when should you use it? 🟡

Reflection lets you inspect/modify classes, methods, fields at runtime:

Class<?> clazz = Class.forName("com.example.Employee");
Object obj = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getMethod("getSalary");
Object result = method.invoke(obj);

Use when: frameworks (Spring DI, Jackson serialization, JPA), testing (access private fields), dynamic proxies.

Avoid when: performance-critical code (10–100x slower than direct calls), you can use interfaces/generics instead.

Modern alternative: MethodHandles (faster, type-safe) or code generation (annotation processors).

--

--

Basuki Nath
Basuki Nath

Written by Basuki Nath

Java backend engineer (6+ years) building scalable AWS systems. I write on Spring Boot microservices, system design, and production-ready engineering practices.