Core Java • Collections • Concurrency • Java 8+ • 2026

Java Interview Questions

30 questions What each one tests, an answer frame, a spoken answer 33 min read

This page is for anyone facing a Java round, from a first job to a senior backend role. Most Java interviews start with the JVM and memory, move to strings, equals and hashCode, and collections internals, then test exceptions, generics and threads, and finish with lambdas, streams, Optional and records. Senior rounds add a production story and a judgement call. Each question shows what the interviewer is really checking, the shape of a strong answer and a short answer you can say out loud. Practise saying them, then change the stories to your own.

Search all questions by round, difficulty and level, or save the ones you want to practise.

JVM & Memory 4 questions

Easy Technical round Fresher Practice question

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

What the interviewer is really testing:
Whether you know how Java code actually gets from a source file to running on a machine, not just the three expansions.
Answer frame:

JVM: runs bytecode; loads classes, verifies them, interprets and JIT-compiles hot code.

JRE: the JVM plus the standard class libraries, enough to run a program.

JDK: everything needed to build: compiler, jar, debugger and other tools on top of the runtime.

Sample spoken answer:

"The JVM is the engine. It loads compiled class files, checks the bytecode is safe, and runs it, first by interpreting and then by compiling the hot parts to native code with the JIT. The JRE is the JVM plus the standard libraries, like java.lang and java.util, so it's what you need to run a Java program. The JDK is what developers install: it has the runtime plus tools like javac to compile, jar to package, and a debugger. The flow is: I write a .java file, javac turns it into bytecode, and any JVM on any operating system can run that bytecode. That's where 'write once, run anywhere' comes from: the bytecode is portable, the JVM is the platform-specific part."

Red flag to avoid:

Saying Java is platform-independent because the JVM is, when it's the bytecode that is portable and the JVM that is built per platform.

They may ask next:
  • What does the JIT compiler do, and why does a Java service often get faster after it has been running a while?
  • Is the JVM itself platform-independent?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

2. Where do objects and local variables live in memory in Java, heap or stack?

What the interviewer is really testing:
Whether you have a correct mental model of memory, which you need to reason about recursion errors, thread safety and garbage collection.
Answer frame:

Stack: one per thread; each method call gets a frame holding local primitives and references.

Heap: shared by all threads; every object and array lives here and is cleaned up by the garbage collector.

Errors: deep recursion gives StackOverflowError; too many live objects give OutOfMemoryError.

Sample spoken answer:

"Every thread gets its own stack. Each time a method is called, a frame is pushed that holds its local variables: primitives are stored right there, and for objects the frame only holds a reference. The objects themselves always go on the heap, which all threads share, and the garbage collector frees them once nothing reachable points to them. So if I write Person p = new Person() inside a method, p is on the stack and the Person object is on the heap. This matters in practice. Local variables are naturally thread-safe because no other thread can see my stack, but objects on the heap can be shared. And the two errors tell you which area ran out: StackOverflowError usually means runaway recursion, OutOfMemoryError means the heap is full."

Red flag to avoid:

Saying objects created inside a method live on the stack, or that primitives are always on the stack even when they are fields of an object.

They may ask next:
  • If a local variable holds a reference to a shared object, is that object thread-safe?
  • What happens to an object on the heap when the method that created it returns?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

3. How does garbage collection decide what to free, and why does the JVM split the heap into generations?

What the interviewer is really testing:
Whether you understand reachability and the generational idea well enough to explain pauses and tuning, beyond saying 'it frees unused objects'.
Answer frame:

Reachability: start from GC roots such as thread stacks and static fields; anything not reachable is garbage, cycles included.

Generations: most objects die young, so new objects go to a young area that is collected often and cheaply.

Promotion and pauses: survivors move to the old generation, collected less often; some phases stop the application.

Sample spoken answer:

"The collector doesn't count references. It starts from GC roots, things like local variables on thread stacks, static fields and a few JVM internals, and marks everything it can reach. Whatever isn't marked is garbage, even if those objects point at each other in a cycle. The heap is split into generations because most objects die very young, like a temporary string built for a log line. So new objects go into the young generation, which is collected often and quickly because it's mostly garbage. Objects that survive several young collections get promoted to the old generation, which is bigger and collected less often. Some phases pause the application threads, and that's what shows up as latency spikes. Modern collectors like G1 and ZGC are designed to keep those pauses short."

Red flag to avoid:

Saying Java uses reference counting, or that an object is freed the moment its variable goes out of scope.

They may ask next:
  • Does calling System.gc() force a collection?
  • What would you look at first if a service had long GC pauses?
Say it in 60 seconds
Hard Situational round Mid-level, Senior Practice question

4. You're on call and a Java service keeps crashing with OutOfMemoryError. What do you do first, and what do you do after it's stable?

What the interviewer is really testing:
Whether you can restore service first while keeping the evidence you need to find the real cause.
Answer frame:

Stabilise: restart or scale out, roll back a recent deploy if the timing fits, protect the users.

Keep evidence: read the exact error message, capture a heap dump and GC logs before they're lost.

Root cause: analyse the dump, fix the leak or the load, add monitoring and an alert on heap use.

Sample spoken answer:

"First I protect users: restart the failing instances or add capacity, and if the crashes started right after a deploy, I roll it back. At the same time I read the exact message, because OutOfMemoryError has several kinds. Java heap space means the heap is full. Metaspace points at class loading. Unable to create native thread means too many threads, not too much data. I make sure we capture a heap dump, turning on dump-on-OOM if it isn't already on, and I keep the GC logs. Raising the maximum heap might buy time, but I treat it as a stopgap, not a fix. Once it's stable, I open the dump and look at what's filling the heap and what's holding on to it. Then I fix the cause, add an alert on heap use after GC, and write up what happened."

Red flag to avoid:

Going straight to doubling the heap size and closing the incident without a heap dump or root cause.

They may ask next:
  • Java has a garbage collector, so how can a Java service leak memory at all?
  • The heap dump shows millions of objects of one class. What is your next step?
  • How would you tell a genuine memory leak from a traffic spike that simply needs more memory?
Say it in 60 seconds

OOP 2 questions

Medium Technical round Fresher, Mid-level Practice question

5. Now that interfaces can have default methods, when would you still choose an abstract class over an interface?

What the interviewer is really testing:
Whether you know what changed in Java 8 and can still give a design reason for each, not an outdated list of differences.
Answer frame:

Interface: a contract; a class can implement many; default and static methods allowed, but no instance state.

Abstract class: can hold fields, constructors and protected members; a class can extend only one.

Rule of thumb: interface for a capability, abstract class for shared state and a partial implementation.

Sample spoken answer:

"Since Java 8, interfaces can have default and static methods, and later private methods too, so the old line 'interfaces have no code' isn't true any more. The real difference now is state and inheritance. An interface can't hold instance fields, only constants, and a class can implement as many interfaces as it likes. An abstract class can have fields, constructors and protected helpers, but a class can extend only one. So I use an interface to describe a capability, like Comparable or a PaymentGateway contract, especially when unrelated classes need it. I'd pick an abstract class when several subclasses share real state and setup logic, for example a base report class that holds a title and a date range and implements the common steps, leaving one method abstract for the part each report does differently."

Red flag to avoid:

Saying interfaces can't contain any method bodies, which has been wrong since Java 8.

They may ask next:
  • What happens if a class implements two interfaces with the same default method?
  • Why were default methods added to the language in the first place?
Say it in 60 seconds
Easy Technical round Fresher Practice question

6. What's the difference between method overloading and method overriding, and when is each one resolved?

What the interviewer is really testing:
Whether you understand compile-time versus runtime binding, which is the real point behind this very common question.
Answer frame:

Overloading: same name, different parameter list, in one class; chosen at compile time from the declared types.

Overriding: a subclass replaces an inherited method with the same signature; chosen at runtime from the actual object.

Rules: an override can't reduce visibility or throw broader checked exceptions; static and private methods aren't overridden.

Sample spoken answer:

"Overloading is having several methods with the same name but different parameters, like print(int) and print(String). The compiler picks which one to call based on the declared types of the arguments, so it's decided at compile time. Overriding is when a subclass provides its own version of a method it inherited, with the same signature. Which version runs is decided at runtime from the real object, not the variable type. So if I have Animal a = new Dog() and call a.speak(), Dog's speak runs. That's runtime polymorphism. An override has rules: it can't make the method less visible, it can't throw broader checked exceptions, and it can return a more specific type. Static methods can't be overridden, only hidden, and private or final methods can't be overridden at all."

Code:
class Animal { String speak() { return "..."; } }
class Dog extends Animal {
    @Override String speak() { return "Woof"; }   // overriding
    String speak(int times) { return "Woof".repeat(times); } // overloading
}
Animal a = new Dog();
a.speak(); // "Woof": chosen at runtime
Red flag to avoid:

Mixing the two up, or saying overloading is decided at runtime.

They may ask next:
  • Why is the @Override annotation worth adding even though it's optional?
  • What happens if you define a static method with the same signature in a subclass?
Say it in 60 seconds

Strings & Objects 3 questions

Easy Technical round Fresher Practice question

7. Why are Strings immutable in Java, and what is the string pool?

What the interviewer is really testing:
Whether you understand the design reasons, and whether you know why == on strings sometimes seems to work and sometimes doesn't.
Answer frame:

Immutable: once created a String never changes; every 'change' returns a new String.

Why: safe to share and cache, safe across threads, the hash code can be cached, and values like file paths can't be altered after a check.

Pool: string literals are stored once and shared; new String() creates a separate object.

Sample spoken answer:

"A String can't change after it's created. Methods like toUpperCase or concat give you a new String and leave the original alone. That design has real benefits. Because nobody can change a String, the JVM can safely share one copy of each literal, which is the string pool: if two places in my code use the literal "hello", they point to the same object. It also makes Strings thread-safe for free, lets the hash code be cached so they're fast as HashMap keys, and stops someone changing a file path or class name after it's been checked. The side effect people trip on is ==. Two literals compare equal with == because they're the same pooled object, but new String("hello") is a separate object, so == is false. That's why I always compare content with equals."

Code:
String a = "hello";
String b = "hello";
String c = new String("hello");
System.out.println(a == b);          // true: same pooled object
System.out.println(a == c);          // false: c is a new object
System.out.println(a.equals(c));     // true: same content
System.out.println(a == c.intern()); // true: intern returns the pooled copy
Red flag to avoid:

Using == to compare string content, or saying immutability exists only to save memory.

They may ask next:
  • Why is building a long string with + inside a loop slow, and what would you use instead?
  • What is the difference between StringBuilder and StringBuffer?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

8. If you override equals, why must you also override hashCode? What goes wrong if you don't?

What the interviewer is really testing:
Whether you know the contract and can show the concrete bug it causes in hash-based collections.
Answer frame:

Contract: equal objects must return the same hash code; unequal objects may share one.

Failure: HashMap and HashSet look in the bucket chosen by hashCode first, so equal objects end up in different buckets.

Good practice: use the same fields in both, prefer immutable fields, generate or use a record.

Sample spoken answer:

"The contract says that if two objects are equal by equals, they must return the same hashCode. The reverse isn't required: different objects can collide. If I override equals and forget hashCode, each object keeps the default identity-based hash code. Then HashMap and HashSet break in a quiet way. Say I add a Point(1, 2) to a HashSet, then ask contains(new Point(1, 2)). equals would say they match, but the set first uses hashCode to pick a bucket, looks in a different bucket, and returns false. I get duplicates in sets and lookups that miss. So I build both methods from the same fields, usually with Objects.equals and Objects.hash, or I use a record, which generates both correctly. I also avoid mutable fields in the hash, because changing one after insertion strands the object in the wrong bucket."

Code:
final class Point {
    private final int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point)) return false;
        Point p = (Point) o;
        return x == p.x && y == p.y;
    }
    @Override public int hashCode() { return Objects.hash(x, y); }
}
Red flag to avoid:

Saying equal hash codes mean equal objects, or not being able to name the collection bug.

They may ask next:
  • Is it legal for hashCode to return the same constant for every object? What would it cost?
  • What goes wrong if a field used in hashCode changes while the object is inside a HashSet?
Say it in 60 seconds
Medium Coding round Mid-level Practice question

9. How would you write your own immutable class in Java? Walk me through the rules.

What the interviewer is really testing:
Whether you know the traps beyond 'make fields final', especially mutable fields leaking through constructors and getters.
Answer frame:

Lock it down: final class, private final fields, no setters.

Defensive copies: copy mutable inputs in the constructor and never hand out the internal object.

Change means new: methods that 'modify' return a new instance.

Sample spoken answer:

"I make the class final so nobody can subclass it and add mutable behaviour. All fields are private and final, and there are no setters. The part people miss is mutable fields. If my class holds a List or a Date and I just store the reference passed into the constructor, the caller can still change it from outside. So I copy it on the way in, for a list with List.copyOf, which also gives me an unmodifiable list, and I never return the internal mutable object from a getter. Any method that looks like a change, like withSalary, returns a new object instead. The payoff is that the object is safe to share between threads without locks, and once I add equals and hashCode it makes a safe map key. For simple data carriers I'd reach for a record now, but I still have to copy mutable components myself."

Code:
public final class Team {
    private final String name;
    private final List<String> members;

    public Team(String name, List<String> members) {
        this.name = name;
        this.members = List.copyOf(members); // defensive, unmodifiable copy
    }
    public String name() { return name; }
    public List<String> members() { return members; } // safe: cannot be modified
    public Team withMember(String m) {
        List<String> next = new ArrayList<>(members);
        next.add(m);
        return new Team(name, next);
    }
}
Red flag to avoid:

Stopping at 'final fields and no setters' while storing a caller's mutable list directly.

They may ask next:
  • Why is making the fields final not enough on its own?
  • Is a record automatically deeply immutable?
Say it in 60 seconds

Collections 5 questions

Medium Technical round Fresher, Mid-level Practice question

10. Walk me through what happens inside a HashMap when you call put and then get.

What the interviewer is really testing:
Whether you understand hashing, buckets, collisions and resizing well enough to reason about performance and bugs.
Answer frame:

Bucket choice: hashCode is spread and masked to an index in an internal array.

Collision: entries sharing a bucket form a linked list, which becomes a balanced tree when it grows large.

Lookup and resize: get finds the bucket, then uses equals; past the load factor the array doubles.

Sample spoken answer:

"Internally a HashMap is an array of buckets. On put, it calls the key's hashCode, mixes the high bits into the low bits so poor hash codes still spread out, and uses that to pick a bucket index. If the bucket is empty the entry goes straight in. If another key is already there, it walks the entries and uses equals: a match replaces the value, otherwise the new entry is added. That's a collision. When one bucket gets crowded, Java converts its list into a red-black tree, so a bad hash doesn't make lookups linear. On get, it computes the same bucket and uses equals to find the key. When the number of entries passes capacity times the load factor, 0.75 by default, it doubles the array and redistributes. So lookups are constant time on average, not guaranteed."

Red flag to avoid:

Claiming get is always constant time, or not mentioning equals at all.

They may ask next:
  • Why must the array size be a power of two in this design?
  • Does HashMap allow null keys and values?
  • Why is HashMap unsafe when several threads write to it?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

11. How is ConcurrentHashMap different from Hashtable or Collections.synchronizedMap, and when does it still not make your code thread-safe?

What the interviewer is really testing:
Whether you know why it scales better, and whether you know that thread-safe methods don't make a check-then-act sequence atomic.
Answer frame:

Old options: Hashtable and synchronizedMap lock the whole map on every call, so threads queue up.

ConcurrentHashMap: reads mostly don't lock; writes lock only one bin; iterators don't throw ConcurrentModificationException.

The trap: get then put is still a race; use putIfAbsent, computeIfAbsent or merge.

Sample spoken answer:

"Hashtable and Collections.synchronizedMap make every method synchronized on one lock, so only one thread can touch the map at a time, even for reads. ConcurrentHashMap was built for concurrency. In modern versions, reads usually take no lock at all, and a write locks only the single bin it's changing, or uses a compare-and-swap if the bin is empty. So many threads can read and write different keys at once. Its iterators are weakly consistent, meaning they won't throw ConcurrentModificationException. It also refuses null keys and values, so a null from get always means 'absent'. The catch is compound actions. If I write 'if the key is missing, put a value', that's two calls, and another thread can slip in between. I use the atomic methods instead: computeIfAbsent, putIfAbsent or merge for counters."

Code:
ConcurrentHashMap<String, Integer> hits = new ConcurrentHashMap<>();

// Race: two threads can both read 0 and both write 1
Integer old = hits.get(page);
hits.put(page, old == null ? 1 : old + 1);

// Atomic per key
hits.merge(page, 1, Integer::sum);
Red flag to avoid:

Saying any code that uses ConcurrentHashMap is automatically thread-safe.

They may ask next:
  • Why doesn't ConcurrentHashMap allow null values?
  • Is the value returned by size() exact while other threads are writing?
  • What should you avoid doing inside a computeIfAbsent function?
Say it in 60 seconds
Easy Technical round Fresher Practice question

12. ArrayList or LinkedList: how do they differ inside, and which one do you actually reach for?

What the interviewer is really testing:
Whether you know the real costs, including the common myth that LinkedList is faster for inserts in the middle.
Answer frame:

ArrayList: backed by an array; fast index access; appending is cheap on average; middle inserts shift elements.

LinkedList: doubly linked nodes; cheap at the ends, but reaching an index means walking the list.

Practice: ArrayList by default; ArrayDeque for queues and stacks.

Sample spoken answer:

"ArrayList stores elements in an array, so get by index is constant time. Adding at the end is cheap on average; now and then it grows the array and copies, but that cost spreads out. Inserting or removing in the middle means shifting everything after that point. LinkedList is a chain of nodes, each pointing to the next and previous, so adding or removing at either end is constant time, but get(i) has to walk the chain. People say LinkedList is better for inserting in the middle, but you still have to walk to that position first, unless you're already there with an iterator. Each node also costs extra memory and is scattered in memory, which the CPU cache doesn't like. So I use ArrayList almost always, and when I need a queue or a stack I use ArrayDeque rather than LinkedList."

Red flag to avoid:

Saying LinkedList is always faster for inserts without mentioning the cost of reaching the position.

They may ask next:
  • What is the time cost of removing the first element of an ArrayList with a million items?
  • When would you set an initial capacity on an ArrayList?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

13. Why does removing items inside a for-each loop over a list throw ConcurrentModificationException, and how do you remove items safely?

What the interviewer is really testing:
Whether you know how fail-fast iterators work and know the clean ways to filter a collection, even in a single thread.
Answer frame:

Cause: the list counts structural changes; the iterator notices the count changed behind its back and fails fast.

Not about threads: one thread is enough to trigger it.

Fixes: Iterator.remove, removeIf, or build a new filtered list.

Sample spoken answer:

"A for-each loop uses an iterator under the hood. ArrayList keeps a modification count, and the iterator remembers the value it started with. If I call list.remove inside the loop, the count changes without the iterator knowing, so on the next step it notices and throws ConcurrentModificationException. It's a fail-fast check, and despite the name it happens in a single thread. The safe ways are to use the iterator's own remove method, which keeps the two in sync, or, much cleaner since Java 8, removeIf with a condition. If I'd rather not change the original list, I stream and filter into a new one. And if many threads really do share the collection, I'd use a concurrent collection instead of relying on this check, because fail-fast behaviour is only best effort."

Code:
List<String> names = new ArrayList<>(List.of("ana", "bob", "al"));

// Throws ConcurrentModificationException
// for (String n : names) if (n.startsWith("a")) names.remove(n);

Iterator<String> it = names.iterator();
while (it.hasNext()) if (it.next().startsWith("a")) it.remove();

// Or, simpler
names.removeIf(n -> n.startsWith("a"));
Red flag to avoid:

Saying the exception only happens with multiple threads, or fixing it with a try-catch.

They may ask next:
  • Would CopyOnWriteArrayList throw here? What does it cost you?
  • Why can't you rely on this exception to detect bugs between threads?
  • Why does removing the second-to-last element in that loop not throw at all?
Say it in 60 seconds
Medium Coding round Mid-level Practice question

14. Build a simple LRU cache in Java that holds at most N entries. What would you use?

What the interviewer is really testing:
Whether you know the collections library well enough to avoid reinventing it, and whether you can explain the plain version if asked.
Answer frame:

Shortcut: LinkedHashMap in access order, overriding removeEldestEntry.

By hand: a HashMap for lookup plus a doubly linked list for recency, both constant time.

Limits: not thread-safe; say how you'd make it safe.

Sample spoken answer:

"The quickest correct answer is LinkedHashMap. It keeps a linked list through its entries, and if I pass true for access order in the constructor, every get moves that entry to the end. So the head of the list is always the least recently used. Then I override removeEldestEntry to return true when the size goes over my limit, and the map drops the least recently used entry after each insert. That's a working LRU cache in a few lines. If the interviewer wants it by hand, I'd keep a HashMap from key to node plus a doubly linked list: get moves the node to the front, put adds at the front, and when full I drop the tail. Both give constant-time operations. Neither version is thread-safe, so for shared use I'd wrap it with a lock or use a proper caching library."

Code:
class LruCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;

    LruCache(int capacity) {
        super(16, 0.75f, true); // true = access order
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }
}
Red flag to avoid:

Scanning a list to find the oldest entry on every call, which makes each operation linear.

They may ask next:
  • Can you write the HashMap plus doubly linked list version without LinkedHashMap?
  • How would you make this cache safe for many threads?
Say it in 60 seconds

Exceptions & Generics 3 questions

Easy Technical round Fresher, Mid-level Practice question

15. What is the difference between checked and unchecked exceptions, and how do you decide which to throw?

What the interviewer is really testing:
Whether you know the hierarchy and have an opinion on when each is right, instead of reciting definitions.
Answer frame:

Checked: subclasses of Exception outside RuntimeException; the compiler makes you catch or declare them.

Unchecked: RuntimeException and its subclasses, plus Error; not enforced by the compiler.

Choice: checked when the caller can reasonably recover; unchecked for programming mistakes.

Sample spoken answer:

"Checked exceptions are the ones the compiler makes me deal with: anything that extends Exception but not RuntimeException, like IOException. I either catch it or declare it with throws. Unchecked exceptions extend RuntimeException, like NullPointerException or IllegalArgumentException, and the compiler doesn't force anything. Errors, like OutOfMemoryError, are also unchecked and usually mean the JVM itself is in trouble, so I don't catch them. For my own code, I throw a checked exception when the caller can realistically do something about it, like retry or ask for a different file. I throw an unchecked one when it's a bug or a broken precondition, like a null argument, where the right fix is to change the code, not catch it. Many teams lean unchecked because checked exceptions don't fit well with lambdas and streams."

Red flag to avoid:

Saying unchecked exceptions can't be caught, or catching Throwable as a habit.

They may ask next:
  • Why is catch (Exception e) with an empty block dangerous?
  • How do you deal with a checked exception thrown inside a lambda in a stream?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

16. Does a finally block always run? And what does try-with-resources give you over finally?

What the interviewer is really testing:
Whether you know the edge cases of finally and why try-with-resources is the safe default for anything that must be closed.
Answer frame:

Almost always: finally runs after a return or an exception, but not if the JVM exits or dies first.

Trap: returning or throwing from finally hides the original exception.

try-with-resources: closes every AutoCloseable in reverse order and keeps close errors as suppressed exceptions.

Sample spoken answer:

"Finally runs whether the try block finishes normally, returns early or throws, which is why it's used for clean-up. It won't run if the JVM stops first, like System.exit, a crash, or the process being killed, or if the try block never finishes. One trap: if finally has its own return, or throws, it silently replaces whatever exception was on its way out, so I never return from finally. For closing things like streams, connections or locks that implement AutoCloseable, I use try-with-resources. It closes each resource automatically, in the reverse order they were opened, even when an exception is thrown. And if close itself throws while another exception is already in flight, the original stays the main exception and the close failure is attached as a suppressed one, so nothing is lost. The old hand-written finally version got that wrong constantly."

Code:
try (var in = Files.newBufferedReader(path);
     var out = Files.newBufferedWriter(target)) {
    String line;
    while ((line = in.readLine()) != null) out.write(line.toUpperCase() + "\n");
} // out closed first, then in, even on exception
Red flag to avoid:

Saying finally runs no matter what, or returning a value from inside finally.

They may ask next:
  • What does getSuppressed return, and when would you look at it?
  • What will a method return if its try block returns 1 and its finally block returns 2?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

17. What is type erasure in Java generics, and what can't you do because of it?

What the interviewer is really testing:
Whether you understand that generics are a compile-time feature, and can explain the real limits and workarounds that follow from it.
Answer frame:

Erasure: the compiler checks generic types, then removes them; at runtime a List<String> is just a List.

Limits: no new T(), no runtime check that an object is a List<String>, no generic arrays, no overloads that differ only by type argument.

Workarounds: pass a Class<T> token; use bounded wildcards for flexible APIs.

Sample spoken answer:

"Generics in Java are checked by the compiler and then erased, which kept old compiled code working when generics arrived. At runtime, a List<String> and a List<Integer> are the same class; the type argument is gone and the compiler inserts casts where values come out. That leads to real limits. I can't write new T() because the JVM doesn't know what T is. I can't ask at runtime whether an object is a List<String>. I can't create a generic array like new T[10] directly. And I can't have two overloads that take List<String> and List<Integer>, because after erasure they have the same signature. The usual workaround is to pass a Class<T> in, so the method can create instances or check types. For flexible APIs I use wildcards: extends when I only read from a collection, super when I only write into it."

Code:
// Does not compile: same erasure
// void save(List<String> xs) {}
// void save(List<Integer> xs) {}

<T> T create(Class<T> type) throws ReflectiveOperationException {
    return type.getDeclaredConstructor().newInstance();
}

double sum(List<? extends Number> xs) {
    double total = 0;
    for (Number n : xs) total += n.doubleValue();
    return total;
}
Red flag to avoid:

Saying generic type information is fully available at runtime, or not knowing why new T() fails.

They may ask next:
  • Why can a List<Integer> not be passed where a List<Number> is expected?
  • When would you use List<? super Integer> as a parameter type?
  • What is a raw type, and why does the compiler warn about it?
Say it in 60 seconds

Concurrency 5 questions

Medium Technical round Mid-level Practice question

18. What does volatile guarantee, and how is it different from synchronized? Would volatile make count++ safe?

What the interviewer is really testing:
Whether you separate visibility from atomicity, which is the most common gap in Java concurrency answers.
Answer frame:

volatile: every read sees the latest write; no reordering across it; no locking and no atomicity.

synchronized: one thread at a time in the block, plus visibility of changes when the lock is released and taken.

count++: read, add, write; volatile doesn't make that atomic; use AtomicInteger or a lock.

Sample spoken answer:

"volatile is about visibility. Without it, one thread can keep reading a stale copy of a field and never see another thread's update, which is how a stop flag loop can spin forever. Marking the field volatile means every read sees the most recent write, and the JVM won't reorder other reads and writes around it in harmful ways. But it doesn't lock anything. synchronized gives mutual exclusion, so only one thread runs the block at a time for a given lock, and it also gives visibility: whatever one thread wrote before releasing the lock, the next thread sees after taking it. count++ is three steps: read, add one, write back. Two threads can both read five and both write six, even with volatile. So for a counter I use AtomicInteger, which does it atomically with compare-and-swap, or a lock."

Code:
class Worker implements Runnable {
    private volatile boolean running = true;     // visibility is enough
    private final AtomicInteger done = new AtomicInteger(); // needs atomicity

    public void run() {
        while (running) { doTask(); done.incrementAndGet(); }
    }
    void stop() { running = false; }
    void doTask() { }
}
Red flag to avoid:

Saying volatile makes operations atomic or thread-safe in general.

They may ask next:
  • What does happens-before mean in the Java memory model?
  • When would you choose a ReentrantLock over synchronized?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

19. What is a deadlock? Show how one happens in Java and tell me how you'd prevent or detect it.

What the interviewer is really testing:
Whether you can produce the classic two-lock example and know practical prevention and diagnosis, not only the textbook conditions.
Answer frame:

What: two or more threads each hold a lock and wait for one the other holds, forever.

Prevent: always take locks in one global order, hold them briefly, or use tryLock with a timeout.

Detect: take a thread dump; the JVM reports Java-level deadlocks and the locks involved.

Sample spoken answer:

"A deadlock is when threads wait on each other in a cycle and none can move. The textbook case is a money transfer. Thread one transfers from A to B, so it locks A then B. Thread two transfers from B to A, so it locks B then A. If each grabs its first lock at the same moment, both wait forever for the second. The most reliable fix is a consistent lock order: always lock the account with the smaller ID first, whatever the direction of the transfer. Other tools are keeping locked sections short, never calling unknown code while holding a lock, and using ReentrantLock.tryLock with a timeout so a thread can back off. To detect one in a running service, I take a thread dump with jstack or jcmd. It reports Java-level deadlocks directly and shows which thread holds which lock."

Code:
void transfer(Account from, Account to, long amount) {
    Account first  = from.id() < to.id() ? from : to;   // one global order
    Account second = first == from ? to : from;
    synchronized (first) {
        synchronized (second) {
            from.debit(amount);
            to.credit(amount);
        }
    }
}
Red flag to avoid:

Only listing the four textbook conditions without a concrete example or a practical fix.

They may ask next:
  • What is the difference between a deadlock and a livelock?
  • How would you handle two accounts with the same ID in that ordering rule?
  • Can a deadlock happen with only one lock?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

20. Why use an ExecutorService instead of creating a new Thread for each task, and how do you configure one safely?

What the interviewer is really testing:
Whether you understand thread cost, pool sizing and the risks hidden in the default factory methods.
Answer frame:

Why pools: threads are expensive; a pool reuses them and caps how many run at once.

Sizing: near the core count for CPU work; more for tasks that mostly wait on I/O; bounded queue plus a rejection policy.

Hygiene: name threads, check Futures for exceptions, shut the pool down.

Sample spoken answer:

"Creating a thread per task is costly, and under load it can create thousands of threads and bring the service down. An ExecutorService keeps a pool of reusable threads and a queue of waiting tasks, so I control how much runs at once. For CPU-heavy work I size the pool close to the number of cores. For tasks that mostly wait on I/O I can go larger. I'm careful with the Executors shortcuts: newFixedThreadPool uses an unbounded queue, so if tasks arrive faster than they finish, the queue grows until memory runs out. In production I'd rather build a ThreadPoolExecutor with a bounded queue, named threads and a clear rejection policy. Two more habits: if I use submit, an exception is stored in the Future and is lost if nobody calls get, and I always shut the pool down on exit."

Code:
AtomicInteger n = new AtomicInteger();
ThreadFactory named = r -> new Thread(r, "report-worker-" + n.incrementAndGet());

ExecutorService pool = new ThreadPoolExecutor(
    8, 8, 0L, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(500),              // bounded queue
    named,                                      // readable names in thread dumps
    new ThreadPoolExecutor.CallerRunsPolicy()); // back-pressure when full

Future<Report> f = pool.submit(() -> buildReport(id));
Report r = f.get(5, TimeUnit.SECONDS); // a task failure arrives as ExecutionException

pool.shutdown();
Red flag to avoid:

Using Executors.newFixedThreadPool everywhere without knowing its queue is unbounded, or never shutting a pool down.

They may ask next:
  • What is the difference between shutdown and shutdownNow?
  • When would you use CompletableFuture instead of a plain Future?
  • How do virtual threads in Java 21 change the way you'd run lots of blocking tasks?
Say it in 60 seconds
Hard Coding round Mid-level, Senior Practice question

21. Write a thread-safe, lazily created singleton. Why does double-checked locking need volatile?

What the interviewer is really testing:
Whether you know the memory-model reason behind volatile here and know the simpler idioms that avoid the problem.
Answer frame:

Double-checked locking: check, lock, check again; the field must be volatile.

Why volatile: without it another thread can see the reference before the constructor's writes are visible.

Simpler options: the holder class idiom, or an enum singleton.

Sample spoken answer:

"With double-checked locking I check whether the instance is null, and only if it is do I lock and check again before creating it, so the lock is taken only on the first calls. The field must be volatile. Creating an object is really several steps: allocate memory, run the constructor, publish the reference. Without volatile, those steps can be reordered from another thread's point of view, so that thread might see a non-null reference to an object whose fields aren't set yet, skip the lock and use a half-built object. volatile forbids that. Honestly, I'd rather not write it at all. The holder idiom is lazy and thread-safe because the JVM initialises a class only once, when it's first used. And if I need protection against serialization and reflection creating copies, an enum with one value is the simplest singleton."

Code:
class Config {
    private static volatile Config instance;
    private Config() { }

    static Config get() {
        Config c = instance;
        if (c == null) {
            synchronized (Config.class) {
                c = instance;
                if (c == null) instance = c = new Config();
            }
        }
        return c;
    }
}

// Simpler: lazy and thread-safe through class initialisation
class Settings {
    private Settings() { }
    private static class Holder { static final Settings INSTANCE = new Settings(); }
    static Settings get() { return Holder.INSTANCE; }
}
Red flag to avoid:

Writing double-checked locking without volatile, or synchronizing the whole getter without knowing its cost.

They may ask next:
  • How can reflection or serialization break a normal singleton, and why doesn't that affect an enum?
  • Why are singletons often considered a problem for testing?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

22. In code review you see a plain HashMap in a singleton service that every request thread reads and writes. The tests pass. What do you do?

What the interviewer is really testing:
Whether you recognise a real concurrency risk that tests won't catch, and raise it constructively with a concrete fix.
Answer frame:

Name the risk: concurrent writes can lose updates and corrupt the map's internal structure.

Why tests pass: unit tests are single-threaded; the race shows only under real load.

Fix and tone: suggest ConcurrentHashMap with atomic methods, or no shared state; explain, don't just block.

Sample spoken answer:

"I wouldn't approve it as is, even though the tests are green. HashMap isn't thread-safe. If two request threads write at once, updates can be lost, and a write during a resize can leave the map in a broken state that's very hard to debug later. The tests pass because they run on one thread, so they can't show the problem. I'd leave a clear comment explaining the risk, and suggest ConcurrentHashMap. I'd also look at how it's used: if the code does a get and then a put, I'd suggest computeIfAbsent or merge so the whole update is atomic. And I'd ask whether the state needs to be shared at all; maybe it belongs in a proper cache or per request. I'd offer to pair on it, because the goal is a correct service and a colleague who spots it next time."

Red flag to avoid:

Approving it because the tests pass, or rejecting it with no explanation of the risk.

They may ask next:
  • How could you write a test that has a real chance of catching this race?
  • What if the author says the map is only written at startup and only read afterwards?
Say it in 60 seconds

Modern Java 5 questions

Easy Technical round Fresher, Mid-level Practice question

23. What is a functional interface, and how do lambdas and method references relate to it?

What the interviewer is really testing:
Whether you know a lambda is just an implementation of an interface with one abstract method, and can name the everyday ones in java.util.function.
Answer frame:

Functional interface: exactly one abstract method; default and static methods don't count; @FunctionalInterface makes the compiler check it.

Lambda: a short way to implement that one method; the interface it's assigned to decides what it means.

Everyday ones: Function, Predicate, Supplier, Consumer; a method reference like String::length is a shorter lambda.

Sample spoken answer:

"A functional interface is an interface with exactly one abstract method. Default and static methods don't count, so Comparator is one even though it has lots of default methods. Runnable and Callable are older examples, and java.util.function adds the everyday ones: Function takes a value and returns one, Predicate returns a boolean, Supplier takes nothing and returns something, and Consumer takes something and returns nothing. A lambda is just a compact way to implement that single method. On its own a lambda has no type; the interface it's assigned to decides what it means. A method reference like String::length is a shorter lambda when all I do is call an existing method. On my own interfaces I add @FunctionalInterface, so the compiler complains if someone adds a second abstract method. And a lambda can only use local variables that are effectively final."

Code:
Predicate<String> blank = s -> s.isBlank();
Function<String, Integer> length = String::length; // method reference
Supplier<List<String>> fresh = ArrayList::new;
Consumer<String> log = System.out::println;

@FunctionalInterface
interface RetryRule { boolean shouldRetry(int attempt); }

RetryRule upToThree = attempt -> attempt < 3;
Red flag to avoid:

Saying a functional interface can't have default methods, or not being able to name any interface from java.util.function.

They may ask next:
  • Why must a local variable used inside a lambda be effectively final?
  • How is a lambda different from an anonymous inner class, for example in what this refers to?
  • When would you use IntPredicate instead of Predicate<Integer>?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

24. Given a list of words, use streams to count how many words there are of each length. Then explain what's lazy about streams.

What the interviewer is really testing:
Whether you can write idiomatic stream code with collectors and understand intermediate versus terminal operations.
Answer frame:

Code: groupingBy with a counting downstream collector.

Laziness: intermediate steps like filter and map only describe work; nothing runs until a terminal operation.

Rules: a stream is used once; lambdas shouldn't change shared state.

Sample spoken answer:

"I'd stream the list and collect with groupingBy, using the word length as the key and Collectors.counting as the downstream collector. That gives a map from length to how many words have it. If I want the lengths sorted, I pass a TreeMap supplier. On laziness: operations like filter, map and sorted are intermediate. They just build a pipeline and return a new stream; nothing actually runs. The work happens only when a terminal operation like collect, count or forEach is called, and then each element flows through the whole pipeline. That's why a findFirst after a filter can stop early without touching the rest of the list. Two rules I keep: a stream can be consumed only once, and the lambdas inside shouldn't modify outside variables or collections. If I need a result, I let a collector build it."

Code:
List<String> words = List.of("java", "heap", "gc", "stream", "jvm");

Map<Integer, Long> byLength = words.stream()
    .collect(Collectors.groupingBy(String::length, Collectors.counting()));
// {2=1, 3=1, 4=2, 6=1}

Map<Integer, Long> sorted = words.stream()
    .collect(Collectors.groupingBy(String::length, TreeMap::new, Collectors.counting()));
Red flag to avoid:

Adding to an outside list from inside forEach instead of collecting, or not knowing that nothing runs without a terminal operation.

They may ask next:
  • What is the difference between map and flatMap?
  • What is the difference between reduce and collect?
  • How would you return the longest word in each group instead of the count?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

25. What problem does Optional solve, and where should you not use it?

What the interviewer is really testing:
Whether you use Optional as a clear return type for 'maybe no result', rather than scattering it everywhere or calling get blindly.
Answer frame:

Purpose: a return type that says 'there may be no value' so the caller must handle it.

Use well: map, filter, orElse, orElseGet, orElseThrow; avoid isPresent followed by get.

Avoid: fields, method parameters, collections of Optionals, and Optional of a collection.

Sample spoken answer:

"Optional is meant for return types where 'no result' is normal, like findUserByEmail. Returning Optional makes that visible in the signature, so the caller has to decide what happens when it's empty instead of getting a NullPointerException later. I use it with map and filter to chain steps, and finish with orElse, orElseGet or orElseThrow. There's a subtle difference there: orElse always evaluates its argument, even when a value is present, so if the default is expensive, like a database call, I use orElseGet with a lambda. What I avoid: calling get without checking, because it throws when empty, and using Optional for fields or parameters, where it adds overhead and noise. For collections I return an empty list, never an Optional of a list. And an Optional variable itself should never be null."

Code:
String city = repo.findByEmail(email)          // Optional<User>
    .map(User::address)
    .map(Address::city)
    .orElse("Unknown");

User u = repo.findByEmail(email)
    .orElseThrow(() -> new NotFoundException(email));

Config c = cache.find(key).orElseGet(() -> loadFromDb(key)); // lazy default
Red flag to avoid:

Using isPresent followed by get everywhere, or putting Optional on every field and parameter.

They may ask next:
  • What is the difference between Optional.of and Optional.ofNullable?
  • Why is Optional not Serializable, and what does that tell you about its intended use?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

26. What are records in Java, and what do they give you over a normal class?

What the interviewer is really testing:
Whether you know what a record generates, where its limits are, and when to use one.
Answer frame:

What: a compact, implicitly final class for carrying data; standard since Java 16.

Generated: private final fields, a canonical constructor, accessors, equals, hashCode and toString.

Limits: can't extend a class, fields can't be reassigned, immutability is shallow; validate in a compact constructor.

Sample spoken answer:

"A record is a class whose job is to carry data. I write record Point(int x, int y) and Java generates private final fields, a constructor that takes both values, accessor methods named x() and y(), and equals, hashCode and toString based on the fields. So a data class that used to take forty lines of boilerplate is one line, and equals and hashCode can't drift out of sync. Records became a standard feature in Java 16. They're implicitly final, can't extend another class, but can implement interfaces and have extra methods. If I need validation, I add a compact constructor. One thing to watch: they're only shallowly immutable. If a component is a List, the caller can still change that list unless I copy it in the constructor. I use them for DTOs, keys in maps and results returned from methods."

Code:
record Money(long amountInCents, String currency) {
    Money {                                   // compact constructor
        if (amountInCents < 0) throw new IllegalArgumentException("negative");
        Objects.requireNonNull(currency);
    }
    Money plus(Money other) {
        if (!currency.equals(other.currency)) throw new IllegalArgumentException("currency mismatch");
        return new Money(amountInCents + other.amountInCents, currency);
    }
}
Red flag to avoid:

Saying records are fully immutable no matter what they contain, or that accessors are named getX().

They may ask next:
  • Would you use a record as a JPA entity? Why or why not?
  • How do you stop a record that holds a List from being changed from outside?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

27. A teammate wants to switch every stream in the service to parallelStream to make it faster. How do you respond?

What the interviewer is really testing:
Whether you know when parallel streams help and when they hurt, and can push back with evidence instead of opinion.
Answer frame:

Shared pool: parallel streams run on the common ForkJoinPool, shared across the whole application.

When it helps: large data, CPU-heavy work, no blocking, easy-to-split sources, no shared mutable state.

Response: measure one real case, keep it where it wins, not everywhere.

Sample spoken answer:

"I'd say it isn't a free speed-up, and suggest we measure instead of switching everything. Parallel streams split the work across the common ForkJoinPool, which the whole application shares. In a web service that already handles many requests at once, the CPU is often busy anyway, so splitting each request's work mostly adds overhead. For small collections, the cost of splitting and merging is bigger than the work itself. If a stream does blocking calls, like HTTP or database calls, it can tie up the shared pool and slow down unrelated code. And any lambda that touches shared state becomes a race. Where it does help is large, CPU-heavy work on data that splits easily, like an ArrayList. So I'd pick the one or two pipelines we think are slow, benchmark sequential against parallel, and switch only where the numbers show a clear win."

Red flag to avoid:

Agreeing to make everything parallel, or refusing flatly without giving the reasons or offering to measure.

They may ask next:
  • How would you benchmark this fairly, given JIT warm-up?
  • What happens to ordering when you use forEach on a parallel stream?
Say it in 60 seconds

Real Work 3 questions

Hard Behavioral round Mid-level, Senior Practice question

28. Tell me about a concurrency bug you found in a Java service. How did you track it down?

What the interviewer is really testing:
Whether you have debugged real threading problems, which rarely reproduce on demand, and whether you fixed the cause rather than the symptom.
Answer frame:

Symptom: what went wrong, and why it was intermittent.

Hunt: how you narrowed it down: logs, thread dumps, a stress test.

Fix and lesson: the root cause, the fix and what you changed so it doesn't recur.

Sample spoken answer:

"At my last company a reporting service sometimes produced wrong dates, and now and then threw a NumberFormatException, but never in testing. It only showed up under load, which already pointed at threads. I went through the classes on that path and found a static SimpleDateFormat shared by every request. SimpleDateFormat keeps parsing state inside the object, so two threads parsing at once corrupt each other. To prove it, I wrote a small test that parsed dates from many threads at the same time, and it failed within seconds. The fix was to switch to DateTimeFormatter from java.time, which is immutable and safe to share. The test then passed every run. Afterwards I added a check to our code review list for mutable objects in static fields, and we found two more similar cases."

Red flag to avoid:

A story where the fix was adding synchronized everywhere or a retry, without ever naming the root cause.

They may ask next:
  • How would you reproduce a race condition that only appears under production load?
  • Why is DateTimeFormatter safe to share when SimpleDateFormat isn't?
Say it in 60 seconds
Medium Behavioral round Fresher, Mid-level Practice question

29. Tell me about a time you made slow Java code noticeably faster. How did you know where the time went?

What the interviewer is really testing:
Whether you measure before optimising and understand the data structure choices that usually cause slow Java code.
Answer frame:

Measure first: profiler or timing, not guessing.

Cause: the specific hot spot and why it was slow.

Result: the change, the before and after, and how you proved nothing broke.

Sample spoken answer:

"At my last company, a nightly job that matched incoming records against an existing list was taking far too long. Instead of guessing, I ran it with a profiler, and almost all the time was inside a loop that called list.contains for every record. On a list with hundreds of thousands of items that's a linear scan each time, so the whole job was quadratic. I built a HashSet of the keys once before the loop, so each lookup became constant time on average. The same profile showed String.matches inside the loop, which compiles the regex again on every call, so I compiled one Pattern into a constant and reused it. The job went from most of an hour to a couple of minutes. I compared the output files from the old and new versions to make sure the results were identical."

Red flag to avoid:

Optimising by instinct with no measurement, or a result with no before and after.

They may ask next:
  • What profiler or tooling would you use on a live service without slowing it down much?
  • When is it not worth optimising code that looks slow?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

30. Have you moved a codebase to a newer Java version? What broke, and how did you manage the risk?

What the interviewer is really testing:
Whether you have handled real upgrade work: removed modules, library compatibility and a staged rollout, rather than just changing a version number.
Answer frame:

Why: the reason for the upgrade, such as support ending or needed features.

What broke: removed JDK modules, libraries using JDK internals, build plugins.

Risk control: dependency upgrades first, full test run, staged rollout, watch GC and latency.

Sample spoken answer:

"At my last company I led moving a group of services from Java 8 to Java 17, mainly because Java 8 support was ending for us and we wanted records and better garbage collectors. The first breakages were at compile time: we used JAXB for XML, and those modules were deprecated in Java 9 and removed from the JDK in Java 11, so we had to add them back as normal dependencies. Then a couple of old libraries broke at runtime because they reached into JDK internals with reflection, which newer versions block by default. We upgraded those libraries first, one pull request at a time, while still on Java 8. After that the switch itself was small. We rolled it out to one service at a time, compared GC pauses and latency with the old version, and kept the old build ready to roll back. None of them needed it."

Red flag to avoid:

Claiming the upgrade needed no changes at all, or upgrading everything in one release with no rollback plan.

They may ask next:
  • Which newer Java features did your team actually start using after the upgrade?
  • How would you convince a team that fears the upgrade to do it?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

The questions above are the prep. The call has ten more.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.

Download ClapAssist with 10 free minutes
Mac and Windows · Stays out of screen share · No card