Java Interview Questions
15 Java questions — == vs .equals(), the String pool, checked exceptions, the Integer cache, HashMap internals, and generics type erasure. The concepts backend and enterprise interviewers ask every time. Tagged for FAANG and startups.
== and .equals()?Java▾== compares references (do two variables point to the same object). .equals() compares logical equality (do two objects have the same content, as defined by the class).
String a = new String("hi"); String b = new String("hi"); a == b; // false — different objects a.equals(b); // true — same content
.equals() for objects (especially Strings). If you override equals(), you must also override hashCode() to keep the contract for hash-based collections.String is immutable — once created, its contents never change; every "modification" produces a new object. Benefits: thread safety, safe use as HashMap keys, and security (paths, connection strings can't be mutated after validation).
The String pool is a special heap region where string literals are interned and reused. Two identical literals point to the same pooled object.
String a = "hi"; // pooled String b = "hi"; // same pooled object a == b; // true String c = new String("hi"); // forced new heap object a == c; // false
StringBuilder for heavy concatenation in loops — building with + creates a new String each iteration (O(n²)).| Checked | Unchecked (RuntimeException) | |
|---|---|---|
| Compiler enforces | Yes — declare or handle | No |
| Represents | Recoverable external problems | Programming bugs |
| Examples | IOException, SQLException | NullPointerException, ArrayIndexOutOfBounds |
throws, which some praise for robustness and others find noisy.
int) and its wrapper (Integer). Java caches Integer objects for values −128 to 127, so == on boxed values in that range can accidentally look correct.
Integer a = 127, b = 127; a == b; // true — cached, same object Integer c = 200, d = 200; c == d; // false — outside cache, different objects c.equals(d); // true — always use equals for wrappers
final, finally, and finalize?Java▾final— a modifier: afinalvariable can't be reassigned, afinalmethod can't be overridden, afinalclass can't be extended.finally— a block aftertry/catchthat always runs, used for cleanup.finalize()— a deprecatedObjectmethod the GC once called before reclaiming an object. Do not rely on it; use try-with-resources instead.
ArrayList and LinkedList?Java▾| ArrayList | LinkedList | |
|---|---|---|
| Backing | Resizable array | Doubly-linked nodes |
| Random access | O(1) | O(n) |
| Insert/remove at ends | O(1) amortized (end) | O(1) |
| Insert in middle | O(n) shift | O(1) once located |
| Memory | Compact | Higher (node pointers) |
ArrayList is the right default almost always — cache-friendly and fast random access. LinkedList rarely wins outside niche queue/deque cases.javac compiles source to platform-independent bytecode in a .class file. The JVM loads that bytecode, verifies it, and executes it — interpreting at first, then JIT-compiling hot methods to native code.
JVM runs bytecode. JRE = JVM plus the standard library, enough to run an application. JDK = JRE plus the tools to build one (javac, jar, javadoc).
.class file runs on any JVM, and the JIT specialises it for the machine it finds itself on.Java is pass-by-value everywhere. For an object, the value copied is the reference — so the method can mutate what the reference points at, but reassigning the parameter only rebinds the local copy.
void swap(int[] a, int[] b) { int[] t = a; a = b; b = t; } // no effect on the caller
void fill(int[] a) { a[0] = 42; } // caller sees this
That is why the standard idiom is to return a new value, or to mutate an object the caller already holds.
String concatenation in a loop — why is it a problem, and what do you use instead?Java▾String is immutable, so s += x allocates a new string and copies the whole contents each time. In a loop over n items that is O(n²) copying.
StringBuilder sb = new StringBuilder(); for (String part : parts) sb.append(part); String out = sb.toString();
StringBuilder keeps a growable char buffer and appends in amortised O(1). StringBuffer is the synchronised version — only worth the lock if the builder is genuinely shared across threads. Outside a loop, plain + is fine: the compiler rewrites it to a builder for you.
equals and hashCode?Java▾If two objects are equal, they must return the same hash code. The converse is not required — unequal objects may collide.
HashMap becomes unfindable, because lookup goes to a different bucket than insertion did.Keep the fields used in equals and hashCode identical, and keep them effectively immutable — mutating a key after insertion strands it in the wrong bucket.
HashMap, LinkedHashMap or TreeMap?Java▾HashMap: O(1) average lookup, no order guarantee — the default. LinkedHashMap: same cost plus a linked list keeping insertion order (or access order, which makes an LRU cache almost free). TreeMap: a red-black tree, O(log n), keys always sorted and range queries available.
| Interface | Abstract Class | |
|---|---|---|
| Multiple inheritance | Yes (implement many) | No (extend one) |
| State (fields) | Only constants | Instance fields allowed |
| Constructors | No | Yes |
| Method bodies | default/static only | Concrete + abstract |
Comparable, Runnable). Use an abstract class as a partial base for closely related types that share state and code.
- Overloading — same method name, different parameter lists, in the same class. Resolved at compile time (static polymorphism).
- Overriding — a subclass provides a new implementation of a parent method with the same signature. Resolved at runtime (dynamic dispatch).
@Override when overriding — the compiler then catches signature typos.equals(), hashCode(), and toString().
record Point(int x, int y) {} Point p = new Point(3, 4); p.x(); // 3 — accessor p.equals(new Point(3, 4)); // true — value equality, free
final, their fields are final, and they can't extend a class (they already extend Record).
| Modifier | Class | Package | Subclass | World |
|---|---|---|---|---|
private | ✓ | ✗ | ✗ | ✗ |
| (default) | ✓ | ✓ | ✗ | ✗ |
protected | ✓ | ✓ | ✓ | ✗ |
public | ✓ | ✓ | ✓ | ✓ |
Inheritance couples a subclass to its parent’s implementation: a change upstream can break every descendant, and you get the whole interface whether it fits or not. Composition holds a collaborator as a field and delegates, so you expose only what you mean to.
Inheritance earns its place when the relationship is genuinely is-a and the base is designed for extension — documented hooks, or an abstract class that exists precisely to be subclassed.
final keyword mean on a class, a method and a field?Java▾On a class, it cannot be subclassed. On a method, it cannot be overridden. On a field, it must be assigned exactly once and cannot be reassigned after.
final on a reference field freezes the reference, not the object. final List<String> xs can still have items added — use List.copyOf for an unmodifiable one.A final field also carries a memory-model guarantee: once a constructor completes, other threads see the fully-initialised value without extra synchronisation.
It is global mutable state: it hides dependencies from a class’s signature, makes tests order-dependent, and is awkward to substitute. Prefer passing the instance in.
public enum Config {
INSTANCE;
private final Properties props = load();
public String get(String k) { return props.getProperty(k); }
}
If you do need one, an enum is the safest form — the JVM guarantees a single instance, initialisation is thread-safe, and it is immune to the serialisation and reflection tricks that defeat a lazily-built holder.
A subclass must be usable anywhere its base is expected without changing correctness. Violations look like a subclass that strengthens what it demands or weakens what it promises.
class Rectangle { void setWidth(int w); void setHeight(int h); }
class Square extends Rectangle { /* setWidth also sets height */ }
// caller: r.setWidth(5); r.setHeight(4); assert r.area() == 20;
// holds for Rectangle, fails for Square
The fix is not a cleverer Square: it is recognising that an immutable Shape with an area() is the right model, and mutable-rectangle is not a supertype of square.
They let an interface gain a method without breaking every existing implementer — which is how Collection.stream() was added to interfaces implemented across the whole ecosystem.
Iface.super.method().They are for evolution, not for shipping behaviour. State still cannot live on an interface, so anything needing fields belongs in an abstract class.
HashMap work internally?Java▾HashMap is an array of buckets. A key's hashCode() (further spread by an internal hash) selects a bucket index. Collisions in a bucket form a linked list, which converts to a balanced tree (Java 8+) once it exceeds 8 entries, improving worst-case lookup from O(n) to O(log n).
- Load factor default 0.75 — at 75% full it resizes (doubles) and rehashes.
- Average get/put: O(1); worst case with tree bins: O(log n).
hashCode() and equals() consistently. Mutating a key after insertion corrupts lookups.Comparable vs Comparator?Java▾Comparable<T>— the class's natural ordering, viacompareTo(). One ordering per class.Comparator<T>— an external, swappable ordering, viacompare(). Many per class.
people.sort(Comparator.comparing(Person::getAge) .thenComparing(Person::getName)); // multi-key sort
List<String> and List<Integer> are both just List at runtime. This preserved backward compatibility with pre-generics code.
Consequences:
- You can't do
new T()ornew T[]. instanceof List<String>is illegal.- Overloads that differ only by generic parameter clash (same erased signature).
- Young generation (Eden + survivor spaces) — new objects; frequent, fast minor GCs.
- Old generation — long-lived objects; less frequent, costlier major GCs.
System.gc() is only a hint.
ConcurrentModificationException if the collection is structurally modified during iteration (except through the iterator's own remove()). It detects modification via an internal modCount.
for (String s : list) { if (s.isEmpty()) list.remove(s); // throws CME } // Fix: use iterator.remove() or removeIf(String::isEmpty)
CopyOnWriteArrayList are fail-safe — they iterate over a snapshot and never throw.
synchronized not enough, and what does volatile actually guarantee?Java▾volatile guarantees visibility and ordering: a write is published to other threads and cannot be reordered across the access. It does not give atomicity — count++ on a volatile is still read-modify-write and still races.
volatile boolean running = true; // correct: a flag one thread sets, another reads volatile int count = 0; count++; // wrong: use AtomicInteger
synchronized gives both atomicity and visibility, at the cost of blocking. For a single counter, AtomicInteger is a compare-and-swap loop with neither lock nor race.
Stream give you that a loop does not — and when is a loop better?Java▾Streams are lazy and composable: intermediate operations build a pipeline that runs once on terminal invocation, so filter().map().findFirst() touches only the elements it needs. They also express intent — the reader sees "filter then map", not index bookkeeping.
parallelStream() in particular is a trap: it costs a fork-join dispatch that only pays off on large, CPU-bound, side-effect-free work.Streams are single-use: consuming one twice throws IllegalStateException.
Optional for, and how is it misused?Java▾It is a return type that makes "there might be no value" part of the signature, so the caller cannot forget the empty case the way a bare null lets them.
Optional<User> findByEmail(String email); // good: absence is expected
user.getName().orElse("anonymous"); // good
void setUser(Optional<User> u); // misuse: parameters
private Optional<String> name; // misuse: fields
Do not use it for parameters or fields — it is not serialisable, adds an allocation, and a null Optional is the worst of both worlds. And never call get() without isPresent(); that is just a null pointer with extra steps.
OutOfMemoryError comes from.Java▾Heap holds objects and is what the garbage collector manages. Stack is per-thread, holding frames, locals and return addresses. Metaspace holds class metadata (native memory since Java 8, replacing PermGen).
OutOfMemoryError: Java heap space means live objects exceed the heap — usually a leak such as an ever-growing static collection, not a genuinely large working set. StackOverflowError is unbounded recursion. Metaspace exhaustion usually means classloader leaks, which is a classic in redeployed application servers.
-XX:+HeapDumpOnOutOfMemoryError) and look at dominators. The leak is nearly always one collection that nothing ever removes from.A thread is an expensive OS resource; creating one per task means unbounded thread counts under load and a scheduler that thrashes. An ExecutorService owns a bounded pool and a queue, so concurrency stays capped and tasks queue instead of overwhelming the machine.
ExecutorService pool = Executors.newFixedThreadPool(8); Future<Report> f = pool.submit(() -> build(id)); Report r = f.get(); // blocks until ready pool.shutdown();
Size the pool to the work: roughly the core count for CPU-bound tasks, higher for I/O-bound ones that spend their time blocked. Virtual threads (Java 21) change the calculus for blocking I/O — they are cheap enough to create per task.