The Day-1 fundamentals interviewers assume you already know cold: the Collections hierarchy, ArrayList vs LinkedList vs Vector, overloading vs overriding, String immutability, ==/equals, final/finally/finalize, autoboxing, transient, and IO vs NIO — each with the why, not just the definition.
Published September 22, 2026
This is the material every Java interview assumes as a baseline before it goes deep on anything else. Getting any of it slightly wrong early signals "this candidate hasn't used Java seriously," regardless of how strong the rest of the interview goes. Treat this as the floor, not the ceiling.
Collection
├── List (ordered, duplicates allowed) → ArrayList, LinkedList, Vector
├── Set (no duplicates) → HashSet, LinkedHashSet, TreeSet
└── Queue (FIFO / priority processing) → ArrayDeque, PriorityQueue
└── Deque (double-ended) → ArrayDeque, LinkedList
Map (separate hierarchy — not a Collection) → HashMap, LinkedHashMap, TreeMap
Why is Map not part of Collection? Because Collection models a group of individual elements; Map models a group of key-value pairs — fundamentally a different contract (Map has no add(E), it has put(K,V)). This is a common quick-check question precisely because it's counter-intuitive to people coming from other languages.
ArrayList is backed by a resizable array. When it's full, it grows by 1.5x (not doubling, unlike HashMap) and copies all elements into the new array — an O(n) operation, amortized to O(1) per add across many inserts. Random access (get(i)) is O(1) because it's direct array indexing; inserting/removing at an arbitrary index is O(n) because everything after that index has to shift.
LinkedList is a doubly-linked list. Adding/removing at either end is O(1) (just pointer updates), but random access is O(n) — there's no indexing, you walk the chain. A common mistake: using LinkedList because "insert/remove is O(1)" without noticing that finding the insertion point in the middle is itself O(n), so "insert in the middle" is O(n) overall for both structures — LinkedList only wins when you already hold a reference to the node (e.g. via an Iterator, using iterator.remove()).
Vector and Stack are legacy (pre-Collections-Framework, Java 1.0) synchronized collections — every method is synchronized, meaning every operation pays a lock cost even in single-threaded code, and synchronizing individual method calls doesn't even make compound operations (like "check size then add") thread-safe. They're avoided today in favor of ArrayList/ArrayDeque plus explicit synchronization (or Collections.synchronizedList, or better, java.util.concurrent structures) only where actually needed.
| Operation | ArrayList | LinkedList | HashSet | TreeSet |
|---|---|---|---|---|
| add (end) | O(1) amortized | O(1) | O(1) avg | O(log n) |
| add (middle) | O(n) | O(n) | — | — |
| get(index) | O(1) | O(n) | n/a | n/a |
| contains | O(n) | O(n) | O(1) avg | O(log n) |
| remove | O(n) | O(1) if node known, else O(n) | O(1) avg | O(log n) |
Choosing the right one, by access pattern: mostly reading by index → ArrayList. Mostly adding/removing at the ends (queue/deque behavior) → ArrayDeque (faster than LinkedList for this in practice, due to cache locality — array-backed beats pointer-chasing). Need uniqueness with fast lookup → HashSet. Need uniqueness and sorted iteration → TreeSet. Need insertion order preserved with uniqueness → LinkedHashSet.
Overloading — same method name, different parameter list, resolved at compile time based on the declared (static) type of the arguments. Overriding — subclass redefines a superclass method with the same signature, resolved at runtime based on the actual object type (dynamic dispatch).
class Animal { void speak() { System.out.println("..."); } }
class Dog extends Animal { @Override void speak() { System.out.println("Woof"); } }
Animal a = new Dog();
a.speak(); // "Woof" — overriding resolved at runtime by actual type (Dog)
The classic trap question: overloaded methods are chosen based on the compile-time reference type, not the runtime object type — so passing null to two overloaded methods (foo(String) and foo(Object)) picks the most specific one at compile time, not based on any runtime check.
String is immutable — every "modification" (concat, +) creates a new object. StringBuilder is mutable, not thread-safe, and fast — the default choice for building strings in a loop. StringBuffer is mutable and synchronized — a legacy holdover from before StringBuilder existed (Java 5); use it only if you specifically need thread-safe mutable string building, which is rare enough that most developers never reach for it deliberately.
Why does this matter beyond terminology? Concatenating in a loop with + on String creates a new object on every iteration — O(n²) total for n concatenations. StringBuilder.append() in a loop is O(n) total.
== compares reference identity for objects (do these two variables point to the same object in memory?) or value for primitives. .equals() compares logical equality, as defined by the class's override (default Object.equals() is just == unless overridden).
String a = new String("hi");
String b = new String("hi");
a == b; // false — different objects
a.equals(b); // true — same content
final — a modifier: prevents reassignment (variable), overriding (method), or extension (class).finally — a block that always runs after try/catch, used for guaranteed cleanup (closing resources), regardless of whether an exception was thrown.finalize() — a deprecated Object method the GC used to call before reclaiming an object; unreliable (no guaranteed timing, sometimes never called) and removed from recommended practice in favor of try-with-resources and AutoCloseable.The only thing these three share is spelling — interviewers ask this specifically to check whether a candidate conflates unrelated concepts under surface-level pattern matching.
Before Java 8, adding a method to an interface broke every existing implementer (compile error). default methods let an interface provide a body, so implementers get a default behavior for free and only override it if they need to.
interface Greeter {
default String greet() { return "Hello"; }
}
The diamond problem: if a class implements two interfaces that both define the same default method, the compiler forces you to explicitly override it and choose (or combine) a behavior — Java doesn't silently pick one, avoiding the classic multiple-inheritance ambiguity.
Four concrete reasons, not just "because it's a design choice":
String; if mutable, code could change a value after a security check but before use.String caches its hashCode() after first computation, making repeated use in HashMap/HashSet cheaper.If a.equals(b) is true, a.hashCode() must equal b.hashCode(). Violate it and hash-based collections (HashMap, HashSet) silently fail to find an "equal" object stored under a different bucket — map.get(key) returns null for a key that's logically present. (Full mechanics in the HashMap Deep Dive lesson.)
Java auto-converts between primitives and wrapper types (int ↔ Integer) at assignment/call boundaries. The trap: unboxing a null Integer throws NullPointerException, not a compile error.
Map<String, Integer> counts = new HashMap<>();
int c = counts.get("missing"); // NPE — get() returns null, auto-unboxed into int
Marks a field to be skipped during Java's default serialization (ObjectOutputStream) — typical uses: a password field, or a derived/cacheable value not worth persisting. On deserialization, that field resets to its type's default value (null, 0, false) unless a custom readObject() restores it explicitly.
Classic java.io is stream-based and blocking — one thread per connection, blocked while waiting on I/O. java.nio is buffer-based and non-blocking, using Channels and Selectors so a single thread can monitor many connections at once. This is the foundation under high-throughput servers and frameworks like Netty, which need to serve thousands of concurrent connections without a thread per connection.
public final class Point {
private final int x;
private final int y;
private final List<String> tags; // mutable type — needs defensive copy
public Point(int x, int y, List<String> tags) {
this.x = x;
this.y = y;
this.tags = new ArrayList<>(tags); // defensive copy IN
}
public int getX() { return x; }
public int getY() { return y; }
public List<String> getTags() { return new ArrayList<>(tags); } // defensive copy OUT
}
The two copies (constructor and getter) are both required — without the first, a caller could mutate the list they passed in after construction; without the second, a caller could mutate the internal list via the returned reference. Missing either one makes the class not actually immutable, despite final fields.
Q: Why doesn't ArrayList implement RandomAccess the same way LinkedList does?
A: It does — ArrayList implements RandomAccess (a marker interface signaling O(1) indexed access); LinkedList does not, since its get(i) is O(n).
Q: Is String really 100% immutable — what about reflection?
A: Reflection can technically mutate the backing char[]/byte[] via setAccessible(true), but this is unsupported, JVM-version-fragile, and irrelevant to normal application code — "immutable" here means immutable through the public API, which is the guarantee that matters in practice.
Q: If two objects have the same hashCode(), are they equal?
A: No — hashCode equality is necessary but not sufficient; .equals() is the actual equality check. Hash collisions between unequal objects are expected and handled.
Q: When would you actually choose LinkedList over ArrayDeque?
A: Rarely in modern code — ArrayDeque is generally faster for queue/deque use due to array cache locality. LinkedList earns its place mainly when you need ListIterator and are actively removing elements while iterating via the iterator itself (O(1) removal at the cursor).
Q: Why does += on a String inside a loop still work, even though String is immutable?
A: Each += compiles to creating a new String (via an implicit StringBuilder in modern javac, for a single-line case) and reassigning the variable to point at it — the original String object is never mutated, the reference is just redirected. In a loop, though, javac does not reuse one StringBuilder across iterations — that's why explicit StringBuilder still wins for loops.