Skip to content
CalliCoder

Core Java Interview Questions with Answers

Interview Questions 13 min read

The topics Java interviews actually probe — equals and hashCode, the memory model, collections, exceptions, generics — with what each question is really testing and the follow-up that separates a memorised answer from an understood one.

Most Java interview lists are answer keys. That is the wrong shape for preparation, because interviewers rarely stop at the first answer. They ask the follow-up, and the follow-up is where a memorised definition falls apart.

This is organised by the six areas that come up most, and for each one: what is being tested, the answer that satisfies it, and the follow-up you should expect.

Written against Java 17.

1. equals and hashCode

What is being tested: whether you understand that collections depend on a contract, not on intuition.

The contract has two halves. If a.equals(b) then a.hashCode() == b.hashCode(), mandatory. The converse is not required: two unequal objects may share a hash code, and that is a collision, not a bug.

Override one without the other and hash-based collections break. Given equals but not hashCode, two equal objects land in different buckets, so a HashMap stores both and get finds neither. Given hashCode but not equals, two identical keys are different keys.

public record Point(int x, int y) { }        // both generated, consistent

A record is the correct answer in modern Java, and saying so demonstrates more than writing the methods out.

The follow-up: what happens if you mutate a key after putting it in a HashMap? The entry is filed under the old hash code and becomes unreachable, still counted by size(), invisible to get. Keys must be effectively immutable. Being able to say that is the difference between having read the contract and having used it.

The other follow-up: is it legal for hashCode to return a constant? Yes, and it is correct but useless: every key collides, and the map degrades to a linked list. Since Java 8 a bucket past eight entries becomes a red-black tree, bounding the damage at O(log n).

2. The memory model

What is being tested: whether “thread safety” means anything specific to you.

A race condition is two problems, and conflating them is the usual failure:

Atomicity. count++ is a read, an add and a write. Two threads can read 41, both compute 42, and one increment is lost.

Visibility. A thread is not guaranteed to observe another thread’s write at all. The memory model permits values to sit in registers or cache lines; without a happens-before relationship there is no promise the reader ever sees it. A loop polling a plain boolean flag can spin forever, and will under optimisation.

volatile fixes visibility and not atomicity. It is correct for a flag written once and read often, and wrong for a counter.

private volatile boolean running = true;    // correct
private volatile int count = 0;             // count++ is still not atomic

The follow-up: how would you make a counter thread-safe? Three answers, in ascending order of what they demonstrate: AtomicInteger for a single variable; synchronized or a ReentrantLock when several fields change together; or do not share mutable state at all, an immutable value has no race. The third answer is the one senior interviewers are listening for.

The other follow-up: what does synchronized guarantee besides mutual exclusion? It establishes happens-before: everything written before releasing a monitor is visible to the next thread that acquires it. That is why a correctly synchronised counter needs no volatile.

3. Collections

What is being tested: whether you choose by behaviour or by habit.

The distinctions worth having ready:

  • ArrayList vs LinkedList. ArrayList for nearly everything. LinkedList’s O(1) insertion requires an iterator already at the position; get(n) is O(n), and its per-node overhead defeats cache locality. ArrayDeque for a queue or stack.
  • HashMap vs LinkedHashMap vs TreeMap. No order, insertion (or access) order, sorted order. LinkedHashMap with removeEldestEntry is an LRU cache in about five lines.
  • HashMap vs Hashtable. Hashtable is legacy: synchronised on every method, no nulls. The modern answer is ConcurrentHashMap, and knowing why matters more, see the follow-up.
  • Fail-fast vs weakly consistent iterators. ArrayList’s iterator throws ConcurrentModificationException when the list changes underneath it. ConcurrentHashMap’s never throws and may or may not reflect concurrent updates.

The follow-up: why is ConcurrentHashMap better than a synchronized map? Not primarily performance. Collections.synchronizedMap serialises all access behind one lock, and, crucially — compound operations are still not atomic:

Integer n = map.get(k);
map.put(k, n == null ? 1 : n + 1);      // broken under concurrency, synchronized or not
map.merge(k, 1, Integer::sum);          // atomic on ConcurrentHashMap

Being able to name that gap is a much stronger answer than “it locks per segment”.

The other follow-up: what happens if you remove from a list inside a for-each? ConcurrentModificationException, usually. Removing the second-to-last element exits the loop early without throwing, because the iterator’s cursor now equals the reduced size. That is worth knowing; it is a real bug that produces no exception.

4. Exceptions

What is being tested: whether you have thought about error handling as design rather than syntax.

Checked exceptions extend Exception and must be declared or caught; unchecked extend RuntimeException and need not be. Error is for conditions an application should not attempt to handle, OutOfMemoryError, StackOverflowError.

The design question behind it: a checked exception says the caller can plausibly recover. A file that might not exist, a network that might be down. An unchecked exception says a programmer made a mistake, a null where one was not allowed, an index out of range.

The follow-up: what does finally do if the try block returns? It runs before the value is handed back. And a return inside finally replaces the one from try, discarding it, including discarding a thrown exception:

static int broken() {
    try {
        throw new IllegalStateException("lost");
    } finally {
        return 1;        // swallows the exception entirely
    }
}

Never return from finally. Interviewers ask this because the answer is unintuitive and the mistake is real.

The other follow-up: what is try-with-resources for? Deterministic closing, and it handles the case a hand-written finally usually gets wrong, if both the body and close() throw, the exception from close() is attached as a suppressed exception rather than replacing the original. A manual finally { stream.close(); } loses the first one.

5. Generics

What is being tested: whether you know that generics are a compile-time construct.

Type erasure: List<String> and List<Integer> are the same class at runtime. The compiler checks types and inserts casts, then discards the parameter. Hence:

List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
a.getClass() == b.getClass();          // true

// and these are impossible
new T[10];                             // cannot create an array of a type parameter
if (obj instanceof List<String>) { }   // cannot test an erased parameter

The follow-up: what does ? extends mean, and why can I not add to such a list?

List<? extends Number> numbers = new ArrayList<Integer>();
numbers.add(1);          // compile error
Number n = numbers.get(0);  // fine

? extends Number means “some specific subtype of Number, unknown here”. Since it might be List<Double>, adding an Integer cannot be proven safe, so reading is allowed and writing is not. ? super Integer is the mirror: you can add an Integer, and reads only give you Object.

The mnemonic is PECS, Producer Extends, Consumer Super. Quoting it is fine; explaining why is better.

6. String

What is being tested: whether you understand immutability and interning.

String is immutable, so every apparent modification allocates. Concatenating in a loop with + allocates each iteration and is O(n²) overall; StringBuilder is O(n).

String a = "hello";
String b = "hello";
String c = new String("hello");

a == b;              // true  — both refer to the same interned literal
a == c;              // false — new String allocates
a.equals(c);         // true

Literals are interned in a shared pool, so identical literals are one object. new String(...) deliberately is not.

The follow-up: why must String be immutable? Three reasons worth having: the string pool would be unsafe to share if strings could change; hashCode is cached, which requires the contents to be fixed; and strings are used as security-sensitive values, a mutable class name or file path could be changed after a check and before its use.

How to answer

A pattern that consistently works better than reciting:

  1. Answer the question directly, in one or two sentences.
  2. Say why it is that way, the constraint that forces the design.
  3. Name the failure mode it prevents or causes.
  4. Stop. Volunteering everything you know invites a question you cannot answer, and buries the part they were listening for.

And say “I do not know, but I would find out by…” when you do not know. Interviewers are calibrating what you do at the edge of your knowledge, and a confident wrong answer is far worse than a named gap.

Frequently asked questions

Which topics come up most often?

equals/hashCode, collections, the memory model and concurrency, exceptions, generics and String. Everything else tends to be a variation or something role-specific.

Do I need to memorise complexity tables?

Not as tables. You need ArrayList versus LinkedList and HashMap versus TreeMap with the reasoning, because the follow-up is always “why”.

Is it acceptable to say I would use a record?

Yes, and it is the better answer for a value class. It shows current knowledge and removes any chance of an inconsistent equals/hashCode.

How much should I say about virtual threads?

Enough to place them: Java 21 makes threads cheap so blocking scales, and it changes nothing about races or deadlocks. Overclaiming here is a common misstep.

What if I do not know an answer?

Say so, then say how you would find out. That is a better signal than guessing, and interviewers are explicitly assessing it.

Are brainteasers still asked?

Rarely, and mostly at companies you can screen out. Expect practical questions about code you would actually write.

Should I ask clarifying questions?

Always, for anything that sounds like a design or coding task. Input size, constraints, whether duplicates or nulls are possible: the assumptions change the answer, and asking demonstrates you know that.

How deep do JVM internals go?

For most roles: heap versus stack, what garbage collection does, what OutOfMemoryError means. Collector tuning specifics come up only where the role involves it.

Do I need to write code on a whiteboard?

Often, and it is usually not about syntax. Narrate your approach, state the complexity, and mention what you would test.

What is the single most common weak answer?

volatile makes it thread-safe.” It addresses visibility only, and the person asking is waiting to see whether you know the difference.

Where should I go next?

Java concurrency basics covers the memory-model section in depth, HashMap covers the collections questions, and the interview guides cover other areas.