Java interview questions — practice with real interview prompts
Reviewed by Mark Dickie · Last updated
Java is a statically typed, object-oriented programming language and runtime platform that compiles source code to bytecode executed by the Java Virtual Machine. For interviews, you should be solid on the core language (generics, inheritance, exceptions, the collections framework), concurrency (threads, locks, java.util.concurrent), JVM internals (garbage collection, classloading, memory model), and commonly used frameworks (Spring, Hibernate). Most Java interviews also include a coding exercise, so fluency with data structures and standard library APIs matters as much as conceptual knowledge.
What does a Java interview typically test?
Interview rounds vary by company, but most cover some combination of the areas below. Knowing where to focus your prep time depends on the role and seniority.
| Area | What comes up | Difficulty range |
|---|---|---|
| Core language | Generics, lambdas, streams, exceptions, access modifiers | 1–3 |
| Collections | HashMap internals, ArrayList vs LinkedList, ConcurrentHashMap | 2–4 |
| Concurrency | Thread lifecycle, synchronized vs ReentrantLock, executors, CompletableFuture | 3–5 |
| JVM internals | Garbage collectors, memory regions, classloading, JIT compilation | 3–5 |
| Frameworks (Spring) | IoC container, bean lifecycle, @Transactional, Spring Boot auto-configuration | 2–4 |
| Design & patterns | Singleton, Factory, Strategy, SOLID principles in Java context | 2–4 |
How should I prepare for a Java coding round?
- Review the collections framework until you can explain
HashMapbucket mechanics and resizing behavior from memory. - Write several threading problems by hand — producer-consumer, rate limiter, parallel merge sort — using
java.util.concurrentclasses rather than rawsynchronizedblocks. - Practice solving LeetCode-style problems in Java with a time limit, paying attention to boxing overhead and
Stringimmutability pitfalls. - Read through a major garbage collector (G1 or ZGC) enough to describe when it pauses and why.
- Build a small Spring Boot application so you can speak to bean scopes, dependency injection, and configuration from direct experience rather than memorized definitions.
The quiz below pulls from these topic areas and lets you check where you stand before sitting down for the real thing.
Key facts
- Tarmac has 95 Java interview questions on this topic, 25 of them on this page, at difficulty 1–5 of 5.
- Tarmac tracked 1,451 job postings asking for Java in August 2026.
- Roles asking for Java advertise a median base salary of £80,000, across 215 job postings as of August 2026.
- Tarmac last reviewed these Java interview questions on 31 August 2026.
At a glance
| Questions | 25 shown · 95 in the bank |
|---|---|
| Difficulty | 1–5 of 5 |
| Formats | Multiple choice, Short answer, Fill in the blank, Multiple answer, Flashcard, Find the bug, True / false, Code output, Ordering |
What you'll review
- strings
- comparable comparator
- try with resources
- collections framework
- hashmap internals
- equals hashcode
- streams
- heap stack
- switch expressions
- checked unchecked
- interfaces
- class loading
- type erasure
- autoboxing
- optional
- concurrent collections
- bounded wildcards
Practice questions
Try one before you open the answer. Pick an option and press Check; it's marked on the spot.
Java/language-basics/strings
Given String a = "hi"; String b = "hi"; String c = new String("hi");, what are the results of a == b and a == c?#
Options
Show answer
a == b is true but a == c is false. String literals are interned into the constant pool, so two identical literals share one object and == (a reference comparison) returns true. new String("hi") forces a separate heap object, so a == c is false. Use .equals() to compare string contents — == only checks whether two references point at the same object.
String literals are interned into the string constant pool, so a and b reference the exact same pooled object and a == b is true. new String("hi") is explicitly told to allocate a fresh object on the heap, so c is a different reference and a == c is false even though the characters match. == compares references, not contents — .equals() is what compares the actual characters and would return true for all three. The trap is that == appears to work for literals because of interning, then silently breaks for strings built at runtime (from input, concatenation, or new), which is why you should always use .equals() to compare string values.
Java/collections/comparable-comparator
In Java, which interface must a class implement to define its own natural ordering (so that a list of its instances can be sorted without passing a separate comparator)?#
Show answer
java.lang.Comparable
Implementing java.lang.Comparable<T> and overriding its single method compareTo(T o) establishes the natural ordering for a class. Collections.sort (or List.sort) can then sort a list of those objects without needing an external Comparator.
Java/exceptions/try-with-resources
For a resource to be managed by a try-with-resources statement, its class must implement the _____ interface (or its subinterface Closeable), which declares the single _____() method the JVM calls automatically when the try block exits.#
Show answer
For a resource to be managed by a try-with-resources statement, its class must implement the **AutoCloseable** interface (or its subinterface Closeable), which declares the single **close**() method the JVM calls automatically when the try block exits.
Try-with-resources (try (var r = ...) { ... }) automatically calls close() on each resource when the block exits — normally or via an exception — as long as the resource's type implements AutoCloseable (or Closeable, which extends it and narrows the thrown type to IOException). Resources close in reverse order of declaration. A subtle but important detail: if the body throws and close() also throws, the body's exception is the one propagated and the close() exception is attached as a suppressed exception (retrievable via Throwable.getSuppressed()) — the opposite of the old try/finally idiom, where a throwing finally would mask the original error.
Java/collections/collections-framework
Which of the following Java collection implementations allow null as an element (or key, where applicable)?#
Options
Pick every one that applies.
Show answer
ArrayList and HashSet both allow null elements. ArrayList stores null by index position, and HashSet permits a single null because it is backed by a HashMap allowing one null key. Hashtable and ConcurrentHashMap both reject null keys and values — Hashtable throws NullPointerException and ConcurrentHashMap disallows them to avoid ambiguity in concurrent access.
ArrayList stores null references without restriction because it indexes by position. HashSet allows a single null element (backed by a HashMap that allows one null key). Hashtable throws NullPointerException on any null key or value, as does ConcurrentHashMap, which disallows null keys and values to avoid ambiguity in concurrent reads.
Java/collections/comparable-comparator
In Java, what does the Comparable<T> interface require a class to implement, and what do the sign conventions of compareTo(T o) mean?#
Show answer
A class implementing Comparable<T> overrides int compareTo(T o). A negative return value means this should sort before o, zero means they are considered equal for ordering, and a positive value means this sorts after o. The ordering defined by this method is called the class's natural ordering, used automatically by Collections.sort, TreeSet, TreeMap, and similar sorted structures.
Comparable defines the natural ordering inside the class itself. The compareTo return sign maps to before/equal/after and is the single method of the java.lang.Comparable interface.
Java/collections/comparable-comparator
What is the purpose of java.util.Comparator<T> and how is it different from implementing Comparable?#
Show answer
A Comparator<T> is a separate, external object that defines an ordering via int compare(T a, T b). Unlike Comparable, it lets you provide multiple different sort orders for the same type without modifying that type's source code. You pass it to Collections.sort(list, comparator), list.sort(comparator), TreeSet(comparator), etc. Use Comparable for the single natural ordering; use Comparator for alternative orderings or when you cannot change the class.
Comparable embeds the natural ordering inside the class (one method, one order). Comparator is external and can be created in multiple variants to sort the same objects in different ways, even for third-party classes you cannot edit.
Java/collections/hashmap-internals
The Employee class below is used as a key in a HashMap. It violates the equals/hashCode contract. Identify the single buggy line.#
import java.util.Objects;
public class Employee {
private int id;
private String name;
private String email;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee e = (Employee) o;
return id == e.id && Objects.equals(name, e.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name, email);
}
}Show answer
The bug is on line 18.
The equals method considers only id and name when determining equality (line 13), but the hashCode method on line 18 also includes email in the computation via Objects.hash(id, name, email). The equals/hashCode contract requires that two objects for which equals returns true must have the same hash code. Two Employee objects with the same id and name but different email values would be considered equal yet produce different hash codes, breaking the contract. The fix is to remove email from the hashCode computation: return Objects.hash(id, name);.
Java/collections/hashmap-internals
The code below compiles and runs but produces a surprising result when used with a HashMap. Select the option that identifies the bug.#
import java.util.HashMap;
import java.util.Objects;
public class Main {
public static void main(String[] args) {
HashMap<Key, String> map = new HashMap<>();
Key k = new Key("alpha");
map.put(k, "value");
k.setName("beta");
System.out.println(map.get(k));
}
}
class Key {
private String name;
Key(String name) { this.name = name; }
void setName(String name) { this.name = name; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Key)) return false;
return Objects.equals(name, ((Key) o).name);
}
@Override
public int hashCode() { return Objects.hash(name); }
}Options
Show answer
Line 9 (k.setName("beta")) mutates the key's hashCode after it was inserted into the HashMap
After map.put(k, "value") stores the entry, k.setName("beta") changes the name field, which is the sole input to hashCode. The entry was placed in the bucket corresponding to the hash of "alpha", but map.get(k) now computes the hash of "beta" and looks in a different bucket, so it returns null even though the exact same object reference is in the map. The equals and hashCode implementations themselves are internally consistent; the bug is mutating a HashMap key after insertion. Option (b) is not a bug — instanceof is a valid pattern here. Option (c) is incorrect because hashCode correctly uses the same field as equals. Option (d) is irrelevant to the failure.
Java/language-basics/equals-hashcode
You override equals() on a class but forget to override hashCode(). You then use instances as HashMap keys. What goes wrong?#
Options
Show answer
Lookups break: two objects that are equals can produce different hash codes, land in different buckets, and a get() with an equal-but-distinct key returns null. HashMap uses hashCode() to choose a bucket and equals() only within it, and the contract requires equal objects to have equal hash codes. The default identity-based hashCode() violates this, so always override both together using the same fields.
The equals/hashCode contract requires that equal objects return equal hash codes. HashMap first uses hashCode() to pick a bucket, then equals() to find the entry within it. The default Object.hashCode() is identity-based, so two distinct-but-equals objects almost always produce different hash codes, route to different buckets, and a get() with an equal-but-not-same key returns null — the entry is effectively lost. It is not a compile error: the compiler never forces you to override both, which is exactly why this bug is so common. Always override hashCode() whenever you override equals() (and prefer the same fields in both).
Java/functional/streams
A Stream pipeline does no work until a terminal operation runs. Which of these are terminal operations (they trigger execution and consume the stream)? Select all that apply.#
Options
Pick every one that applies.
Show answer
collect, forEach, and count are terminal operations: they trigger the pipeline, produce a result or side effect, and consume the stream so it can't be reused. map and filter are intermediate and lazy — they only describe a transformation and run nothing until a terminal op pulls elements through. A pipeline with no terminal op does zero work. Other terminal ops include reduce, findFirst, anyMatch, and toArray.
Stream operations split into intermediate (lazy, return a new Stream) and terminal (eager, produce a result or side effect and consume the stream). collect, forEach, and count are terminal — they trigger the pipeline and you cannot reuse the stream afterward. map and filter are intermediate: they only describe a transformation and do nothing until a terminal op pulls elements through. The practical consequence of laziness is that a pipeline of map/filter with no terminal op runs zero times — a common 'my forEach printed nothing' confusion — and that intermediate ops are fused and applied element-by-element rather than as separate passes. Other terminal ops include reduce, findFirst, anyMatch, toArray, and min/max.
Java/jvm/heap-stack
Java is strictly pass-by-value: when you pass an object to a method, the method receives a copy of the reference, so reassigning the parameter inside the method does not change which object the caller's variable points to (though mutating the object's fields is visible to the caller).#
Options
Show answer
True — Java is always pass-by-value. For objects, the value copied is the reference, so the parameter and the caller's variable point at the same object: mutating that object's fields is visible to the caller. But reassigning the parameter only rebinds the local copy of the reference, leaving the caller's variable unchanged. That is why a method can never swap two of the caller's variables — the giveaway that Java has no pass-by-reference.
Java is always pass-by-value, with no exceptions. For object types, the value that is copied is the reference itself, not the object — so the parameter and the caller's variable initially point at the same object. Mutating that shared object's state (e.g. list.add(x) or obj.setName(...)) is visible to the caller, which is why people mistakenly call it 'pass-by-reference'. But reassigning the parameter (obj = new Thing()) only rebinds the local copy of the reference; the caller's variable still points at the original object. The tell that it is pass-by-value: a method cannot swap two of the caller's variables or make the caller's variable point somewhere new.
Java/language-basics/switch-expressions
What does this Java program print? (Java 17)#
public class Main {
public static void main(String[] args) {
int day = 6;
String kind = switch (day) {
case 1, 2, 3, 4, 5 -> "weekday";
case 6, 7 -> "weekend";
default -> "unknown";
};
System.out.println(kind);
}
}Options
Show answer
weekend
This is a switch expression with arrow labels. day is 6, which matches case 6, 7 ->, so kind is assigned "weekend" and the program prints weekend. Arrow-form switch has no fall-through — each label runs only its own branch and yields a value, unlike the old colon-and-break statement form where a missing break would fall through to the next case. A multi-value label (case 6, 7) matches either constant. Switch expressions must be exhaustive (hence default), and the whole switch evaluates to a single value you can assign. The 'weekendunknown' option is the fall-through answer you'd fear with colon syntax and no break — but arrow labels never fall through.
Java/exceptions/checked-unchecked
What is the difference between checked and unchecked exceptions in Java, including their class hierarchy and how the compiler treats each?#
Show answer
Checked exceptions are subclasses of Exception (but not RuntimeException), and the compiler enforces them: a method that can throw one must either catch it or declare it in a throws clause, or the code won't compile. They model recoverable, expected conditions outside the program's control — like IOException or SQLException. Unchecked exceptions are subclasses of RuntimeException (themselves under Exception), plus Error. The compiler does not require you to declare or catch them; they model programming bugs that usually shouldn't be caught — like NullPointerException, IllegalArgumentException, or IndexOutOfBoundsException. Error (e.g. OutOfMemoryError, StackOverflowError) sits under Throwable alongside Exception and represents serious JVM-level problems you normally don't handle. The rule of thumb: use checked for conditions a caller can reasonably recover from, unchecked for programming errors.
Checked exceptions extend Exception (excluding RuntimeException) and the compiler forces you to catch or declare them with throws; they model recoverable external conditions like IOException. Unchecked exceptions extend RuntimeException (plus the Error family) and carry no compiler obligation; they model programming bugs like NullPointerException. The whole hierarchy descends from Throwable → Exception/Error. A strong answer names the RuntimeException boundary and the compiler's role; this informs API design — overusing checked exceptions leads to noisy throws chains and swallowed exceptions.
Java/oop/interfaces
When should you choose an interface over an abstract class in Java, and how have default methods blurred the line?#
Show answer
Choose an interface to define a capability or contract that unrelated types can implement, and because a class can implement many interfaces (Java has no multiple class inheritance). Choose an abstract class when you want to share state (instance fields), constructors, and a common implementation across a tight 'is-a' hierarchy — a class can extend only one. Since Java 8, interfaces can carry default methods (concrete implementations) and static methods, so they can supply behavior, not just signatures — which narrowed the gap. But interfaces still cannot hold instance state (only public static final constants) or constructors. Rule of thumb: interface for a role/ability across diverse types, abstract class for shared state and a partial implementation among closely related types.
Interfaces define contracts and support multiple inheritance of type; abstract classes share state, constructors, and partial implementation within a single-inheritance hierarchy. Default methods (Java 8+) let interfaces ship concrete behavior, so the 'interfaces are only signatures' distinction is outdated — but interfaces still can't hold instance fields or constructors, which remains the deciding factor. In modern design, prefer interfaces for flexibility (a type can implement many) and reach for an abstract class only when shared mutable state or constructor logic genuinely belongs in the hierarchy.
Java/jvm/class-loading
Order the phases the JVM performs the first time a class is actively used, from locating the bytecode to the class being ready to run.#
Put these in order
Show answer
The JVM loads a class in a fixed order: first Loading reads the .class bytecode and creates the Class object, then Verification checks the bytecode for safety, then Preparation allocates static fields and sets them to default zero values, then Resolution turns constant-pool symbolic references into direct references, and finally Initialization runs static initializers and assigns static fields their real values. Verification, Preparation, and Resolution together form the linking phase, and initialization is lazy — triggered on first active use.
Class loading follows a fixed sequence: Loading reads the bytecode and produces the Class object; Linking then runs as Verification (bytecode safety checks), Preparation (static fields get memory and default zero values, NOT their declared values yet), and Resolution (constant-pool symbolic references become direct references); finally Initialization runs static initializer blocks and assigns static fields their actual values. The key distinction interviewers probe is Preparation vs Initialization — a static int x = 5 is set to 0 in Preparation and only becomes 5 in Initialization, which is why static state is observable as its zero value during early bootstrapping. Initialization is lazy: it happens on first active use (instantiation, a static access, etc.), not at JVM startup.
Java/collections/collections-framework
Which of the following statements about the Java Collections Framework (Java 17 LTS) are true?#
Options
Pick every one that applies.
Show answer
The true statements are: LinkedHashMap maintains insertion order by default and can switch to access-order; EnumSet uses a bit-vector implementation and is not thread-safe; and TreeSet requires elements to implement Comparable or a Comparator at construction. The false ones: CopyOnWriteArrayList uses a snapshot iterator that never throws ConcurrentModificationException, and PriorityQueue does not return elements in sorted order when iterated — only peek/poll respect heap ordering.
a is true: LinkedHashMap uses a doubly-linked list running through its entries, preserving insertion order by default. The constructor LinkedHashMap(int, float, boolean) with accessOrder=true switches it to access-order (LRU) mode.
b is false: CopyOnWriteArrayList creates a snapshot of the underlying array at iterator-creation time. Its iterator never throws ConcurrentModificationException — it simply reflects the list's state when the iterator was created, even if other threads modify the list concurrently.
c is true: EnumSet is an abstract class whose concrete subclasses (RegularEnumSet for ≤64 elements, JumboEnumSet otherwise) store elements as bit fields in a long[]. Like most non-concurrent collections in java.util, it is not thread-safe.
d is true: TreeSet is a NavigableSet backed by a TreeMap. It relies on natural ordering (elements must implement Comparable) or an explicit Comparator provided via a constructor. If neither is available and elements are not mutually comparable, operations will throw ClassCastException at runtime.
e is false: PriorityQueue is a binary heap. It guarantees that peek()/poll() return the least element (per the ordering), but its Iterator traverses the underlying array in arbitrary heap order — not sorted order.
Correct answers: a, c, d.
Java/generics/type-erasure
Because of type erasure, which of these is legal at runtime in Java generics?#
Options
Show answer
Only list instanceof List<?> is legal. Java generics use type erasure, so type arguments are gone at runtime — instanceof List<String> is a compile error because there is no List<String> type to test. Erasure also bans new T[10] and generic array creation like new ArrayList<String>[5], since arrays are reified and covariant while generics are not. Use a List<T> or an Object[] cast when you need a generic container.
Generics are implemented by erasure: type arguments exist only at compile time and are erased to their bounds (or Object) in the bytecode, so the runtime has no List<String> type to check against. That makes instanceof List<String> a compile error, and only the reifiable unbounded wildcard form instanceof List<?> is permitted. Erasure also forbids new T[10] (the runtime can't know T's reified type) and new ArrayList<String>[5] (generic array creation), because arrays are covariant and reified while generics are not — allowing them would let a String[]-shaped store silently accept the wrong element type and defeat the type system. The usual workaround for generic arrays is an Object[] cast or, better, a List<T>.
Java/language-basics/autoboxing
What does this Java program print?#
public class Main {
public static void main(String[] args) {
Integer a = 100, b = 100;
Integer c = 200, d = 200;
System.out.println(a == b);
System.out.println(c == d);
}
}Options
Show answer
true
false
Autoboxing an int into an Integer goes through Integer.valueOf, which caches boxed values in the range -128 to 127. So a and b (both 100) reference the same cached object and a == b is true, while c and d (both 200) are outside the cache, get separate Integer objects, and c == d is false because == compares references for objects. The fix is to compare wrapper values with .equals() or .intValue(), never ==. This is a notorious trap because small test values (which fall in the cache) make == appear to work, then the same code silently breaks in production once the numbers grow past 127.
Java/functional/optional
What does this program print? Note that expensive() prints a line as a side effect.#
import java.util.Optional;
public class Main {
static String expensive() {
System.out.println("computing");
return "fallback";
}
public static void main(String[] args) {
Optional<String> present = Optional.of("value");
String r = present.orElse(expensive());
System.out.println(r);
}
}Options
Show answer
computing
value
orElse(expensive()) takes a value argument, so expensive() is evaluated eagerly — before orElse runs and regardless of whether the Optional is present. It prints computing and returns "fallback", but because the Optional actually holds "value", that fallback is discarded and r is "value". So the output is computing then value. The trap: the side effect (and any cost) of the default happens even though it's never used. Use orElseGet(Main::expensive) to defer the computation behind a Supplier so it runs only when the Optional is empty. Choosing orElse over orElseGet for an expensive or side-effecting default is a real performance and correctness bug.
Java/concurrency/concurrent-collections
This counter uses a ConcurrentHashMap but still loses increments under concurrency. What is the root cause?#
ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
void increment(String key) {
Integer current = counts.get(key);
if (current == null) {
counts.put(key, 1);
} else {
counts.put(key, current + 1);
}
}Options
Show answer
The get/check/put is a non-atomic check-then-act: two threads can read the same current, both compute the same next value, and one increment is lost. Use an atomic operation like counts.merge(key, 1, Integer::sum) or compute.
Each individual get and put on a ConcurrentHashMap is atomic, but the sequence get → check → put is not. Two threads can both read current = 5, both compute 6, and both put(key, 6) — so one increment is lost. The map being concurrent only protects single operations, never a compound read-modify-write you compose yourself. The fix is an atomic combinator the map runs under its own per-bin lock: counts.merge(key, 1, Integer::sum) (or compute/computeIfAbsent), or use LongAdder/AtomicInteger values. Option d is a real Java gotcha in other contexts but irrelevant here — the arithmetic value is correct; the lost-update race is the actual bug. Reaching for a concurrent collection and assuming compound operations are now safe is a classic mistake.
Java/collections/collections-framework
In Java 8 and later, ConcurrentHashMap rejects both null keys and null values with a NullPointerException, whereas HashMap accepts both null keys and null values.#
Options
Show answer
ConcurrentHashMap does not allow null keys or null values — both throw NullPointerException — while HashMap permits a single null key and any number of null values. The restriction is deliberate: in a concurrent map a null return from get would be ambiguous between 'absent key' and 'mapped to null,' and the containsKey workaround is not safe against races.
HashMap explicitly permits one null key and any number of null values. ConcurrentHashMap, by contrast, throws NullPointerException on any attempt to insert a null key or null value. This restriction is intentional: in a concurrent map, ambiguities arise if get returns null because it cannot distinguish 'the key maps to null' from 'the key is absent.' Eliminating nulls removes that ambiguity and avoids the need for the containsKey workaround that single-threaded HashMap callers can use.
Java/collections/collections-framework
The iterator returned by CopyOnWriteArrayList.iterator() throws UnsupportedOperationException when its remove() method is invoked.#
Options
Show answer
The CopyOnWriteArrayList iterator always throws UnsupportedOperationException when remove() is called. The iterator traverses a snapshot of the array taken at creation time, so it has no live backing state to remove from. This is by design — the snapshot guarantees traversal never sees ConcurrentModificationException — at the cost of making the iterator read-only.
CopyOnWriteArrayList creates a fresh snapshot copy of the underlying array each time the list is modified, and its iterator operates on a frozen reference to the array that existed at the time the iterator was created. Because mutations are not reflected back through the iterator's view, the remove() operation is unsupported and always throws UnsupportedOperationException. The iterator also never throws ConcurrentModificationException because it never sees structural changes made after it was created.
Java/collections/hashmap-internals
In java.util.HashMap (JDK 8+), when the number of entries in a single bucket's linked list reaches the treeification threshold of _____ (the default value of the TREEIFY_THRESHOLD constant), HashMap does not immediately convert that bucket to a red-black tree. It first checks whether the table capacity is at least _____ (the value of the MIN_TREEIFY_CAPACITY constant). If the table is smaller than that minimum, HashMap performs a resize (doubling the table) instead of treeifying, because resizing spreads entries across more buckets and is generally more effective when the table is still small.#
Show answer
In java.util.HashMap (JDK 8+), when the number of entries in a single bucket's linked list reaches the treeification threshold of 8 (the default value of the TREEIFY_THRESHOLD constant), HashMap does not immediately convert that bucket to a red-black tree. It first checks whether the table capacity is at least 64 (the value of the MIN_TREEIFY_CAPACITY constant). If the table is smaller than that minimum, HashMap performs a resize (doubling the table) instead of treeifying, because resizing spreads entries across more buckets and is generally more effective when the table is still small.
The TREEIFY_THRESHOLD constant is defined as 8 in HashMap's source. When a bin reaches 8 entries, the treeifyBin method is called, but it first checks if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY), where MIN_TREEIFY_CAPACITY = 64. If the table length is below 64, the method calls resize() instead of converting the bin to a tree. Only when the table has at least 64 slots will the linked list in that bucket be replaced with a TreeNode-based red-black tree. This design choice reflects the fact that resizing is cheaper and more broadly beneficial when the table is still small, whereas treeifying individual buckets becomes worthwhile only at larger table sizes where collisions are due to genuinely bad hash distribution rather than an undersized table.
Java/generics/bounded-wildcards
Explain the PECS rule for bounded wildcards in Java generics. When do you use ? extends T versus ? super T, and why?#
Show answer
PECS stands for 'Producer Extends, Consumer Super'. Use ? extends T when a parameterized type is a producer — you only read T values out of it. A List<? extends Number> could be a List<Integer> or List<Double>, so you can safely read elements as Number, but you cannot add anything (except null) because the compiler doesn't know the exact element type. Use ? super T when the type is a consumer — you only write T values into it. A List<? super Integer> could be a List<Integer>, List<Number>, or List<Object>, so it can accept Integer (and subtypes) being added, but reads only give you Object. The reason is variance and type safety: extends makes the type covariant (safe to read, unsafe to write), super makes it contravariant (safe to write, unsafe to read). The canonical example is Collections.copy(List<? super T> dest, List<? extends T> src) — the source produces, the destination consumes.
PECS — Producer Extends, Consumer Super — tells you which wildcard bound to pick. ? extends T is for producers you read from (covariant, read-safe, write-forbidden because the exact subtype is unknown); ? super T is for consumers you write to (contravariant, write-safe, reads only yield Object). The mnemonic captures the variance rules that keep the type system sound. A strong answer ties each bound to read vs write and explains why (the unknown exact type); weak answers just recite the acronym. This shows up in real API design — e.g. Collections.copy and Stream.collect signatures — and in deciding flexible method parameter types.
Java/collections/comparable-comparator
Given the following Java code, arrange the five Employee records in the order they would appear after emps.sort(cmp) completes.#
Put these in order
Show answer
After sorting, the order is Eve, Carol, Bob, Alice, Dave. The comparator sorts by salary descending, then name descending (because a pre-reversed comparator is passed to thenComparing), then years ascending. reversed() negates only the comparator it is called on, so the salary and name stages are both descending while the years stage is ascending.
The comparator chain applies three stages: (1) salary descending via comparingInt(salary).reversed(), (2) name descending via thenComparing(comparing(name).reversed()), and (3) years ascending via thenComparingInt(yearsOfService). Salary descending puts the two 120 000 employees (Carol, Eve) first, then the two 90 000 employees (Alice, Bob), then Dave at 80 000. Within the 120 000 tie, name descending orders "Eve" before "Carol" (E > C), giving Eve then Carol — years never come into play because names differ. Within the 90 000 tie, name descending orders "Bob" before "Alice" (B > A), giving Bob then Alice. Dave is alone at 80 000. Final sorted order: Eve (e), Carol (c), Bob (b), Alice (a), Dave (d). The critical subtlety is that reversed() is scoped: it negates only the comparator instance it is invoked on, not subsequent thenComparing appendages, and the second-stage name comparator was independently reversed before being passed in.
Related interview questions
Job market
See java salaries and hiring demand from live job postings.
The other 70 questions
This page shows 25 and marks what you pick. That's as far as a page can go. A free account opens the other 70 and keeps every answer. What you miss comes back until it's right: after a day, then at longer gaps.
Free · the whole bank · 100 marked answers per 30 days · written feedback on the paid plan