Top 65 Java Interview Questions and Answers (2026)

The 65 Java questions interviewers actually ask, with direct answers, runnable code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.

65 questions with answers

What Is Java?

Key Takeaways

  • Java is a class-based, object-oriented language that compiles to bytecode and runs on the Java Virtual Machine, so the same build runs on any platform with a JVM.
  • The JDK is what you develop with, the JRE is what runs programs, and the JVM is the engine inside the JRE that executes bytecode.
  • Interviews test the object model (references, equality, immutability), collections, concurrency, and the memory model, not memorized syntax.
  • This page is a question bank: work your tier first, read one tier up, and say answers out loud because delivery is evaluated too.

Java is a class-based, object-oriented programming language created by James Gosling at Sun Microsystems and first released in 1995, built on the promise of write once, run anywhere. Source compiles to platform-neutral bytecode, and the Java Virtual Machine (JVM) executes that bytecode on any operating system, which is why one build runs on Windows, macOS, Linux, and Android alike. It's statically typed, so the compiler catches type errors before the program runs, and it ships with a large standard library covering collections, concurrency, I/O, and networking. Three acronyms come up constantly: the JDK (Java Development Kit) is the full toolchain you compile with, the JRE (Java Runtime Environment) is what end users need to run programs, and the JVM lives inside the JRE and actually runs the bytecode. These answers assume a modern long-term-support release (Java 17 or 21), so records, sealed classes, switch expressions, and the var keyword are fair game. This page collects the 65 questions that come up most, each with a direct answer and runnable code. If you're still building fundamentals, Oracle's official Java Tutorials are the canonical path; increasingly the first Java round is a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.

65Questions with answers on this page
3Difficulty tiers: freshers, intermediate, experienced
40+Runnable code snippets you can practice from
45-60 minTypical length of a Java technical round

Watch: Java Full Course for Beginners

Video: Java Full Course for Beginners (Programming with Mosh, YouTube)

Test yourself and earn a certificate

10 quick questions. Score 70%+ to download your Java certificate.

Jump to quiz

All Questions on This Page

65 questions
Java Interview Questions for Freshers
  1. 1. What is Java and what makes it platform independent?
  2. 2. What is the difference between the JDK, the JRE, and the JVM?
  3. 3. What are primitive types and wrapper classes?
  4. 4. Why are Strings immutable in Java, and what does that imply?
  5. 5. What is the difference between == and .equals()?
  6. 6. What is the contract between equals() and hashCode()?
  7. 7. What are the four pillars of object-oriented programming in Java?
  8. 8. What is inheritance and does Java support multiple inheritance?
  9. 9. What is the difference between a class and an object?
  10. 10. What is a constructor and what are its rules?
  11. 11. What is the difference between method overloading and overriding?
  12. 12. What do the access modifiers private, default, protected, and public mean?
  13. 13. What does the static keyword mean?
  14. 14. What does the final keyword do in its three positions?
  15. 15. What do this and super refer to?
  16. 16. What is the difference between an array and an ArrayList?
  17. 17. What are the main interfaces in the Java Collections Framework?
  18. 18. When would you choose a List, a Set, or a Map?
  19. 19. How does exception handling work with try, catch, and finally?
  20. 20. What is the difference between checked and unchecked exceptions?
  21. 21. What is a NullPointerException and how do you avoid it?
  22. 22. What is the difference between String, StringBuilder, and StringBuffer?
  23. 23. What are autoboxing and unboxing?
  24. 24. Is Java pass-by-value or pass-by-reference?
  25. 25. Why is the main method public static void main(String[] args)?
Java Intermediate Interview Questions
  1. 26. How does a HashMap work internally?
  2. 27. What is the difference between HashMap, Hashtable, and ConcurrentHashMap?
  3. 28. What is the difference between ArrayList and LinkedList?
  4. 29. What is a fail-fast iterator and the ConcurrentModificationException?
  5. 30. What is the difference between Comparable and Comparator?
  6. 31. How does a HashSet work, and how is it related to HashMap?
  7. 32. What do Collectors like groupingBy and toMap do in the Stream API?
  8. 33. What are method references and how do they relate to lambdas?
  9. 34. What is the difference between an interface and an abstract class?
  10. 35. How does polymorphism work through dynamic dispatch?
  11. 36. What are generics and what is type erasure?
  12. 37. What are streams and lambda expressions?
  13. 38. What is a functional interface and which built-in ones matter?
  14. 39. What is Optional and how should you use it?
  15. 40. How do you make a class immutable in Java?
  16. 41. What are records and when do you use them?
  17. 42. What is try-with-resources?
  18. 43. What are enums and what can they do beyond constants?
  19. 44. What does the synchronized keyword do?
  20. 45. What does the volatile keyword guarantee, and what doesn't it?
  21. 46. How do you create and run a thread in Java?
  22. 47. What is an ExecutorService and why prefer it over raw threads?
Java Interview Questions for Experienced Developers
  1. 48. How is JVM memory organized (heap, stack, metaspace)?
  2. 49. How does garbage collection work in Java?
  3. 50. How does class loading work in the JVM?
  4. 51. What is the Java Memory Model and why does happens-before matter?
  5. 52. How does ConcurrentHashMap achieve thread safety without locking the whole map?
  6. 53. What is CompletableFuture and how does it improve on Future?
  7. 54. How do you implement a singleton correctly in Java?
  8. 55. Design question: how would you implement a producer-consumer queue?
  9. 56. What causes a deadlock and how do you prevent it?
  10. 57. What is ThreadLocal and where is it useful, and where does it bite?
  11. 58. What are sealed classes and what problem do they solve?
  12. 59. How do default methods work, and how does Java resolve the diamond problem?
  13. 60. Why is Cloneable considered broken, and how do you copy objects instead?
  14. 61. How does Java serialization work and what are its risks?
  15. 62. What is reflection and when is it justified?
  16. 63. What is the string pool and how does intern() work?
  17. 64. What actually happens when you run a Java program, and what does the JIT do?
  18. 65. A Java service is slow or leaking memory in production. Walk through your debugging approach.

Java Interview Questions for Freshers

Freshers25 questions

The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.

Q1. What is Java and what makes it platform independent?

Java is a class-based, object-oriented, statically typed language. Its calling card is write once, run anywhere: source compiles to bytecode, and any machine with a Java Virtual Machine runs that bytecode without recompiling.

Platform independence comes from that extra layer. The compiler targets the JVM, not a specific CPU or OS, so the JVM absorbs the platform differences. Add a large standard library and mature tooling, and you get a language that runs everywhere from servers to Android phones.

Key point: A one-line definition plus the bytecode-and-JVM mechanism beats a memorized feature list. Interviewers open with this to hear how you organize an answer.

Q2. What is the difference between the JDK, the JRE, and the JVM?

The JVM (Java Virtual Machine) is the engine that executes bytecode and manages memory. The JRE (Java Runtime Environment) bundles the JVM with the standard class libraries, which is everything needed to run a Java program. The JDK (Java Development Kit) is the full kit: the JRE plus the compiler (javac), debugger, and other development tools.

The one-line mental model: JDK contains the JRE, and the JRE contains the JVM. You develop with the JDK and end users only need a JRE (or a bundled runtime) to run your program.

JVMJREJDK
PurposeRuns bytecodeRuns programsBuilds programs
ContainsJust the engineJVM + librariesJRE + compiler and tools
Who needs itEveryone (inside JRE)End usersDevelopers

Key point: Say 'JDK contains JRE contains JVM' as one sentence. That nesting is exactly what the question checks.

Q3. What are primitive types and wrapper classes?

Java has eight primitives: byte, short, int, long, float, double, boolean, and char. They store raw values directly, not objects, so they're fast and memory-light. Each has a wrapper class (Integer, Long, Boolean, and so on) that boxes the value into an object.

You need wrappers because generics and collections work with objects, not primitives: a List holds Integer, never int. Autoboxing converts between them automatically, which is convenient but can surprise you with null unboxing errors and hidden object allocation in loops.

java
int primitive = 42;
Integer boxed = primitive;        // autoboxing
int back = boxed;                 // auto-unboxing

List<Integer> nums = new ArrayList<>();
nums.add(7);                      // int boxed to Integer

Integer maybe = null;
// int oops = maybe;              // NullPointerException on unboxing

Key point: The follow-up trap is 'what happens if you unbox a null Integer?'. the technical answer is a NullPointerException, which surprises people.

Q4. Why are Strings immutable in Java, and what does that imply?

A String's contents can't change after creation. Immutability makes strings safe to share without copying, safe to use as HashMap keys (the hash never changes), naturally thread-safe, and cacheable in the string pool so identical literals share one object.

The practical implication: building a big string with + in a loop is O(n squared) because each concatenation allocates a whole new String. Use StringBuilder to collect the pieces in one growable buffer and produce the result once.

java
StringBuilder sb = new StringBuilder();
for (String row : rows) {
    sb.append(render(row));
}
String html = sb.toString();   // one buffer, the idiomatic way

Building a big string: + in a loop vs StringBuilder

Operations to assemble a string of N pieces, N = 1000 (log scale). + in a loop copies the whole string each step.

+ in a loop
1,000,000 ops
StringBuilder
1,000 ops
  • + in a loop: O(n squared): each step allocates a new String of the whole text so far
  • StringBuilder: O(n): one growable buffer, one final toString()

Key point: Volunteering the StringBuilder fix answers the guaranteed follow-up before it's asked. That's a fast way to look experienced.

Watch a deeper explanation

Video: Java Strings are Immutable - Here's What That Actually Means (Coding with John, YouTube)

Q5. What is the difference between == and .equals()?

== compares references for objects: are these two names pointing at the same object in memory? .equals() compares logical equality as the class defines it. For value comparison, especially of strings, always use .equals().

For primitives, == compares actual values and is correct. The classic bug is comparing two strings with == and getting false even though the text matches, because they're different objects.

java
String a = new String("hi");
String b = new String("hi");
a == b;         // false: different objects
a.equals(b);    // true:  same contents

int x = 5, y = 5;
x == y;         // true:  primitives compare by value

Key point: The trap is 'why did "hi" == "hi" return true in my test?'. String literals are pooled, so they share one object; new String() breaks that.

Q6. What is the contract between equals() and hashCode()?

Two rules. If two objects are equal by equals(), they must return the same hashCode(). And an object's hashCode() must stay consistent while it lives in a hash-based collection. Objects that aren't equal may share a hash code, that's just a collision.

Break the contract and hash collections misbehave: a HashMap computes the hash to pick a bucket, so a wrong hash sends it to the wrong bucket and it never finds your key even though equals() would match. Always override both together, using the same fields in each.

java
@Override public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Money m)) return false;
    return amount == m.amount && currency.equals(m.currency);
}
@Override public int hashCode() {
    return Objects.hash(amount, currency);   // same fields as equals
}

Key point: Say 'override them together, using the same fields'. Overriding equals but not hashCode is the single most common Java bug interviewers screen for.

Watch a deeper explanation

Video: Map and HashMap in Java - Full Tutorial (Coding with John, YouTube)

Q7. What are the four pillars of object-oriented programming in Java?

Encapsulation: bundle data with the methods that operate on it and hide internals behind access modifiers. Inheritance: a subclass reuses and extends a parent with extends. Polymorphism: one reference type, many runtime behaviors, mainly through method overriding. Abstraction: expose what an object does through interfaces and abstract classes while hiding how.

The point the question needs is why these matter: they reduce coupling and let code depend on stable contracts rather than concrete implementations, which is what makes large Java systems maintainable.

Key point: The four, then give one concrete example (a private field with a getter for encapsulation). Definitions alone read as memorized.

Q8. What is inheritance and does Java support multiple inheritance?

Inheritance lets a subclass reuse and extend a parent class with the extends keyword: the child gets the parent's non-private fields and methods and can add or override behavior. It models an is-a relationship, like a Dog is an Animal.

Java allows single inheritance of classes: a class extends exactly one parent, which avoids the ambiguity of the diamond problem. It does support multiple inheritance of type through interfaces, so a class can implement many interfaces while extending one class.

java
class Animal { void eat() { System.out.println("eating"); } }
class Dog extends Animal {
    void bark() { System.out.println("woof"); }
}
new Dog().eat();   // inherited from Animal

Key point: The follow-up is always 'why no multiple class inheritance?'. Say 'to avoid the diamond ambiguity; interfaces give multiple inheritance of type instead'.

Q9. What is the difference between a class and an object?

A class is a blueprint: it defines fields and methods but allocates nothing on its own. An object is a concrete instance of that class created with new, living on the heap with its own copy of the instance fields.

One class, many objects. The class Car defines that every car has a color and a speed; each new Car() is a separate object with its own values.

java
class Car {
    String color;
    Car(String color) { this.color = color; }
}

Car red = new Car("red");    // one object
Car blue = new Car("blue");  // another object, same class

Q10. What is a constructor and what are its rules?

A constructor initializes a new object. It has the same name as the class, no return type, and runs when you call new. If you write none, the compiler provides a no-argument default; the moment you write any constructor, that default disappears.

Constructors can be overloaded (several with different parameters), one can call another with this(...), and a subclass constructor implicitly calls super() first. This is why adding a parameterized constructor sometimes breaks code that relied on the default.

java
class Account {
    private double balance;
    Account() { this(0); }              // delegates
    Account(double balance) { this.balance = balance; }
}

Key point: The follow-up is 'what happens to the default constructor when you add your own?'. It's gone, and that's a real source of compile errors.

Q11. What is the difference between method overloading and overriding?

Overloading is compile-time: same method name, different parameter lists, in the same class. The compiler picks the right one by the arguments. Overriding is runtime: a subclass replaces an inherited method with the same signature, and the JVM dispatches to the actual object's version.

Overloading is a convenience for related operations; overriding is the mechanism behind polymorphism. Mark overrides with @Override so the compiler catches signature typos.

java
class Printer {
    void print(int n) {}
    void print(String s) {}       // overloading
}
class Loud extends Printer {
    @Override void print(String s) {}  // overriding
}

Key point: Say 'overloading is compile-time, overriding is runtime'. That one distinction is the whole answer the question needs.

Q12. What do the access modifiers private, default, protected, and public mean?

private: visible only inside the same class. default (no keyword): visible within the same package. protected: same package plus subclasses anywhere. public: visible everywhere.

The everyday guidance is to default to the most restrictive modifier that works, usually private fields with public methods, so a class controls its own state and you can change internals without breaking callers.

ModifierSame classSame packageSubclass (other pkg)Everywhere
privateYesNoNoNo
defaultYesYesNoNo
protectedYesYesYesNo
publicYesYesYesYes

Key point: People forget default (package-private) exists. Naming all four, including the no-keyword one, signals you actually know the model.

Q13. What does the static keyword mean?

static binds a member to the class itself rather than to any instance. A static field is shared by every object of the class; a static method belongs to the class and can't touch instance fields or this. You call them on the class name, like Math.max(...).

Use static for values and helpers that don't depend on a specific object: constants, factory methods, utility functions. Overuse creates shared mutable state, which is a common concurrency bug.

java
class Counter {
    static int total = 0;      // shared across all instances
    Counter() { total++; }
}
new Counter(); new Counter();
Counter.total;                 // 2

Q14. What does the final keyword do in its three positions?

On a variable, final means assign once: a constant reference or value that can't be reassigned. On a method, final prevents a subclass from overriding it. On a class, final prevents subclassing entirely, like String and Integer.

A subtle point for the variable case: final stops reassignment of the reference, not mutation of the object it points to. A final List can't be pointed at a new list, but you can still add to it.

java
final int MAX = 100;
// MAX = 200;                 // compile error

final List<String> names = new ArrayList<>();
names.add("Asha");            // fine: mutating, not reassigning
// names = new ArrayList<>(); // compile error

Key point: The distinction between reassignment and mutation on a final reference is the follow-up. Have the final List example ready.

Q15. What do this and super refer to?

this is a reference to the current object: it disambiguates a field from a parameter of the same name, passes the current object to another method, or calls another constructor with this(...). super refers to the parent class: super.method() calls the overridden parent version, and super(...) calls a parent constructor.

Both this(...) and super(...) as constructor calls must be the first statement in a constructor, which is why you can't use both in the same constructor.

Q16. What is the difference between an array and an ArrayList?

An array has a fixed length set at creation and can hold primitives or objects. An ArrayList is a resizable list backed by an array that grows automatically, holds only objects (primitives get boxed), and offers methods like add, remove, and contains.

Reach for an array when the size is fixed and you want raw speed or primitives without boxing. Reach for ArrayList when the collection changes size or you want the convenience methods, which is most of the time.

java
int[] fixed = new int[3];        // length locked at 3
fixed[0] = 10;

List<Integer> flexible = new ArrayList<>();
flexible.add(10);                 // grows on demand
flexible.add(20);

Q17. What are the main interfaces in the Java Collections Framework?

Three top-level interfaces cover most needs. List is an ordered sequence that allows duplicates (ArrayList, LinkedList). Set holds unique elements (HashSet, TreeSet, LinkedHashSet). Map stores key-value pairs (HashMap, TreeMap, LinkedHashMap). Queue and Deque handle FIFO and double-ended access (ArrayDeque, LinkedList).

Choosing well is the real skill: List when order and duplicates matter, Set to enforce uniqueness, Map for lookups by key. Naming the interface and a concrete implementation for each is the answer the question needs.

Key point: Pair each interface with one implementation (List to ArrayList, Set to HashSet, Map to HashMap). That shows you use them, not just recite them.

Q18. When would you choose a List, a Set, or a Map?

Use a List when order matters and duplicates are allowed, like a sequence of events. Use a Set when you need uniqueness and membership tests, like the distinct tags on a post. Use a Map when you look things up by a key, like user ID to profile.

The reflex worth building: whenever a problem says unique or distinct, think Set; whenever it says by name or by id, think Map. Picking the right structure often solves half the problem for free.

Key point: Interviewers slip data-structure choice into coding problems on purpose. Saying 'a Set gives me O(1) uniqueness here' out loud earns points.

Q19. How does exception handling work with try, catch, and finally?

try wraps code that might fail. catch handles a specific exception type if one is thrown. finally always runs, thrown or not, for cleanup like closing resources. You can chain multiple catch blocks from most specific to most general.

Catch specific types, not bare Exception, so you don't swallow bugs you didn't mean to handle. For resource cleanup, try-with-resources is cleaner than finally.

java
try {
    int value = Integer.parseInt(input);
} catch (NumberFormatException e) {
    System.out.println("bad number");
} finally {
    System.out.println("always runs");
}

Q20. What is the difference between checked and unchecked exceptions?

Checked exceptions extend Exception (but not RuntimeException) and represent recoverable conditions the compiler forces you to handle: either catch them or declare them with throws. IOException and SQLException are examples. Unchecked exceptions extend RuntimeException, usually signal programming bugs like NullPointerException or IllegalArgumentException, and the compiler doesn't require handling.

The design intent: checked for conditions a caller can reasonably recover from, unchecked for mistakes that should be fixed in code rather than caught. Errors (like OutOfMemoryError) are a third category you generally don't catch at all.

CheckedUnchecked
ExtendsExceptionRuntimeException
Compiler enforcedYes (catch or declare)No
Typical causeExternal failure (I/O, DB)Programming bug (null, bad arg)
ExampleIOExceptionNullPointerException

Key point: The clean summary is 'checked = recoverable and compiler-enforced, unchecked = programming bugs'. Say that first, then give an example of each.

Watch a deeper explanation

Video: Checked vs. Unchecked Exceptions in Java Tutorial - What's The Difference? (Coding with John, YouTube)

Q21. What is a NullPointerException and how do you avoid it?

A NullPointerException is thrown when you call a method or access a field on a reference that's null. It's the most common runtime error in Java because any object reference can be null.

Avoid it by initializing fields, checking for null at boundaries, returning empty collections instead of null, using Optional to express may-be-absent, and preferring constants on the left of equals ("yes".equals(value)) so a null value can't trigger it. Modern JVMs also print helpful NPE messages naming exactly which reference was null.

Key point: helpful NullPointerException messages (JEP 358, on by default since Java 15) matters.

Q22. What is the difference between String, StringBuilder, and StringBuffer?

String is immutable: every modification creates a new object. StringBuilder is a mutable, growable character buffer meant for building strings efficiently, but it's not thread-safe. StringBuffer is the older thread-safe version of StringBuilder, with synchronized methods and the overhead that brings.

The practical rule: use String for fixed text, StringBuilder for building strings in a single thread (the common case), and StringBuffer only when multiple threads share one buffer, which is rare.

StringStringBuilderStringBuffer
MutableNoYesYes
Thread-safeYes (immutable)NoYes (synchronized)
Best forFixed textSingle-thread buildingShared building (rare)

Key point: Almost always the right answer is StringBuilder. Knowing StringBuffer exists but is rarely needed is the depth signal.

Q23. What are autoboxing and unboxing?

Autoboxing is the automatic conversion of a primitive to its wrapper object (int to Integer); unboxing is the reverse. The compiler inserts these conversions so you can put an int into a List<Integer> or compare an Integer with an int without writing the conversion yourself.

The gotchas to name: unboxing a null wrapper throws a NullPointerException, and == on two Integer objects compares references, which quietly works for small cached values (-128 to 127) but fails for larger ones. Use .equals() or unbox deliberately.

java
Integer a = 127, b = 127;
a == b;            // true:  cached
Integer c = 200, d = 200;
c == d;            // false: different objects
c.equals(d);       // true:  compare values

Key point: The Integer cache (-128 to 127) is a favorite trick question. Explaining why 127 == 127 but 200 != 200 shows real understanding.

Q24. Is Java pass-by-value or pass-by-reference?

Java is always pass-by-value. For primitives, the value copied is the number itself. For objects, the value copied is the reference, so both the caller and the method point at the same object, which is why mutating that object inside a method is visible outside.

The distinction that trips people up: reassigning the parameter to a new object inside the method doesn't affect the caller, because only the copied reference was changed. Mutation is shared; reassignment is not.

java
void mutate(List<Integer> list) { list.add(99); }   // caller sees this
void rebind(List<Integer> list) { list = new ArrayList<>(); }  // caller does not

List<Integer> items = new ArrayList<>(List.of(1));
mutate(items);   // items == [1, 99]
rebind(items);   // items unchanged

Key point: Interviewers love the 'pass-by-reference?' trap. The correct answer is 'always pass-by-value, but the value for objects is a reference'. Say the whole sentence.

Q25. Why is the main method public static void main(String[] args)?

public so the JVM can call it from outside the class. static so it runs without creating an instance first, since there's no object yet at startup. void because it returns nothing to the JVM. String[] args to receive command-line arguments.

Each modifier is there for a reason, and interviewers ask this to see whether you understand them or just memorized the incantation. Since Java 21 there's also a preview simplified main for beginners, worth a sentence if it comes up.

java
public class App {
    public static void main(String[] args) {
        System.out.println("running: " + args.length + " args");
    }
}
Back to question list

Java Intermediate Interview Questions

Intermediate22 questions

For candidates with working experience: language mechanics, collections judgment, and the questions that separate users from understanders.

Q26. How does a HashMap work internally?

A HashMap stores entries in an array of buckets. To put or get a key, it computes the key's hashCode(), spreads the bits, and maps the result to a bucket index. Multiple keys can land in the same bucket, a collision, so each bucket holds a short linked list of entries that equals() then distinguishes.

Two performance details: when a bucket grows past a threshold (8 entries in modern JDKs) it converts to a balanced tree for O(log n) instead of O(n) lookups, and when the map's load factor is exceeded it resizes by doubling capacity and rehashing everything. Average operations are O(1); a broken hashCode() degrades that toward O(n).

java
Map<String, Integer> counts = new HashMap<>();
counts.merge("apple", 1, Integer::sum);   // insert or increment
counts.getOrDefault("pear", 0);            // 0, no exception

How HashMap resolves a key

1Hash the key
call key.hashCode(), then spread the bits to reduce collisions
2Pick the bucket
map the hash to an index in the backing array (capacity - 1 bitmask)
3Scan the bucket
walk the entries, comparing with equals() to find the exact key
4Return or insert
return the matched value, or add a new entry (treeify the bucket past 8 entries)

This is why both hashCode() and equals() matter: hashCode() finds the bucket, equals() finds the entry.

Key point: Say 'hashCode picks the bucket, equals finds the entry'. The treeify-at-8 and resize-on-load-factor details are the senior-sounding extras.

Q27. What is the difference between HashMap, Hashtable, and ConcurrentHashMap?

HashMap is unsynchronized, allows one null key and null values, and is the default single-threaded choice. Hashtable is a legacy fully synchronized map that locks the whole table on every operation and allows no nulls; it's effectively obsolete. ConcurrentHashMap is the modern concurrent map: thread-safe with fine-grained locking (striping), high read concurrency, and no nulls.

For shared mutable maps, reach for ConcurrentHashMap, never Hashtable. The whole-table lock in Hashtable makes it a bottleneck, which is exactly why ConcurrentHashMap replaced it.

HashMapHashtableConcurrentHashMap
Thread-safeNoYes (whole-table lock)Yes (fine-grained)
Null keys/valuesOne null key, null valuesNoneNone
Use todaySingle threadAvoid (legacy)Shared across threads

Key point: If asked for a thread-safe map, say ConcurrentHashMap and explain why Hashtable's single lock is the reason it was replaced.

Q28. What is the difference between ArrayList and LinkedList?

ArrayList is backed by a resizable array: random access by index is O(1), but inserting or removing in the middle shifts elements, which is O(n). LinkedList is a doubly linked chain of nodes: inserting or removing at a known position is O(1), but reaching an index means walking the list, which is O(n).

In practice ArrayList wins almost always because of cache locality and because most code reads by index far more than it inserts in the middle. LinkedList earns its place mainly as a queue or deque with frequent adds and removes at the ends.

ArrayList vs LinkedList: random access by index at N = 10000

Elements touched for get(i) at a middle index (log scale). ArrayList indexes directly; LinkedList walks the chain.

ArrayList get(i)
1 elements touched
LinkedList get(i)
5,000 elements touched
  • ArrayList get(i): O(1): direct array index
  • LinkedList get(i): O(n): walk from the nearest end to the middle

Key point: The honest production-ready answer is 'default to ArrayList'. LinkedList looks good on paper but loses on real hardware because of cache misses.

Q29. What is a fail-fast iterator and the ConcurrentModificationException?

Most collection iterators are fail-fast: they track a modification count, and if the collection changes structurally during iteration through anything other than the iterator itself, they throw a ConcurrentModificationException instead of risking corrupted results.

To remove while iterating, use the iterator's own remove() method, or removeIf, or iterate over a copy. On concurrent collections, iterators are weakly consistent and don't throw. The exception name is misleading: it fires even in a single thread when you modify a list inside a for-each loop.

java
List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4));
nums.removeIf(n -> n % 2 == 0);   // safe

Iterator<Integer> it = nums.iterator();
while (it.hasNext()) {
    if (it.next() == 1) it.remove();   // safe, via the iterator
}

Key point: The gotcha to name: the exception fires in single-threaded code too, whenever you mutate a list inside a for-each. removeIf is the clean fix.

Q30. What is the difference between Comparable and Comparator?

Comparable defines a type's one natural ordering through compareTo, implemented on the class itself; Collections.sort uses it by default. Comparator is a separate object defining an alternative ordering, which lets you sort the same type many different ways without touching the class.

Use Comparable for the obvious default order (numbers ascending, strings alphabetical) and Comparator when you need multiple orderings or can't modify the class. Modern Comparator chaining (comparing, thenComparing, reversed) makes multi-key sorts readable.

java
record Person(String name, int age) {}
List<Person> people = new ArrayList<>(/* ... */);

people.sort(Comparator
    .comparingInt(Person::age)
    .thenComparing(Person::name));   // by age, then name

Key point: Show the Comparator.comparing(...).thenComparing(...) chain. That fluent multi-key sort is what modern interviewers hope to see.

Q31. How does a HashSet work, and how is it related to HashMap?

A HashSet is a collection of unique elements with O(1) average add, remove, and contains. Internally it's backed by a HashMap: each element becomes a key, mapped to a shared dummy value, so the set inherits the map's hashing and collision handling.

That's why elements you put in a HashSet must have correct equals and hashCode, exactly like map keys: uniqueness is decided by those methods. Use LinkedHashSet when you also need predictable iteration order, and TreeSet when you need sorted order.

java
Set<String> tags = new HashSet<>();
tags.add("java");
tags.add("java");     // ignored: duplicate
tags.size();          // 1
tags.contains("java"); // true, O(1) average

Key point: Saying 'a HashSet is a HashMap under the hood' impresses, and it explains why set elements need the same equals/hashCode discipline as map keys.

Q32. What do Collectors like groupingBy and toMap do in the Stream API?

Collectors are the terminal recipes that turn a stream into a result. Collectors.toList and toSet gather elements; toMap builds a map from key and value functions; groupingBy partitions elements into a map of lists (or a downstream collector) by a classifier; joining concatenates strings; counting and summingInt aggregate.

groupingBy is the workhorse for the common group-and-aggregate task, replacing a manual map-of-lists loop with one expression. Watch for duplicate keys in toMap, which throws unless you supply a merge function.

java
Map<String, List<Employee>> byDept = employees.stream()
    .collect(Collectors.groupingBy(Employee::department));

Map<String, Long> counts = employees.stream()
    .collect(Collectors.groupingBy(Employee::department, Collectors.counting()));

Key point: The toMap duplicate-key trap (it throws without a merge function) is a favorite follow-up. Volunteer the merge-function fix.

Q33. What are method references and how do they relate to lambdas?

A method reference is a compact form of a lambda that just calls an existing method: String::length instead of s -> s.length(). There are four kinds: static (Integer::parseInt), bound instance (System.out::println), unbound instance (String::toUpperCase), and constructor (ArrayList::new).

They're syntactic sugar over lambdas, so they target the same functional interfaces, but they read better when the lambda would only forward its arguments to one method. When any logic beyond a single call is involved, a lambda is clearer.

java
List<String> names = List.of("ben", "asha");
names.stream()
    .map(String::toUpperCase)   // unbound instance ref
    .forEach(System.out::println); // bound instance ref

Key point: Being able to The four kinds of method reference, especially unbound instance (String::length), is the detail that indicates fluent rather than surface-level.

Q34. What is the difference between an interface and an abstract class?

An interface declares a capability: a set of methods a type promises to provide. A class can implement many interfaces, so interfaces give Java its multiple inheritance of type. An abstract class is a partial class: it can hold state (fields), constructors, and concrete methods, but a class can extend only one.

Choose an interface to define a contract that unrelated types can fulfill; choose an abstract class to share common state and implementation among closely related subclasses. Since Java 8 interfaces can carry default and static methods, which narrows the gap but doesn't erase it: interfaces still can't hold instance state.

InterfaceAbstract class
Multiple inheritanceYes (implement many)No (extend one)
Instance fields / stateNoYes
ConstructorsNoYes
Use forA capability / contractShared base for related types

Key point: The modern follow-up is 'if interfaces have default methods now, why keep abstract classes?'. The answer: interfaces still can't hold instance state.

Watch a deeper explanation

Video: Abstract Classes and Methods in Java Explained in 7 Minutes (Coding with John, YouTube)

Q35. How does polymorphism work through dynamic dispatch?

When you call an overridden method through a parent-type reference, the JVM decides at runtime which implementation to run based on the object's actual class, not the reference type. This is dynamic (runtime) dispatch, and it's what lets one line of code drive many behaviors.

It only applies to overridable instance methods. Static methods, private methods, and fields are resolved by the reference type at compile time, which is why hiding a static method or shadowing a field behaves differently from overriding.

java
abstract class Shape { abstract double area(); }
class Circle extends Shape { double area() { return 3.14; } }
class Square extends Shape { double area() { return 4.0; } }

Shape s = new Circle();
s.area();   // 3.14: dispatched on the actual Circle

Q36. What are generics and what is type erasure?

Generics let you write classes and methods parameterized by type, like List<String>, so the compiler enforces that only strings go in and casts come out automatically. They move whole categories of ClassCastException from runtime to compile time.

Type erasure is how they're implemented: the type parameters exist only at compile time and are erased to their bounds (usually Object) in the bytecode. That's why you can't write new T[], can't check obj instanceof List<String>, and why List<String> and List<Integer> share one runtime class. Wildcards (? extends, ? super) handle variance.

java
List<String> names = new ArrayList<>();
names.add("Asha");
String first = names.get(0);   // no cast needed

// erasure: both are List.class at runtime
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
a.getClass() == b.getClass();  // true

Key point: Type erasure is the common follow-up. Being able to say 'you can't do new T[] because the type is gone at runtime' is the depth check.

Watch a deeper explanation

Video: Generics In Java - Full Simple Tutorial (Coding with John, YouTube)

Q37. What are streams and lambda expressions?

A lambda is a concise anonymous function, (x) -> x * 2, that implements a functional interface (one abstract method). The Stream API uses lambdas to process collections declaratively: a source, a chain of intermediate operations like filter and map, and a terminal operation like collect or reduce that produces a result.

Streams read like the intent (keep the active users, take their emails, join them) rather than the mechanics of a loop. They're lazy: nothing runs until the terminal operation, and a stream is single-use, so you can't iterate one twice.

java
List<String> emails = users.stream()
    .filter(User::isActive)
    .map(User::email)
    .sorted()
    .toList();

Key point: The trap is 'can you reuse a stream?'. No, it's consumed by its terminal operation. laziness and single-use matters.

Q38. What is a functional interface and which built-in ones matter?

A functional interface has exactly one abstract method, which makes it the target type for a lambda or method reference. The @FunctionalInterface annotation documents that intent and makes the compiler enforce the single-method rule.

The java.util.function set covers most needs: Function<T,R> transforms, Predicate<T> tests, Consumer<T> takes and returns nothing, Supplier<T> produces, and their bi- and primitive variants. Recognizing which one a method wants is half of reading stream and callback code fluently.

java
Predicate<String> isLong = s -> s.length() > 8;
Function<String, Integer> len = String::length;
Supplier<UUID> id = UUID::randomUUID;

isLong.test("interview");   // true

Q39. What is Optional and how should you use it?

Optional<T> is a container that either holds a value or is empty, meant to make may-be-absent explicit in a method's return type instead of returning null. Callers then handle both cases with map, filter, orElse, or ifPresent rather than risking a NullPointerException.

The discipline that matters: use it as a return type for lookups that can miss, and avoid it for fields, method parameters, or collection elements. Don't call get() without checking, that just recreates the null problem with extra steps.

java
Optional<User> found = repo.findByEmail(email);
String name = found
    .map(User::name)
    .orElse("guest");   // no null check, no NPE

Key point: The anti-pattern to call out in practice is calling .get() without isPresent(). Saying 'prefer orElse or map over get' indicates mature.

Q40. How do you make a class immutable in Java?

Make the class final so it can't be subclassed, make all fields private and final, set them only through the constructor, and provide no setters. For any mutable field (a List, a Date), store a defensive copy in the constructor and return a copy or an unmodifiable view from getters, so callers can't reach in and mutate your state.

Immutability buys thread safety for free, safe use as map keys, and simpler reasoning. In modern Java, a record gives you a shallowly immutable data carrier with far less boilerplate.

java
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; }
}
// or simply:  record Point(int x, int y) {}

Key point: the defensive-copy step for mutable fields matters. That's the part people forget.

Q41. What are records and when do you use them?

A record is a concise way to declare a class that's just data. record Point(int x, int y) {} generates the constructor, private final fields, accessors, equals, hashCode, and toString for you. Records are implicitly final and shallowly immutable.

Use them for DTOs, value objects, keys, and return types where the whole point is carrying data. You can add methods and validate in a compact constructor, but if you need mutable state or inheritance, records aren't the tool.

java
record Money(long amount, String currency) {
    Money {                        // compact constructor validates
        if (amount < 0) throw new IllegalArgumentException("negative");
    }
}
new Money(100, "USD").equals(new Money(100, "USD"));  // true, generated

Key point: Records are a favorite modern-Java question. Knowing they auto-generate equals/hashCode and are shallowly immutable is the expected depth.

Q42. What is try-with-resources?

try-with-resources declares one or more resources in the parentheses of a try, and the JVM closes them automatically at the end of the block, in reverse order, even if an exception is thrown. Any resource that implements AutoCloseable qualifies: files, streams, database connections.

It replaces the error-prone finally block where people forgot to close, closed in the wrong order, or let a close() exception mask the real one. It also handles suppressed exceptions correctly.

java
try (var reader = Files.newBufferedReader(path)) {
    return reader.readLine();
}   // reader.close() runs automatically, even on exception

Key point: If you still write a finally block just to close a stream, that's a red flag. try-with-resources is the expected modern answer.

Q43. What are enums and what can they do beyond constants?

An enum defines a fixed set of named instances, type-safe constants the compiler checks. Unlike int constants, you can't pass an invalid value, and they work cleanly in switch statements.

Java enums are full classes: they can have fields, constructors, and methods, and each constant can override behavior. That makes them ideal for small state machines or strategy tables, like an operation enum where each constant implements apply differently.

java
enum Op {
    ADD { int apply(int a, int b) { return a + b; } },
    MUL { int apply(int a, int b) { return a * b; } };
    abstract int apply(int a, int b);
}
Op.ADD.apply(2, 3);   // 5

Q44. What does the synchronized keyword do?

synchronized enforces mutual exclusion: only one thread at a time can hold a given object's monitor lock. A synchronized method locks on the instance (or the Class for static methods); a synchronized block locks on whatever object you name. This prevents two threads from interleaving on shared mutable state.

It also provides a memory guarantee: changes made under the lock are visible to the next thread that acquires it. The costs are contention (threads waiting) and deadlock risk if locks are taken in inconsistent orders.

java
class Counter {
    private int count;
    synchronized void inc() { count++; }   // one thread at a time
    synchronized int get() { return count; }
}

Key point: Point out that synchronized gives both mutual exclusion and visibility. People remember the locking half and forget the memory-visibility half.

Q45. What does the volatile keyword guarantee, and what doesn't it?

volatile guarantees visibility and ordering: a write to a volatile field is immediately visible to other threads reading it, and the compiler and CPU won't reorder around it. It's the right tool for a simple flag one thread sets and another reads.

What it does not give is atomicity of compound operations. count++ is a read, an increment, and a write, so two threads can still lose updates on a volatile int. For that use AtomicInteger, an atomic class, or a lock.

java
private volatile boolean running = true;   // visibility for a flag
public void stop() { running = false; }
public void loop() { while (running) { /* work */ } }

Key point: The killer follow-up is 'is volatile enough for count++?'. No, it's not atomic. Getting that right is the whole point of the question.

Q46. How do you create and run a thread in Java?

The two classic ways: extend Thread and override run, or implement Runnable and pass it to a Thread; the Runnable approach is preferred because it doesn't use up your one inheritance slot. You call start() (not run()) to actually launch a new thread; calling run() directly just executes it on the current thread.

In real code you rarely manage Thread objects by hand. You submit tasks to an ExecutorService, which pools and reuses threads. Since Java 21, virtual threads make it cheap to run huge numbers of blocking tasks.

java
Runnable task = () -> System.out.println("on " + Thread.currentThread());
Thread t = new Thread(task);
t.start();          // new thread; start(), not run()

try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
    pool.submit(task);   // the modern way
}

Key point: The classic trap is start() versus run(). Calling run() directly gives you zero new threads. Say start() and explain why.

Q47. What is an ExecutorService and why prefer it over raw threads?

An ExecutorService is a managed pool of worker threads you submit tasks to. It decouples what to run from how threads are created, reuses threads instead of paying creation cost per task, bounds concurrency, and returns a Future for each submitted task so you can get results or cancel.

Prefer it because manually creating threads doesn't scale: no reuse, no backpressure, no clean shutdown. Executors gives you fixed pools, cached pools, scheduled pools, and, in modern Java, a virtual-thread-per-task executor for massive I/O concurrency.

java
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> f = pool.submit(() -> expensive());
Integer result = f.get();       // blocks for the result
pool.shutdown();

Key point: Saying 'I don't hand-roll threads, I submit to an ExecutorService' immediately indicates production experience rather than textbook Java.

Back to question list

Java Interview Questions for Experienced Developers

Experienced18 questions

advanced rounds probe internals, design judgment, and production scars. Expect every answer here to draw a follow-up.

Q48. How is JVM memory organized (heap, stack, metaspace)?

Each thread has its own stack holding stack frames: local variables, operand stacks, and return addresses, which is why deep or infinite recursion throws StackOverflowError. Objects live on the shared heap, split into young and old generations for the garbage collector. Class metadata lives in metaspace (native memory, which replaced the old fixed PermGen in Java 8).

The distinction that matters in interviews: a reference variable sits on the stack, but the object it points to sits on the heap. Escaping references, large caches, and classloader leaks are the usual causes of heap growth, while runaway recursion blows the stack.

Key point: The clean framing is 'references on the stack, objects on the heap, class metadata in metaspace'. That sentence answers most follow-ups by itself.

Q49. How does garbage collection work in Java?

The garbage collector reclaims objects that are no longer reachable from GC roots (stack references, static fields, and such). It relies on the generational hypothesis: most objects die young, so the heap is split into a young generation collected often and cheaply, and an old generation collected less often. Survivors get promoted from young to old.

Modern collectors like G1 (the default) and the low-pause ZGC and Shenandoah do most work concurrently to keep pause times low. You don't call the collector; System.gc() is only a hint. The senior points are that GC is about reachability, not reference counting, and that you tune it by choosing a collector and sizing the heap, not by nulling variables.

Key point: 'GC reclaims unreachable objects, not zero-referenced ones' is the distinction that separates real understanding from a memorized definition (Java doesn't reference-count).

Q50. How does class loading work in the JVM?

A class moves through loading (a classloader reads the bytecode into a Class object), linking (verification, preparation of static fields, and resolution of symbolic references), and initialization (running static blocks and static assignments). Initialization is lazy: it happens the first time the class is actively used, and exactly once per class per loader.

Classloaders form a hierarchy and use parent-first delegation: bootstrap loads core JDK classes, then platform, then application loaders. This is why you can't shadow java.lang classes and why classloader design underpins app servers, plugins, and hot reloading. Custom loaders let frameworks isolate or reload code.

How a class gets loaded and initialized

1Loading
a classloader finds the .class bytes and creates the Class object
2Linking
verify the bytecode, prepare static fields with defaults, resolve references
3Initialization
run static initializers and static field assignments, once, lazily on first use
4Delegation
loaders ask their parent first (parent-delegation), so core classes load from the bootstrap loader

Initialization is lazy and happens once per class per loader, which is why the static-holder singleton is both lazy and thread-safe.

Key point: Parent-delegation is the concept interviewers dig for. It's why two different classloaders can load 'the same' class as two incompatible types.

Q51. What is the Java Memory Model and why does happens-before matter?

The Java Memory Model defines when a write by one thread is guaranteed visible to a read by another. Without synchronization, threads may see stale values because of CPU caches and instruction reordering. The model expresses the rules through the happens-before relationship: if action A happens-before action B, then A's effects are visible to B.

Synchronized blocks, volatile writes and reads, thread start and join, and the concurrent utilities all establish happens-before edges. This is why unsynchronized shared mutable state is a bug even on a single core with a compiler that reorders: correctness comes from establishing happens-before, not from luck.

Key point: Naming happens-before as the correctness contract, rather than just 'use synchronized', is what marks The production-ready answer on concurrency.

Watch a deeper explanation

Video: Multithreading in Java Explained in 10 Minutes (Coding with John, YouTube)

Q52. How does ConcurrentHashMap achieve thread safety without locking the whole map?

Modern ConcurrentHashMap locks at the bucket level, not the whole table. Reads are lock-free (fields are volatile), and writes lock only the specific bin being modified using compare-and-swap for the common uncontended case, so unrelated buckets update in parallel. Resizing is cooperative: multiple threads help transfer bins.

The trade-offs to name: iterators are weakly consistent (they don't throw ConcurrentModificationException and may or may not reflect concurrent updates), size() is an estimate under concurrency, and it forbids null keys and values so that a null return unambiguously means absent. It replaced the older segment-based design (pre-Java 8).

Key point: Contrasting bucket-level locking with Hashtable's single lock, and mentioning weakly-consistent iterators, is the depth this question screens for.

Q53. What is CompletableFuture and how does it improve on Future?

A plain Future only lets you block on get() or poll isDone(); you can't chain work or react to completion. CompletableFuture is a composable future: you attach callbacks (thenApply, thenCompose, thenCombine), handle errors (exceptionally, handle), and run stages on chosen executors, building non-blocking pipelines.

It's how you fan out to several services and combine results without blocking a thread per call. The senior cautions: always supply your own executor rather than relying on the common ForkJoinPool for blocking work, and handle exceptions explicitly because a swallowed exception in a stage is easy to miss.

java
CompletableFuture<String> pipeline =
    CompletableFuture.supplyAsync(() -> fetchUser(id), pool)
        .thenApply(User::email)
        .exceptionally(ex -> "unknown");
String email = pipeline.join();

Key point: 'pass your own executor, don't block the common pool' is the operational detail that matters.

Q54. How do you implement a singleton correctly in Java?

The cleanest correct answer is a single-element enum: the JVM guarantees one instance, handles serialization, and is thread-safe for free. If you need lazy initialization, the static-holder idiom (a nested static class that holds the instance) is lazy and thread-safe because class initialization is lazy and runs once.

The double-checked locking version works only if the field is volatile, and it's easy to get subtly wrong, which is why the enum or holder patterns are preferred. Whatever you pick, remember singletons are global mutable state and complicate testing, so many teams inject dependencies instead.

java
// enum singleton: thread-safe, serialization-safe, one line
public enum Config { INSTANCE; public int poolSize() { return 8; } }

// lazy holder idiom
public class Cache {
    private Cache() {}
    private static class Holder { static final Cache INSTANCE = new Cache(); }
    public static Cache get() { return Holder.INSTANCE; }
}

Key point: Leading with the enum singleton and singletons hurt testability.

Q55. Design question: how would you implement a producer-consumer queue?

Reach for a BlockingQueue (like ArrayBlockingQueue or LinkedBlockingQueue) rather than hand-rolling wait/notify. Producers call put, which blocks when the queue is full; consumers call take, which blocks when it's empty. The queue handles all the locking and signaling, so the bounded buffer applies natural backpressure.

If asked to build it from primitives, guard a shared buffer with a lock and two condition variables (notFull and notEmpty), always wait in a while loop to handle spurious wakeups, and signal the other side after each operation. Being able to say 'in production I'd just use a BlockingQueue' first, then explain the mechanics, is the ideal shape of this answer.

java
BlockingQueue<Task> queue = new ArrayBlockingQueue<>(100);

// producer
queue.put(task);        // blocks if full (backpressure)

// consumer
Task t = queue.take();  // blocks if empty

Key point: Say 'BlockingQueue in production, wait/notify only if you make me build it'. Reaching for the primitive first, when a library exists, indicates junior.

Q56. What causes a deadlock and how do you prevent it?

A deadlock happens when two or more threads each hold a lock the other needs and none can proceed. It requires four conditions together: mutual exclusion, hold-and-wait, no preemption, and a circular wait. Break any one and the deadlock can't form.

The practical prevention that scales: impose a global lock ordering so every thread acquires locks in the same sequence, which kills the circular-wait condition. Also use tryLock with a timeout to back off, keep critical sections small, and prefer higher-level concurrency utilities over raw locks. Thread dumps (jstack) diagnose a deadlock in production by showing the cycle.

Key point: Naming a consistent global lock ordering as the primary fix is the answer the question needs; listing the four Coffman conditions shows you understand why it works.

Q57. What is ThreadLocal and where is it useful, and where does it bite?

ThreadLocal gives each thread its own independent copy of a variable, so threads don't share or contend on it. Common uses are per-thread context that would be awkward to thread through every method call: a SimpleDateFormat (which isn't thread-safe), a database transaction or user context, or a per-request trace ID.

The bites are real in pooled environments. Because thread pools reuse threads, a ThreadLocal not cleared after a task leaks stale state into the next task on that thread, and it can hold objects alive, causing a memory leak. Always clear it in a finally block, and be aware that virtual threads change the calculus since spawning many is cheap.

java
private static final ThreadLocal<String> USER = new ThreadLocal<>();

try {
    USER.set(currentUser());
    handleRequest();
} finally {
    USER.remove();     // essential on pooled threads
}

Key point: The leak-on-pooled-threads point (clear it in finally) is what interviewers dig for. Anyone can define ThreadLocal; knowing its failure mode is the production signal.

Q58. What are sealed classes and what problem do they solve?

A sealed class or interface restricts which types can extend or implement it, listed with the permits clause. It sits between a fully open type (anyone can subclass) and a final type (no one can), letting you model a closed set of variants on purpose.

Paired with pattern matching in switch, sealed hierarchies give the compiler exhaustiveness checking: if you handle every permitted subtype, you need no default, and adding a new variant makes the switch fail to compile until you handle it. That's how Java expresses algebraic-data-type-style modeling for things like a Shape that's only Circle, Square, or Triangle.

java
sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double s) implements Shape {}

double area(Shape shape) {
    return switch (shape) {          // exhaustive: no default needed
        case Circle c -> Math.PI * c.r() * c.r();
        case Square s -> s.s() * s.s();
    };
}

Key point: The payoff to name is exhaustiveness: a sealed type plus switch means the compiler forces you to handle every case. That's the reason to reach for it.

Q59. How do default methods work, and how does Java resolve the diamond problem?

Default methods let an interface ship a concrete implementation, which is how the JDK added stream() and forEach to existing interfaces without breaking implementers. They give interfaces behavior, not state.

If a class inherits conflicting defaults from two interfaces, the compiler refuses to guess: it forces the class to override the method and can pick a specific parent with the InterfaceName.super.method() syntax. So Java sidesteps the classic multiple-inheritance diamond by making the ambiguity a compile error you must resolve.

java
interface A { default String hi() { return "A"; } }
interface B { default String hi() { return "B"; } }
class C implements A, B {
    public String hi() { return A.super.hi(); }   // resolve explicitly
}

Q60. Why is Cloneable considered broken, and how do you copy objects instead?

Object.clone with the Cloneable marker interface is widely regarded as a mistake: clone is protected and on Object rather than Cloneable, it bypasses constructors, it does a shallow copy by default so nested mutable state is shared, and the contract is loosely specified. Getting a correct deep clone right is fiddly and error-prone.

Prefer a copy constructor or a static factory (new Foo(existing) or Foo.copyOf(existing)) that you control, which is explicit about depth and works with final fields. For records, a small with-style method or a builder handles copies cleanly. Serialization-based deep copy exists but is slow and fragile.

Key point: Saying 'I use a copy constructor, not Cloneable' and knowing why (shallow, bypasses constructors) is a well-known senior-Java signal (Effective Java item).

Q61. How does Java serialization work and what are its risks?

Implementing Serializable lets the JVM convert an object graph to a byte stream and back with ObjectOutputStream and ObjectInputStream. A serialVersionUID identifies the class version so deserialization can detect incompatible changes, and transient fields are skipped.

The risks are real: deserializing untrusted data can execute arbitrary code through gadget chains, so never deserialize input you don't trust. Built-in serialization is also brittle across class changes and slow. Most teams use JSON or a schema format like protobuf for external data and keep Java serialization for controlled internal use only.

Key point: The security point (never deserialize untrusted input) is what the key signal is. It's caused real, severe CVEs, so it indicates production awareness.

Q62. What is reflection and when is it justified?

Reflection lets code inspect and manipulate classes, methods, and fields at runtime: discover an object's methods, read private fields, or instantiate a class named by a string. Frameworks for dependency injection, serialization, ORMs, and testing lean on it heavily.

The costs are why you avoid it in ordinary code: it's slower, it bypasses compile-time type checking and access control (a source of fragility and security concern), and the module system (JPMS) now restricts deep reflection unless a package is opened. Use it for framework-level work, not application logic.

Q63. What is the string pool and how does intern() work?

The string pool is a cache of unique string values the JVM maintains so that identical string literals share one object, saving memory. Every literal is automatically interned, which is why "hi" == "hi" is true even with ==.

Strings built at runtime (with new String or concatenation of variables) are not pooled, so they're distinct objects; calling intern() on such a string returns the pooled instance, adding it if absent. It's rarely needed in modern code and can hurt if overused, but it explains the == surprises around strings and shows understanding of how immutability enables the optimization.

java
String a = "java";
String b = new String("java");
a == b;            // false: b is a fresh object
a == b.intern();   // true:  intern returns the pooled instance

Key point: This closes the loop on the == versus equals question. Explaining the pool is what turns 'strings are immutable' into a full mental model.

Q64. What actually happens when you run a Java program, and what does the JIT do?

javac compiles source to platform-neutral bytecode in .class files. At runtime the JVM loads that bytecode and starts interpreting it, while profiling which methods run hot. The just-in-time compiler then compiles those hot methods to optimized native code, applying inlining, escape analysis, and other optimizations based on real runtime behavior.

This is why Java is both compiled and interpreted, and why a server warms up: early calls are interpreted, later ones run as native code. It also explains why microbenchmarks need warm-up and why profile-guided native compilation can beat naive ahead-of-time compilation. Tiered compilation (C1 then C2) balances fast startup against peak throughput.

Key point: The framing 'both compiled and interpreted, with a JIT that optimizes hot paths from real profiles' is the complete answer. Warm-up is the classic follow-up.

Q65. A Java service is slow or leaking memory in production. Walk through your debugging approach.

Observe before changing anything: metrics and logs to locate the slow path, then a low-overhead profiler like async-profiler or Java Flight Recorder on the live process to see where CPU and allocations actually go. For a suspected leak, take heap dumps over time and diff them (Eclipse MAT, jmap) to find the growing dominator, usually an unbounded cache, a static collection, or a classloader leak.

Check the usual suspects: GC logs for long pauses or constant full GCs, thread dumps (jstack) for deadlocks or a blocked pool, N+1 queries, and missing timeouts causing thread exhaustion. Then fix at the right layer and confirm with the same measurement. The structure, observe, hypothesize, verify, fix, confirm, matters more than any single tool.

Key point: The methodology is; naming JFR, heap-dump diffing, and GC logs is the supporting evidence.

Back to question list

Why Java? Java vs Python, C#, and Kotlin

Java wins when you need static typing, mature tooling, and a runtime that has been tuned for throughput across decades of production use. It trades some brevity for compile-time safety and predictable performance, which is why banks, retailers, and Android all lean on it. Where it feels heavier is quick scripting and data work, where Python's syntax and libraries move faster, and in language ergonomics, where Kotlin trims the boilerplate while running on the same JVM. Knowing these trade-offs out loud is itself an interview signal: it shows you pick tools on merits, not habit.

LanguageTypingBest atWatch out for
JavaStaticEnterprise backends, Android, big systemsVerbosity, slower to prototype
PythonDynamicData, ML, scripting, fast prototypingRuntime speed, CPU parallelism (GIL)
C#Static.NET services, Windows, game dev (Unity)Ecosystem centered on Microsoft stack
KotlinStaticModern Android, concise JVM codeSmaller talent pool, newer libraries

How to Prepare for a Java Interview

Prepare in layers, and practice out loud. Most Java rounds move from concept questions to live coding to a design or debugging discussion, so rehearse each stage rather than only reading answers.

  • Master your tier's concepts until you can explain them without notes, then read one tier up for the stretch questions.
  • Type and run every snippet in a real JDK; modifying working code cements it far faster than reading.
  • Practice thinking aloud on small problems with a timer, because your process, not just the final answer is the technical point.
  • Take the quiz on this page to find weak spots, then revisit those questions before the real round.

The typical Java interview flow

1Recruiter or phone screen
background, motivation, a few concept checks
2Core concepts
OOP, collections, exceptions, equals and hashCode, the JVM
3Live coding
write and explain a method under observation
4Design or debugging
concurrency trade-offs, reading unfamiliar code, follow-ups

Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.

Test Yourself: Java Quiz

Ready to test your Java knowledge?

10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.

10 questions Instant feedback Free certificate on 70%+

Frequently  Asked  Questions

Which Java topics should I prioritize before a technical round?

Prioritize the areas that change real decisions: fundamentals, practical tasks, debugging, trade-offs, and evidence. The highest-value questions are tied to a project example, a validation check, or a failure mode.

Are these questions enough to pass a Java interview?

They cover the question-answer portion well, but most Java rounds also include live coding: writing a method under observation while explaining your thinking. solving small problems out loud with a timer is the practice path. Our AI coding interview prep guide covers that format, including how automated evaluation reads your process.

Which Java version do these answers assume?

A modern long-term-support release, Java 17 or 21. That means records, sealed classes, switch expressions, text blocks, and var are all fair game. If an interviewer's codebase runs Java 8, the collections, streams, and lambda answers still hold; the newer language features are the main difference. When in doubt, say which version you're answering for.

How long does it take to prepare for a Java interview?

If you use Java at work or in study, one to two weeks of an hour a day covers this bank with practice runs. Starting colder, plan three to four weeks and write code daily; reading answers without typing them is how preparation quietly fails.

Do interviewers ask exactly these questions?

The concepts repeat far more reliably than the exact wording. Interviewers rephrase, chain follow-ups, and push toward whatever you claim confidence in. Prepare the underlying ideas, string immutability, the equals and hashCode contract, how HashMap works, the GC model, and the phrasing takes care of itself.

Is there a way to test my Java knowledge?

Yes. The quiz on this page scores you as you go and explains every answer, so you find your weak spots fast. Pass the threshold and you can download a shareable certificate with your name and score, free and with no sign-up. It's the quickest way to check yourself before the real round.

From the team that builds coding interviews

Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages, Java included. These questions reflect what actually gets asked and evaluated inside the interviews we host for 5,000+ HR teams.

See how the AI Coding Interviewer works

Sources

Adithyan RKWritten by Adithyan RK
Surya N
Fact-checked by Surya N
Published on: 22 Apr 2026Last updated: 3 Jul 2026
Share: