Java Interview Questions and Answers
174 hand-picked Java interview questions with
detailed answers. Open the interactive version above to search, filter
by difficulty, run code, bookmark questions and track your progress.
What are the four OOP principles?
- Encapsulation — bundle data + methods, hide internals behind access modifiers.
- Abstraction — expose what an object does, hide how (interfaces/abstract classes).
- Inheritance — reuse via an is-a relationship (
extends). - Polymorphism — one interface, many implementations (overriding/overloading).
Method overloading vs overriding.
Overloading — same method name, different parameters, in the same class (compile-time polymorphism).
Overriding — a subclass redefines a parent method with the same signature (runtime polymorphism); mark it @Override.
Interface vs abstract class.
Interface — a contract; a class can implement many. Since Java 8 it can have default/static methods but no instance state.
Abstract class — a partial base with state and constructors; single inheritance only. Use it to share code among related classes.
Why override equals() and hashCode() together?
Hash-based collections (HashMap, HashSet) use hashCode() to find the bucket and equals() to confirm the match. If you override one but not the other, objects that are 'equal' can land in different buckets, breaking lookups.
ArrayList vs LinkedList vs HashMap vs HashSet.
- ArrayList — dynamic array; fast random access, slower mid-list inserts.
- LinkedList — doubly linked; fast inserts/removes, slow random access.
- HashMap — key/value, O(1) average lookup, unordered.
- HashSet — unique elements, backed by a HashMap.
Checked vs unchecked exceptions.
Checked (IOException) — must be declared or caught; the compiler enforces handling.
Unchecked (RuntimeException, NullPointerException) — programming errors, not enforced by the compiler.
Why are Strings immutable, and String vs StringBuilder?
Immutability enables the String pool (interning), thread-safety, and safe use as map keys. Concatenating Strings in a loop creates many objects.
Use StringBuilder for heavy concatenation — it mutates one buffer.
StringBuilder sb = new StringBuilder();
for (String s : items) sb.append(s);
What does the final keyword do?
- final variable — assigned once (a constant).
- final method — cannot be overridden.
- final class — cannot be extended (e.g. String).
What does static mean?
A static member belongs to the class, not an instance — shared across all objects and accessible without creating one. Used for constants, utility methods, and counters.
public static int square(int n) { return n * n; }
What are Java 8 Streams?
A declarative API for processing collections via a pipeline of operations — filter, map, reduce, collect. Streams are lazy and can run in parallel, making data transformation concise and readable.
List<String> names = users.stream()
.filter(u -> u.isActive())
.map(User::getName)
.collect(Collectors.toList());
What is Optional and why use it?
Optional<T> is a container that may or may not hold a value — a clearer alternative to returning null. It forces callers to handle the empty case, reducing NullPointerExceptions.
return repo.findById(id)
.map(User::getName)
.orElse("Unknown");
How do you create and manage threads in Java?
Implement Runnable (preferred) or extend Thread. In practice use an ExecutorService / thread pool instead of raw threads, and share state safely with synchronized, locks, or concurrent collections.
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> doWork());
JDK vs JRE vs JVM, and how does GC work?
JVM runs bytecode. JRE = JVM + libraries (to run apps). JDK = JRE + compiler/tools (to build apps).
Garbage collection automatically frees objects no longer reachable from roots, mostly in the young generation (frequent, cheap) vs old generation (rare, costly).
Is Java pass-by-value or pass-by-reference?
Java is always pass-by-value. For objects, the value that gets copied is the reference (the address), not the object. So a method can mutate the object your variable points at, but reassigning the parameter inside the method never affects the caller's variable.
void rename(User u) { u.setName("Bob"); } // caller sees "Bob"
void replace(User u) { u = new User("Bob"); } // caller sees nothing
== vs equals().
== compares identity — for primitives the value, for objects whether both references point to the same instance.
equals() compares logical equality as defined by the class. Object.equals() defaults to ==, so you must override it for value semantics.
String a = new String("hi"), b = new String("hi");
a == b; // false — different objects
a.equals(b); // true — same characters
What do this and super do?
this — the current instance. Used to disambiguate a field from a parameter, or to call another constructor via this(...).super — the parent. super.method() calls the parent's version; super(...) calls the parent constructor.
class Dog extends Animal {
Dog(String name) { super(name); }
Dog() { this("Rex"); }
}
What is a constructor, and what's the default constructor?
A constructor initializes a new object. It has the class's name, no return type, and runs when you call new.
If you write no constructor, the compiler inserts a no-arg default constructor. The moment you declare any constructor, that free one disappears.
What is the class initialization order?
On first use of the class: static fields and static blocks run once, in source order. Then for each new: instance fields and instance init blocks run in source order, then the constructor body.
With inheritance the parent is fully initialized before the child.
static { } // 1. once, on class load
{ } // 2. before every constructor body
MyClass() { } // 3.
Static block vs instance initializer block.
A static block static { } runs once when the class is loaded — used to set up static state.
An instance block { } runs before every constructor body — useful to share setup across overloaded constructors (though calling this(...) is usually clearer).
Explain the four access modifiers.
- private — same class only.
- default (no keyword) — same package.
- protected — same package plus subclasses anywhere.
- public — everywhere.
Static nested class vs inner class.
A static nested class is just a top-level class scoped inside another — no link to an outer instance.
An inner (non-static) class holds an implicit reference to its enclosing instance, so it can read the outer fields — but that reference can keep the outer object alive and leak memory.
class Outer {
static class Nested { } // no outer reference
class Inner { } // holds Outer.this
}
Outer.Nested n = new Outer.Nested();
Outer.Inner i = new Outer().new Inner();
What is an anonymous inner class, and when do lambdas replace it?
An anonymous class is a one-off unnamed implementation declared and instantiated inline — traditionally used for listeners and callbacks.
Since Java 8, a lambda replaces it whenever the target is a functional interface (exactly one abstract method). Anonymous classes are still needed for multiple methods or to hold state.
Runnable a = new Runnable() { public void run() { work(); } };
Runnable b = () -> work(); // same thing, Java 8+
What is an enum and what can it do?
An enum is a type-safe fixed set of constants. Unlike int constants it can't hold an invalid value.
Enums are real classes: they can have fields, constructors, methods, implement interfaces, and even give each constant its own behaviour.
enum Status {
ACTIVE("A"), CLOSED("C");
private final String code;
Status(String code) { this.code = code; }
String code() { return code; }
}
How do you use enums with switch, EnumMap and values()?
switch on an enum needs only the bare constant name. values() returns all constants, valueOf(String) parses one (throwing if unknown), and ordinal() gives the index.
EnumMap/EnumSet are array-backed and far faster than HashMap/HashSet for enum keys.
switch (status) {
case ACTIVE -> start();
case CLOSED -> stop();
}
Map<Status, Integer> counts = new EnumMap<>(Status.class);
What are varargs?
Type... name lets a method take any number of arguments; inside, it's just an array. Only one varargs parameter is allowed and it must be last.
int sum(int... nums) {
int t = 0;
for (int n : nums) t += n;
return t;
}
sum(); sum(1, 2, 3);
What is autoboxing and what are its pitfalls?
Autoboxing converts a primitive to its wrapper automatically (int ↔ Integer). Convenient, but it hides three traps: NullPointerException when unboxing null, performance cost in loops, and broken == comparisons.
Map<String, Integer> m = new HashMap<>();
int v = m.get("missing"); // NPE — null unboxed to int
Why does Integer a = 127, b = 127; a == b give true but 128 give false?
Integer.valueOf() caches instances for -128 to 127, so both references point to the same cached object. Above 127 each box is a new object, so == (identity) is false.
Integer a = 127, b = 127; // a == b -> true
Integer c = 128, d = 128; // c == d -> false
c.equals(d); // true — always use this
What are Java's primitive types, and why do wrappers exist?
Eight primitives: byte, short, int, long, float, double, char, boolean. They hold raw values, live on the stack, and can't be null.
Wrappers (Integer, Long, …) are objects, so they can be null and can be used in generics and collections — which can't hold primitives.
How can a ternary expression throw a NullPointerException?
If the two branches have different types, the compiler applies binary numeric promotion and unboxes them. A null wrapper in the chosen branch is then unboxed → NPE, even though you never wrote a dereference.
Integer x = null;
Integer y = true ? x : 0; // NPE — x is unboxed because 0 is an int
How do you write a truly immutable class?
- Make the class
final (no subclass can add mutability). - Make all fields
private final. - No setters; set everything in the constructor.
- Defensively copy mutable inputs in, and mutable fields out.
Immutable objects are automatically thread-safe and safe as map keys.
public final class Trip {
private final List<String> stops;
public Trip(List<String> stops) {
this.stops = List.copyOf(stops); // copy in
}
public List<String> getStops() {
return Collections.unmodifiableList(stops); // copy/guard out
}
}
What is a record (Java 14+)?
A record is a concise immutable data carrier. The compiler generates the private final fields, a canonical constructor, accessors, and value-based equals/hashCode/toString.
Records are implicitly final and can't extend a class — they're for data, not hierarchies.
public record Point(int x, int y) { }
// validation via a compact constructor
public record Range(int lo, int hi) {
public Range {
if (lo > hi) throw new IllegalArgumentException();
}
}
What are sealed classes (Java 17)?
sealed restricts which classes may extend a type via a permits clause. Each subclass must then be final, sealed, or non-sealed.
It gives you a closed hierarchy the compiler understands — so a switch over it can be checked for exhaustiveness with no default.
public sealed interface Shape permits Circle, Square { }
public record Circle(double r) implements Shape { }
public record Square(double s) implements Shape { }
What is var (Java 10)?
var is local-variable type inference — the compiler infers the type from the initializer. Java stays statically typed; the variable's type is fixed at compile time, it's just not written out.
Only for local variables with an initializer — never fields, parameters, or return types.
var users = new ArrayList<User>(); // ArrayList<User>
var name = "Ann"; // String
// var x; // error — no initializer
What is the String pool, and what does intern() do?
The String pool is a JVM-managed cache of unique string literals (on the heap since Java 7). Two identical literals share one object, saving memory.
new String("hi") deliberately bypasses the pool and allocates a fresh object. intern() puts a runtime-built string into the pool (or returns the pooled one).
String a = "hi";
String b = "hi"; // same pooled object
String c = new String("hi"); // new object on the heap
a == b; // true
a == c; // false
a == c.intern(); // true
StringBuilder vs StringBuffer.
Both are mutable character buffers with identical APIs. StringBuffer is synchronized (thread-safe, slower); StringBuilder is not (faster).
Use StringBuilder — buffers are almost always local variables, so the synchronization buys nothing.
Which String methods come up most in interviews?
length(), charAt(i), substring(a, b) — end index is exclusive.equals() / equalsIgnoreCase(), compareTo() (lexicographic).contains(), indexOf(), startsWith(), split(), join(), replace(), trim()/strip().- Java 11+:
isBlank(), lines(), repeat(n).
What are text blocks (Java 15)?
A multi-line string literal delimited by """. Incidental leading whitespace is stripped automatically and quotes need no escaping — ideal for embedded JSON, SQL, or HTML.
String json = """
{ "name": "Ann", "age": 30 }
""";
What methods does every object inherit from Object?
equals(), hashCode() — logical equality (override together).toString() — override it for readable logs.getClass() — runtime type.clone() — protected, shallow copy.wait(), notify(), notifyAll() — legacy thread coordination.finalize() — deprecated, never use.
What is the equals() contract?
equals() must be: reflexive (x.equals(x)), symmetric (x.equals(y) ⇔ y.equals(x)), transitive, consistent across calls, and x.equals(null) must be false.
Break any of these and hash-based collections misbehave in ways that are very hard to debug.
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User u)) return false;
return Objects.equals(id, u.id);
}
@Override public int hashCode() { return Objects.hash(id); }
What is the hashCode() contract, and what makes a good hash?
Rules: equal objects must return equal hash codes; unequal objects may collide (but shouldn't often); the value must be consistent while the object's state is unchanged.
A good hash spreads values evenly. Objects.hash(...) is fine for most classes.
@Override public int hashCode() {
return Objects.hash(firstName, lastName);
}
Comparable vs Comparator.
Comparable — the class defines its own natural order via compareTo(). One ordering per class.
Comparator — an external ordering passed to the sort. Any number of them, and you can order classes you don't own.
users.sort(Comparator
.comparing(User::getLastName)
.thenComparing(User::getFirstName)
.reversed());
Shallow copy vs deep copy, and why is clone() discouraged?
A shallow copy duplicates the object but shares the referenced objects. A deep copy recursively duplicates them too, so the copies are fully independent.
Object.clone() is shallow, protected, needs the marker interface Cloneable, and skips constructors — so prefer a copy constructor or a static factory.
// Preferred over clone()
public User(User other) {
this.name = other.name;
this.roles = new ArrayList<>(other.roles); // deep
}
What is serialization, and what does transient do?
Serialization converts an object graph into a byte stream (via Serializable) for storage or transfer; deserialization rebuilds it — without calling the constructor.
transient marks a field to be skipped (passwords, caches, non-serializable handles); it comes back as the default value.
class Session implements Serializable {
private static final long serialVersionUID = 1L;
private String user;
private transient String password; // not written
}
What is serialVersionUID?
A version stamp for a Serializable class. On deserialization the JVM compares the stream's id with the class's; a mismatch throws InvalidClassException.
If you don't declare it, the JVM computes one from the class structure — so any change (even adding a method) breaks compatibility with old data.
private static final long serialVersionUID = 1L;
Why override toString()?
The default prints ClassName@1b6d3586 — the hex hash, which tells you nothing. An overridden toString() shows the object's meaningful state in logs, debuggers, and error messages.
@Override public String toString() {
return "User{id=" + id + ", name='" + name + "'}";
}
What is pattern matching for instanceof (Java 16)?
It binds a typed variable as part of the check, removing the redundant cast that always followed instanceof. The binding is scoped to where the check is guaranteed true.
// old
if (o instanceof String) { String s = (String) o; use(s); }
// Java 16+
if (o instanceof String s) { use(s); }
What are switch expressions (Java 14)?
switch can now return a value using arrow labels — no fall-through, no break, and the compiler enforces exhaustiveness. Use yield to return from a block body.
int days = switch (month) {
case FEB -> 28;
case APR, JUN, SEP, NOV -> 30;
default -> 31;
};
What is pattern matching for switch (Java 21)?
switch can match on type patterns, with optional when guards and record deconstruction. Combined with sealed types, the compiler verifies you handled every case — no default needed.
String describe(Shape s) {
return switch (s) {
case Circle c when c.r() > 10 -> "big circle";
case Circle c -> "circle";
case Square(double side) -> "square " + side;
};
}
How do you format strings in Java?
String.format("%s is %d", name, age) — printf-style."...".formatted(args) — Java 15+, reads better in a chain.String.join(", ", list) — joining.
Common specifiers: %s string, %d integer, %.2f 2-decimal float, %n newline.
Why is 0.1 + 0.2 != 0.3, and when do you use BigDecimal?
double/float are binary (IEEE-754) and cannot represent decimal fractions like 0.1 exactly, so errors accumulate.
Use BigDecimal for money and any exact decimal arithmetic — constructed from a String, and compared with compareTo().
0.1 + 0.2 == 0.3; // false!
new BigDecimal("0.1").add(new BigDecimal("0.2"))
.compareTo(new BigDecimal("0.3")); // 0 — equal
What happens on integer overflow in Java?
It wraps around silently — no exception. Integer.MAX_VALUE + 1 becomes Integer.MIN_VALUE.
Use Math.addExact() / multiplyExact() to throw ArithmeticException instead, or use long.
Integer.MAX_VALUE + 1; // -2147483648
Math.addExact(Integer.MAX_VALUE, 1); // ArithmeticException
Describe the Collections framework hierarchy.
Iterable → Collection → List (ordered, duplicates), Set (unique), Queue/Deque (ends).
Map sits outside Collection — it stores key/value pairs, not elements.
How does HashMap work internally?
An array of buckets. put takes the key's hashCode(), spreads it, and maps it to an index. Collisions chain in a linked list — which becomes a red-black tree past 8 entries in one bucket (Java 8+), keeping worst case at O(log n).
At load factor 0.75 the array doubles and everything is rehashed.
Map<String, User> m = new HashMap<>(32); // presize to avoid resizes
HashMap vs Hashtable vs ConcurrentHashMap.
- HashMap — not synchronized, allows one null key + null values. The default choice.
- Hashtable — legacy, synchronizes every method on the whole map, no nulls. Don't use.
- ConcurrentHashMap — thread-safe with fine-grained locking, no nulls, high concurrent throughput.
How does ConcurrentHashMap achieve thread-safety without locking everything?
Since Java 8 it locks only the individual bucket (via synchronized on the head node) and uses lock-free CAS for empty-bucket inserts. Reads are non-blocking — the table is volatile.
So many threads write different buckets concurrently, while Hashtable would serialize them all.
// NOT atomic — race between get and put
if (map.get(k) == null) map.put(k, v);
// atomic
map.computeIfAbsent(k, key -> v);
map.merge(k, 1, Integer::sum); // counter
HashMap vs LinkedHashMap vs TreeMap.
- HashMap — no order guarantee, O(1). Default.
- LinkedHashMap — keeps insertion (or access) order via a linked list, O(1). Access-order mode makes an LRU cache trivial.
- TreeMap — sorted by key (Comparable/Comparator), O(log n), and offers navigation like
firstKey()/headMap().
// LRU cache in a few lines
new LinkedHashMap<K,V>(16, 0.75f, true) {
protected boolean removeEldestEntry(Map.Entry<K,V> e) {
return size() > MAX;
}
};
HashSet vs LinkedHashSet vs TreeSet.
Each is backed by the matching map: HashSet (unordered, O(1)), LinkedHashSet (insertion order, O(1)), TreeSet (sorted, O(log n)).
All reject duplicates — decided by equals/hashCode, except TreeSet which uses compareTo.
How does ArrayList grow internally?
It wraps an Object[]. When full, it allocates a new array at 1.5× the capacity and copies everything over (Arrays.copyOf).
So add() is amortized O(1); get(i) is O(1); add/remove in the middle is O(n) because of the shift.
List<String> l = new ArrayList<>(10_000); // no resizing
Array vs ArrayList.
Array — fixed length, can hold primitives, covariant, no helper methods.
ArrayList — grows dynamically, objects only (autoboxing), generic-safe, rich API.
Object[] arr = new String[2];
arr[0] = 42; // compiles! ArrayStoreException at runtime
What is ConcurrentModificationException and how do you avoid it?
Most collections are fail-fast: the iterator tracks a modCount and throws ConcurrentModificationException if the collection is structurally modified while iterating — even on a single thread.
Fix it with iterator.remove(), removeIf(), or by iterating a copy.
// throws
for (String s : list) if (s.isEmpty()) list.remove(s);
// correct
list.removeIf(String::isEmpty);
What are the pitfalls of Arrays.asList()?
It returns a fixed-size view backed by the array — not an independent ArrayList. add()/remove() throw UnsupportedOperationException, and set() writes through to the array.
With primitives, Arrays.asList(intArray) gives a single-element List<int[]>, not a list of ints.
List<String> l = Arrays.asList("a", "b");
l.set(0, "z"); // ok — writes to the array
l.add("c"); // UnsupportedOperationException
int[] nums = {1, 2, 3};
Arrays.asList(nums).size(); // 1 !
List.of() vs Arrays.asList() vs unmodifiableList().
- List.of() (Java 9) — truly immutable, rejects nulls, no backing array to mutate.
- Arrays.asList() — fixed-size view; set() still works.
- Collections.unmodifiableList(l) — an unmodifiable wrapper; the original list can still change under it.
List.of("a", "b"); // immutable
List.copyOf(source); // immutable snapshot
Queue, Deque, and why not Stack?
Queue — FIFO (offer/poll/peek). Deque — both ends, so it serves as both a queue and a stack. PriorityQueue — heap-ordered, not FIFO.
The legacy Stack extends Vector, so it's synchronized and slow — use ArrayDeque.
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); stack.pop();
Deque<Integer> queue = new ArrayDeque<>();
queue.offer(1); queue.poll();
Iterator vs Iterable, and how does for-each work?
Iterable — 'can be iterated'; it just supplies an iterator(). Iterator — the cursor itself: hasNext(), next(), remove().
A for-each loop is pure syntax sugar: the compiler calls iterator() and loops on hasNext()/next(). Implement Iterable and your class works with for-each.
Why do generics exist?
They move type errors from runtime to compile time and remove casts. Before generics, List held Object, so a wrong type only blew up as a ClassCastException later, far from the bug.
List raw = new ArrayList();
raw.add(42);
String s = (String) raw.get(0); // ClassCastException at runtime
List<String> safe = new ArrayList<>();
safe.add(42); // compile error — caught early
What is type erasure?
Generics exist only at compile time. The compiler checks types, then erases them — replacing T with Object (or the bound) and inserting casts. At runtime a List<String> is just a List.
That's why you can't do new T(), o instanceof List<String>, or overload on List<String> vs List<Integer>.
new ArrayList<String>().getClass()
== new ArrayList<Integer>().getClass(); // true!
Explain wildcards and PECS.
PECS — Producer Extends, Consumer Super.
? extends T — you read T from it (a producer). You can't add.? super T — you write T into it (a consumer). Reads come back as Object.? — unbounded; you know nothing but Object.
// producer — read only
double sum(List<? extends Number> nums) {
double t = 0;
for (Number n : nums) t += n.doubleValue();
return t;
}
// consumer — write only
void fill(List<? super Integer> dest) { dest.add(1); }
How do you write a generic class and a generic method?
A generic class declares the parameter after the name: class Box<T>. A generic method declares its own before the return type: <T> T first(List<T> l).
Bounded parameters restrict it: <T extends Comparable<T>> lets you call compareTo.
public <T extends Comparable<T>> T max(List<T> list) {
T best = list.get(0);
for (T t : list) if (t.compareTo(best) > 0) best = t;
return best;
}
What are the time complexities of the common collections?
- ArrayList — get O(1), add(end) amortized O(1), add/remove(middle) O(n), contains O(n).
- LinkedList — get O(n), add/remove at ends O(1).
- HashMap/HashSet — get/put O(1) average, O(log n) worst.
- TreeMap/TreeSet — O(log n) all, sorted.
- ArrayDeque — O(1) at both ends.
When would you use CopyOnWriteArrayList?
Every write copies the whole backing array, so writes are O(n) but reads are lock-free and iterators never throw ConcurrentModificationException (they see a snapshot).
Ideal for read-heavy, rarely-written data — listener lists, config. Terrible if writes are frequent.
List<Listener> listeners = new CopyOnWriteArrayList<>();
Which sorting algorithms does Java use?
Objects (Collections.sort, Arrays.sort(Object[])) → TimSort: a stable merge/insertion hybrid, O(n log n), near O(n) on partly-sorted data.
Primitives (Arrays.sort(int[])) → dual-pivot quicksort: faster, not stable — which doesn't matter, since identical primitives are indistinguishable.
Describe the exception hierarchy. Error vs Exception.
Throwable splits into Error and Exception.
- Error — serious JVM problems (
OutOfMemoryError, StackOverflowError). Don't catch them. - Exception — application problems.
RuntimeException and its subclasses are unchecked; everything else is checked.
What is try-with-resources?
Any resource declared in try(...) is closed automatically — in reverse order — as long as it implements AutoCloseable. It replaces error-prone finally blocks.
It also fixes exception masking: if both the body and close() throw, the close exception is attached as suppressed rather than hiding the real one.
try (var in = new FileInputStream(f);
var out = new FileOutputStream(g)) {
in.transferTo(out);
} // both closed, out first
Does finally always run? What if it returns?
It runs after try/catch in essentially every case — including when they return. It's skipped only on System.exit(), a JVM crash, or an infinite loop/killed thread.
A return or throw inside finally discards the try block's return value or exception — which silently swallows bugs.
int f() {
try { return 1; }
finally { return 2; } // returns 2 — the 1 is lost
}
What is multi-catch, and what's the catch ordering rule?
Multi-catch handles several types in one block: catch (IOException | SQLException e). The variable is implicitly final.
Ordering rule: subclasses must be caught before superclasses — putting Exception first makes later blocks unreachable, a compile error.
try { risky(); }
catch (FileNotFoundException e) { /* specific */ }
catch (IOException | SQLException e) { /* multi */ }
How do you create a custom exception, and checked or unchecked?
Extend RuntimeException for an unchecked one, Exception for a checked one. Always provide a constructor taking a cause so the original stack trace survives.
Rule of thumb: checked if the caller can plausibly recover; unchecked if it's a programming error or unrecoverable.
public class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException(String msg, Throwable cause) {
super(msg, cause);
}
}
What is exception chaining and why does it matter?
Wrapping the original exception as the cause when rethrowing at a higher abstraction level. The trace then shows 'Caused by:' all the way down to the root.
Losing the cause is the fastest way to make a production bug undebuggable.
catch (SQLException e) {
throw new DaoException("Saving order failed", e); // keep the cause
}
What are exception-handling best practices?
- Never swallow: an empty
catch hides bugs. - Catch specific types, not
Exception. - Always chain the cause when rethrowing.
- Don't use exceptions for control flow — they're expensive (stack capture).
- Clean up with try-with-resources, not
finally. - Log or rethrow — doing both duplicates noise.
How do you avoid NullPointerExceptions?
- Return
Optional or an empty collection — never null. - Use
Objects.requireNonNull() to fail fast at the boundary. - Put the constant first:
"ACTIVE".equals(status). Objects.equals(a, b) is null-safe.- Watch for unboxing a null wrapper.
this.name = Objects.requireNonNull(name, "name");
if ("ACTIVE".equals(status)) { } // null-safe
What is a lambda expression?
An anonymous function you can pass around — shorthand for implementing a functional interface (exactly one abstract method).
Lambdas capture effectively final locals only, and this refers to the enclosing instance (unlike an anonymous class).
(a, b) -> a + b
name -> System.out.println(name)
() -> 42
What are the built-in functional interfaces?
- Function<T,R> —
apply: T in, R out. - Predicate<T> —
test: T in, boolean out. - Consumer<T> —
accept: T in, nothing out. - Supplier<T> —
get: nothing in, T out. - BiFunction, UnaryOperator, BinaryOperator.
Function<String,Integer> len = String::length;
Predicate<String> empty = String::isEmpty;
Supplier<List<String>> maker = ArrayList::new;
Consumer<String> print = System.out::println;
What are the four kinds of method reference?
- Static —
Integer::parseInt - Bound instance —
System.out::println (a specific object) - Unbound instance —
String::length (the receiver becomes the first argument) - Constructor —
ArrayList::new
list.forEach(System.out::println); // bound
list.stream().map(String::length); // unbound
list.stream().collect(toCollection(TreeSet::new)); // constructor
Intermediate vs terminal operations — and why is laziness important?
Intermediate ops (filter, map, sorted) return a Stream and are lazy — they build a pipeline and do nothing.
A terminal op (collect, forEach, reduce, count) triggers execution. Without one, no code runs at all.
list.stream().map(this::expensive); // nothing happens!
list.stream().map(this::expensive).toList(); // now it runs
map() vs flatMap().
map is 1→1 — each element becomes exactly one element.
flatMap is 1→many, flattened — each element becomes a stream, and all of those are concatenated into one. It's how you turn nested structures flat.
// map -> Stream<List<String>>
users.stream().map(User::getRoles);
// flatMap -> Stream<String>
users.stream().flatMap(u -> u.getRoles().stream()).distinct().toList();
Which Collectors do you use most?
toList(), toSet(), toMap(k, v)groupingBy(fn) → Map<K, List<T>>, with an optional downstream collectorpartitioningBy(pred) → Map<Boolean, List<T>>joining(", "), counting(), summingInt(), averagingDouble()
Map<Dept, List<User>> byDept =
users.stream().collect(groupingBy(User::getDept));
Map<Dept, Long> counts =
users.stream().collect(groupingBy(User::getDept, counting()));
Map<String, User> byId =
users.stream().collect(toMap(User::getId, u -> u, (a, b) -> a));
How does reduce() work?
reduce folds a stream into one value with an associative accumulator. Three forms: reduce(op) → Optional; reduce(identity, op) → T; reduce(identity, acc, combiner) → for parallel/different types.
int sum = nums.stream().reduce(0, Integer::sum);
Optional<String> longest = words.stream()
.reduce((a, b) -> a.length() >= b.length() ? a : b);
When should you use parallelStream()?
It splits work across the shared ForkJoinPool.commonPool. Worth it only for large datasets, CPU-bound work, and splittable sources (arrays, ArrayList) with no shared mutable state.
For small lists or I/O-bound work it's usually slower — coordination costs more than it saves.
// good — big, CPU-bound
list.parallelStream().map(this::heavyMath).toList();
// bad — shared mutable state, race
List<X> out = new ArrayList<>();
list.parallelStream().forEach(out::add); // broken
How should you NOT use Optional?
- Don't call
get() without checking — that's just an NPE with extra steps. - Don't use it as a field or parameter — it isn't Serializable and adds noise.
- Don't return
Optional<List> — return an empty list. - Don't wrap it in if/else — chain
map/orElse.
// bad
if (opt.isPresent()) return opt.get().getName();
else return "Unknown";
// good
return opt.map(User::getName).orElse("Unknown");
orElse() vs orElseGet() vs orElseThrow().
orElse(x) — x is evaluated always, even when the Optional has a value.
orElseGet(supplier) — lazy; only runs when empty. Use it when the fallback is expensive.
orElseThrow(supplier) — throws when empty.
opt.orElse(loadFromDb()); // ALWAYS hits the DB
opt.orElseGet(() -> loadFromDb()); // only when empty
opt.orElseThrow(() -> new NotFoundException(id));
How do you create a Stream?
collection.stream() — the usual way.Arrays.stream(arr) / Stream.of(a, b, c)IntStream.range(0, n) — primitive, no boxing.Stream.iterate / Stream.generate — infinite, needs limit().Files.lines(path) — lazy file reading (close it!).
IntStream.rangeClosed(1, 5).sum(); // 15
Stream.iterate(1, n -> n * 2).limit(10).toList();
Streams or plain loops — which should you use?
Streams win for declarative transform/filter/group pipelines: less code, clearer intent, easy parallelism.
Loops win for simple iteration, primitives on hot paths, early exit with side effects, and debugging (you can step through and see the stack).
What are the thread states?
NEW → RUNNABLE (via start()) → TERMINATED. In between it may sit in BLOCKED (waiting for a monitor lock), WAITING (wait(), join()), or TIMED_WAITING (sleep(n)).
Thread t = new Thread(task);
t.start(); // new thread
t.run(); // just a method call!
Runnable vs Callable.
Runnable.run() returns void and can't throw checked exceptions.
Callable<V>.call() returns a value and can throw. Submit it to an ExecutorService and you get a Future<V>.
Future<Integer> f = pool.submit(() -> compute());
Integer result = f.get(); // blocks
What does synchronized do?
It gives mutual exclusion (one thread at a time holds the object's monitor) and visibility (changes made inside are visible to the next thread that enters).
On an instance method the lock is this; on a static method it's the Class object. It's reentrant.
private final Object lock = new Object();
void inc() {
synchronized (lock) { count++; }
}
What does volatile do, and when is it not enough?
volatile guarantees visibility (reads/writes go to main memory, no caching) and prevents instruction reordering — but gives no atomicity.
So count++ is still broken: it's read-modify-write, three operations. Use it for a simple flag; use AtomicInteger or a lock for compound updates.
private volatile boolean running = true; // correct use
while (running) { work(); }
private volatile int count;
count++; // BROKEN — not atomic
What are the Atomic classes and how does CAS work?
AtomicInteger, AtomicLong, AtomicReference give lock-free thread-safe updates using compare-and-swap — a single CPU instruction that updates a value only if it still matches the expected one, retrying in a loop otherwise.
Faster than locks under low-to-moderate contention: no blocking, no context switch.
AtomicInteger c = new AtomicInteger();
c.incrementAndGet(); // atomic
c.updateAndGet(v -> v * 2);
c.compareAndSet(5, 10); // only if still 5
What is a deadlock and how do you prevent it?
Two threads each hold a lock the other needs, so both block forever. It needs all four Coffman conditions: mutual exclusion, hold-and-wait, no preemption, circular wait.
Break any one — most practically, impose a global lock ordering so a cycle can't form, or use tryLock(timeout).
// deadlock: thread 1 locks A then B, thread 2 locks B then A
// fix: both lock in a fixed order (e.g. by account id)
if (a.id() < b.id()) { lock(a); lock(b); }
else { lock(b); lock(a); }
What is a race condition?
When correctness depends on the timing of threads. The classic form is check-then-act or read-modify-write: count++ looks like one step but is three, so two threads can interleave and lose an update.
Fix by making the compound action atomic — a lock, an Atomic class, or an atomic map method.
// race — both threads can pass the check
if (!map.containsKey(k)) map.put(k, v);
// atomic
map.putIfAbsent(k, v);
What is ExecutorService and why use a thread pool?
It decouples task submission from thread management. Threads are expensive (~1MB stack each), so a pool reuses a fixed set instead of creating one per task — bounding resource use and avoiding churn.
submit() returns a Future; shut down with shutdown() then awaitTermination().
ExecutorService pool = Executors.newFixedThreadPool(4);
try {
pool.submit(() -> work());
} finally {
pool.shutdown();
pool.awaitTermination(30, TimeUnit.SECONDS);
}
Future vs CompletableFuture.
Future is passive: the only way to get the result is get(), which blocks. No chaining, no combining, no callbacks.
CompletableFuture (Java 8) is composable — thenApply, thenCompose, thenCombine, allOf, plus exceptionally for errors. Fully non-blocking.
CompletableFuture.supplyAsync(() -> fetchUser(id))
.thenApply(User::getName)
.thenCombine(fetchOrders(id), (name, orders) -> render(name, orders))
.exceptionally(ex -> "fallback")
.thenAccept(System.out::println);
How do wait() and notify() work?
wait() releases the monitor and parks the thread until another calls notify()/notifyAll() on the same object. All three must be called while holding that monitor, or you get IllegalMonitorStateException.
Always wait in a while loop, never an if — spurious wakeups are permitted by the spec.
synchronized (lock) {
while (queue.isEmpty()) lock.wait(); // while, not if!
process(queue.poll());
}
sleep() vs wait().
| sleep() | wait() |
|---|
| Defined on | Thread (static) | Object |
| Releases the lock? | No | Yes |
| Woken by | timeout | notify()/timeout |
| Needs a monitor? | no | yes |
ReentrantLock vs synchronized.
ReentrantLock adds what synchronized can't do: tryLock() (with timeout), interruptible acquisition, fairness, and multiple Condition objects.
The cost: you must unlock() in a finally — forget it and the lock is held forever.
lock.lock();
try { work(); }
finally { lock.unlock(); } // MUST be in finally
What is a ReadWriteLock?
It splits the lock in two: many threads may hold the read lock at once, but the write lock is exclusive. Great for read-heavy shared state, since readers no longer block each other.
Java 8's StampedLock goes further with optimistic reads.
ReadWriteLock rw = new ReentrantReadWriteLock();
rw.readLock().lock(); // concurrent
rw.writeLock().lock(); // exclusive
What is ThreadLocal and what's the danger?
It gives each thread its own independent copy of a variable — used for per-request context (user id, transaction, SimpleDateFormat).
The danger: with a thread pool, threads are reused forever, so a value you never remove leaks memory and bleeds into the next request.
static final ThreadLocal<User> CTX = new ThreadLocal<>();
try {
CTX.set(user);
handle();
} finally {
CTX.remove(); // essential with pooled threads
}
What are virtual threads (Java 21)?
Lightweight threads managed by the JVM, not the OS. Millions can exist; when one blocks on I/O it unmounts from its carrier thread instead of parking an OS thread.
So the simple blocking, thread-per-request style finally scales — no reactive rewrite needed.
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
for (var task : tasks) exec.submit(task);
}
CountDownLatch, CyclicBarrier, Semaphore — what's the difference?
- CountDownLatch — wait for N events; one-shot, can't be reset. 'Wait until startup finishes.'
- CyclicBarrier — N threads wait for each other; reusable each round.
- Semaphore — N permits limiting concurrent access. 'At most 10 threads hit this API.'
CountDownLatch latch = new CountDownLatch(3);
latch.countDown();
latch.await();
Semaphore sem = new Semaphore(10);
sem.acquire();
try { call(); } finally { sem.release(); }
What is a BlockingQueue?
A thread-safe queue where put() blocks while full and take() blocks while empty. It implements the whole producer-consumer pattern for you — no wait/notify to get wrong.
A bounded one (ArrayBlockingQueue) also gives you natural backpressure.
BlockingQueue<Task> q = new ArrayBlockingQueue<>(100);
// producer
q.put(task); // blocks when full
// consumer
Task t = q.take(); // blocks when empty
What are the ways to make a class thread-safe?
- Immutability — no mutable state, nothing to race. Best option.
- Confinement — don't share; keep state thread-local or on the stack.
- Synchronization — guard state with a lock.
- Atomics — lock-free CAS for single variables.
- Concurrent collections — let the JDK do it.
What is double-checked locking, and why is volatile required?
An optimization that checks for null, and only then locks and checks again — so the lock is paid once, not on every read.
volatile is mandatory: without it, the JVM may reorder the construction so the reference is published before the object is fully built, letting another thread see a half-initialized object.
private static volatile Singleton instance; // volatile!
static Singleton get() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) instance = new Singleton();
}
}
return instance;
}
Stack vs heap memory.
Stack — one per thread; holds local variables, primitives, and references, in frames pushed/popped per method call. Fast, automatic, small.
Heap — shared by all threads; holds every object and its instance fields. Managed by the GC.
void m() {
int x = 5; // stack
User u = new User(); // reference on stack, object on heap
}
Which garbage collectors does the JVM offer?
- Serial — one thread; tiny heaps/containers.
- Parallel — multi-threaded, throughput-focused, long pauses.
- G1 — the default since Java 9; region-based, aims at a pause-time target. Good all-rounder.
- ZGC / Shenandoah — concurrent, sub-millisecond pauses on huge heaps.
java -XX:+UseG1GC -Xms2g -Xmx2g -jar app.jar
Why is the heap split into generations?
Because of the weak generational hypothesis: most objects die young.
New objects go in Eden; survivors are copied between Survivor spaces, and after enough rounds are promoted to the Old generation. Minor GCs are frequent and cheap; major GCs are rare and expensive.
How does the GC decide an object is garbage?
Reachability, not reference counting. The GC traces from GC roots — thread stacks, static fields, JNI references, active threads — and anything not reached is collectible.
That's why circular references are collected correctly in Java: two objects pointing at each other but unreachable from a root are still garbage.
Can Java have memory leaks? What causes them?
Yes — a leak is an object that's unused but still reachable, so the GC can't touch it. Common causes:
- Static collections that only ever grow (a cache with no eviction).
ThreadLocal not removed on a pooled thread.- Unclosed resources / unregistered listeners.
- A non-static inner class holding its outer instance.
- Mutating a key's hash while it's in a HashMap.
// classic leak — nothing is ever removed
private static final Map<String, User> CACHE = new HashMap<>();
What kinds of OutOfMemoryError are there?
- Java heap space — too many live objects: a leak, or the heap is too small.
- GC overhead limit exceeded — >98% of time in GC reclaiming <2%. Effectively a leak.
- Metaspace — too many loaded classes (classic in app servers that redeploy).
- unable to create new native thread — OS thread limit hit.
java -Xmx2g -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/dump.hprof -jar app.jar
What causes StackOverflowError?
The thread's stack is exhausted — almost always unbounded recursion (a missing or wrong base case), or occasionally mutual recursion or an accidental infinite toString()/equals() loop.
Java has no tail-call optimization, so deep recursion doesn't get flattened — convert it to a loop.
int bad(int n) { return bad(n - 1); } // no base case
How does class loading work?
Three phases: Loading (read the bytecode), Linking (verify → prepare → resolve), Initialization (run static blocks/fields).
Loaders form a chain — Bootstrap → Platform → Application — using parent delegation: each asks its parent first, so nobody can substitute a fake java.lang.String.
ClassNotFoundException vs NoClassDefFoundError.
ClassNotFoundException (checked) — an explicit dynamic lookup like Class.forName("x") found nothing on the classpath.
NoClassDefFoundError (an Error) — the class was present at compile time but is missing at runtime, or it failed its static initialization earlier and the JVM now refuses to use it.
What is the JIT compiler?
The JVM starts by interpreting bytecode, profiles what actually runs, then the JIT compiles hot methods to native code — applying inlining, loop unrolling, and escape analysis.
Because it optimizes on real runtime data, long-running Java can rival C++ — but only after warm-up.
What are the headline features of the recent LTS releases?
- Java 8 — lambdas, Streams, Optional, new date/time API.
- Java 11 —
var in lambdas, HttpClient, String.isBlank/lines/repeat. - Java 17 — records, sealed classes, switch expressions, text blocks, pattern-matching instanceof.
- Java 21 — virtual threads, pattern matching for switch, record patterns, sequenced collections.
Why did Java 8 replace Date and Calendar?
The old API was mutable (so not thread-safe), had a 0-based month, and SimpleDateFormat was famously not thread-safe.
java.time is immutable and clear: LocalDate (no time), LocalDateTime (no zone), ZonedDateTime (zone-aware), Instant (a UTC timestamp), Duration/Period.
LocalDate d = LocalDate.now().plusDays(7);
Instant now = Instant.now(); // UTC timestamp
Duration.between(start, end).toMinutes();
What is reflection and what is it used for?
Inspecting and invoking classes, fields, and methods at runtime by name. It's how Spring does dependency injection, Jackson maps JSON, JUnit finds @Test, and Hibernate populates entities.
Costs: no compile-time safety, slower than direct calls, and it can break encapsulation.
Class<?> c = Class.forName("com.app.User");
Object o = c.getDeclaredConstructor().newInstance();
Method m = c.getMethod("getName");
String name = (String) m.invoke(o);
What are annotations, and how do you write one?
Metadata attached to code, read by the compiler or by frameworks at runtime via reflection.
To be visible at runtime an annotation needs @Retention(RUNTIME); @Target restricts where it can be applied.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Audited {
String value() default "";
}
What is the module system (Java 9)?
JPMS adds strong encapsulation above the package level. A module-info.java declares what the module exports and what it requires — so a public class in a non-exported package is genuinely inaccessible.
It also allows jlink to build a trimmed runtime with only the modules you use.
module com.app.orders {
requires com.app.core;
exports com.app.orders.api; // internals stay hidden
}
Strong, weak, soft and phantom references.
- Strong — the normal kind; never collected while reachable.
- Soft — collected only when memory is tight. Good for caches.
- Weak — collected at the next GC. Used by
WeakHashMap for metadata keyed on objects. - Phantom — for cleanup actions after finalization.
WeakReference<Image> ref = new WeakReference<>(img);
Image i = ref.get(); // may be null — always check
Which JVM flags matter in production?
-Xms / -Xmx — initial/max heap. Set them equal in production to avoid resize pauses.-XX:+UseG1GC, -XX:MaxGCPauseMillis — collector and pause target.-XX:+HeapDumpOnOutOfMemoryError — essential for diagnosing OOM.-XX:MaxRAMPercentage — the right way to size a heap in a container.
java -Xms2g -Xmx2g -XX:+UseG1GC \
-XX:+HeapDumpOnOutOfMemoryError -jar app.jar
How do you diagnose a slow or leaking Java application?
- jstack — thread dump; finds deadlocks and stuck threads.
- jmap + Eclipse MAT — heap dump; finds what's retaining memory.
- jstat — live GC statistics.
- JFR + JDK Mission Control — low-overhead production profiling.
- JMH — trustworthy microbenchmarks.
jstack <pid> # thread dump
jmap -dump:live,format=b,file=heap.hprof <pid>
jstat -gcutil <pid> 1000 # GC every second
What are the SOLID principles?
- Single responsibility — one reason to change.
- Open/closed — open for extension, closed for modification.
- Liskov substitution — a subtype must work anywhere its parent does.
- Interface segregation — many small interfaces beat one fat one.
- Dependency inversion — depend on abstractions, not concretions.
How do you implement a Singleton correctly?
Best: an enum — the JVM guarantees one instance, and it's serialization- and reflection-safe for free.
Otherwise use the holder idiom: a static nested class loaded lazily on first access, made thread-safe by the classloader with no synchronization.
// best
public enum Config { INSTANCE; public void load() { } }
// holder idiom — lazy + thread-safe, no locks
public class Config {
private Config() { }
private static class Holder { static final Config I = new Config(); }
public static Config get() { return Holder.I; }
}
What is the Factory pattern?
It moves object creation behind a method, so callers ask for what they need without knowing which concrete class they get. Adding a new type doesn't touch calling code.
The JDK is full of them: List.of(), Integer.valueOf(), Executors.newFixedThreadPool().
static Notifier of(Channel c) {
return switch (c) {
case EMAIL -> new EmailNotifier();
case SMS -> new SmsNotifier();
};
}
What is the Builder pattern and when do you use it?
It builds an object step by step through a fluent chain — the fix for the telescoping constructor problem, where many optional parameters produce a dozen overloads and unreadable call sites.
It also makes the result immutable and lets you validate in build().
User u = User.builder()
.name("Ann")
.email("a@x.com")
.active(true)
.build();
What is the Strategy pattern?
It puts each algorithm behind a common interface so it can be swapped at runtime — replacing a growing if/else or switch over behaviour types.
In modern Java the strategy is usually just a lambda: Comparator passed to sort() is Strategy.
interface Discount { BigDecimal apply(BigDecimal amt); }
Map<Tier, Discount> rules = Map.of(
Tier.GOLD, amt -> amt.multiply(new BigDecimal("0.8")),
Tier.NONE, amt -> amt);
What is the Observer pattern?
A subject keeps a list of listeners and notifies them all when its state changes — so publishers and subscribers stay decoupled.
It's everywhere: Swing listeners, Spring's ApplicationEvent, RxJava, and any pub/sub system.
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
void publish(Event e) { listeners.forEach(l -> l.onEvent(e)); }
What is the Decorator pattern?
It wraps an object in another with the same interface to add behaviour without subclassing — and the wrappers compose in any order.
The canonical example is java.io: new BufferedReader(new InputStreamReader(in)).
Reader r = new BufferedReader(
new InputStreamReader(
new FileInputStream(file)));
What is dependency injection and why does it matter?
A class receives its collaborators from outside instead of constructing them (new). That inverts control: the class depends on an interface, and the caller (or a container like Spring) decides the implementation.
The payoff is testability — you can pass a mock in a unit test.
// hard to test — the dependency is welded in
class Service { private final Repo r = new MySqlRepo(); }
// injectable, mockable
class Service {
private final Repo r;
Service(Repo r) { this.r = r; }
}
Why favour composition over inheritance?
Inheritance is the tightest coupling in OOP: a subclass depends on its parent's implementation, so a parent change silently breaks it (the fragile base class problem). It's also fixed at compile time and single.
Composition — holding a collaborator and delegating — is swappable at runtime and unaffected by the other class's internals.
// fragile — addAll() calls add() internally, double-counting
class CountingList<E> extends ArrayList<E> { }
// robust — delegate
class CountingList<E> {
private final List<E> inner = new ArrayList<>();
}
How do you structure a typical layered Java backend?
- Controller — HTTP only: parse, validate, map to DTOs. No business logic.
- Service — business rules and transaction boundaries.
- Repository — persistence only.
Each layer depends on the one below through an interface, and entities don't leak out as API responses — map to DTOs.
Why is Java not 100% object-oriented?
Because of primitives — int, double, boolean etc. are not objects. Java also has static members that belong to no instance, and operators that aren't method calls.
Wrapper classes and autoboxing paper over the gap, but the primitives themselves remain non-objects (a deliberate performance choice).
What changed in interfaces in Java 8 (default/static) and Java 9 (private methods)?
Java 8: interfaces can have default methods (implementation inherited by implementors — lets you evolve an API without breaking every implementor, e.g. List.sort) and static methods (utility helpers called on the interface itself).
Java 9: private (and private static) methods — shared helper code between default methods without exposing it.
Fields are still implicitly public static final; there's no state, so interfaces still aren't abstract classes.
interface A { default String hi() { return "A"; } }
interface B { default String hi() { return "B"; } }
class C implements A, B {
@Override public String hi() { return A.super.hi(); } // must resolve the clash
}
Can an abstract class have a constructor? Can it be instantiated?
Yes, it can have a constructor; no, it cannot be instantiated directly with new.
The constructor runs via constructor chaining when a concrete subclass is instantiated — it initializes the fields the abstract class owns. Anonymous subclasses (new AbstractType() { ... }) are still subclasses, not direct instantiation.
abstract class Shape {
private final String name;
protected Shape(String name) { this.name = name; } // runs via super(...)
}
class Circle extends Shape {
Circle() { super("circle"); }
}
What is a covariant return type?
Since Java 5, an overriding method may declare a narrower (subclass) return type than the method it overrides — that's a covariant return type.
It removes casts for callers: Dog.reproduce() can return Dog even though Animal.reproduce() returns Animal. It only works for reference types, never primitives (long → int is illegal).
class Animal { Animal reproduce() { return new Animal(); } }
class Dog extends Animal {
@Override Dog reproduce() { return new Dog(); } // covariant — no cast for callers
}
What is a marker interface? Examples?
An interface with no methods — it only tags a class with a capability that the runtime or a framework checks via instanceof.
Examples: Serializable, Cloneable, RandomAccess. ObjectOutputStream throws NotSerializableException for non-Serializable objects; Object.clone() throws for non-Cloneable ones.
Can you override a static method? A private method? A final method?
No to all three — but for different reasons:
- static — redeclaring it in a subclass is method hiding, not overriding: which one runs depends on the reference type, not the object. No polymorphism.
- private — invisible to the subclass; a same-signature method is simply a brand-new, unrelated method.
- final — compile error; that's the whole point of
final.
class Parent { static String who() { return "parent"; } }
class Child extends Parent { static String who() { return "child"; } }
Parent p = new Child();
p.who(); // "parent" — hiding, resolved at compile time by reference type
throw vs throws?
throw is a statement that actually raises one exception instance: throw new IllegalStateException(...).
throws is a method-signature declaration listing checked exceptions the method may propagate, shifting handling responsibility to the caller.
public void withdraw(double amt) throws InsufficientFundsException { // declares
if (amt > balance) throw new InsufficientFundsException(amt); // raises
}
What is exception propagation?
When an exception isn't caught in the method where it's thrown, it bubbles up the call stack frame by frame until a matching catch is found. If it reaches the top, the thread dies and the default handler prints the stack trace.
Unchecked exceptions propagate silently; checked exceptions propagate only if every method on the way declares them in throws.
void a() { b(); } // no handler -> propagates out of a()
void b() { c(); } // no handler -> propagates out of b()
void c() { int x = 1 / 0; } // ArithmeticException starts here
What are the rules for exceptions when overriding a method?
An override may throw:
- the same checked exceptions, narrower (subclass) ones, fewer, or none at all;
- any unchecked exceptions — they're never part of the contract.
It may not throw new or broader checked exceptions — callers coded against the parent type would face exceptions they never had to handle.
Related override rules: access modifier can only widen (protected → public), return type may be covariant.
class Parent { void read() throws IOException {} }
class Child extends Parent {
@Override void read() throws FileNotFoundException {} // OK: narrower
// void read() throws Exception {} // compile error: broader
}
final vs finally vs finalize()?
Three unrelated things that only sound alike:
- final — keyword: constant variable, non-overridable method, non-extendable class.
- finally — block after try/catch that runs regardless of exceptions; used for cleanup.
- finalize() — a method the GC might call before reclaiming an object. Unpredictable, slow, deprecated since Java 9 and removed for good reasons — never rely on it; use try-with-resources or
Cleaner.
IdentityHashMap, WeakHashMap, EnumMap — what are they for?
- IdentityHashMap — compares keys with
== instead of equals(). Used by frameworks for object-graph traversal (serialization, deep-copy) where two equal-but-distinct objects must stay separate. - WeakHashMap — keys are held by weak references; once a key has no strong references elsewhere, GC removes the entry. Classic for memory-sensitive caches keyed by objects you don't own.
- EnumMap — keys are a single enum type, backed by a plain array indexed by ordinal. Faster and leaner than HashMap; iteration follows enum declaration order.
Map<Status, Handler> handlers = new EnumMap<>(Status.class); // not HashMap
Iterator vs ListIterator vs Enumeration?
- Iterator — universal (any Collection), forward-only, supports
remove(), fail-fast. - ListIterator — lists only; adds bidirectional traversal (
hasPrevious/previous), index access, and set()/add() during iteration. - Enumeration — legacy (Vector/Hashtable), forward-only, no remove, not fail-fast. Only met in old APIs; prefer Iterator.
ListIterator<String> it = list.listIterator(list.size());
while (it.hasPrevious()) System.out.println(it.previous()); // reverse walk
Vector vs ArrayList?
Both are resizable arrays. Vector (Java 1.0) synchronizes every method and grows by 100%; ArrayList is unsynchronized and grows by 50%.
Vector's per-method locking is both slow and insufficient (iteration still needs external locking), so it's effectively deprecated. Need thread safety? Use CopyOnWriteArrayList or Collections.synchronizedList().
findFirst vs findAny? anyMatch/allMatch/noneMatch? What are short-circuit operations?
findFirst() returns the first element in encounter order; findAny() returns whichever is found first — identical for sequential streams, but findAny is faster on parallel streams because it doesn't coordinate ordering. Both return Optional.
anyMatch / allMatch / noneMatch take a Predicate and return boolean.
Short-circuit operations stop processing as soon as the result is known: findFirst/findAny, the three matchers, limit(), takeWhile() — an infinite stream + limit still terminates.
boolean hasAdmin = users.stream().anyMatch(u -> u.role() == ADMIN); // stops at first hit
Optional<User> any = users.parallelStream().findAny(); // no order penalty
What is the difference between peek() and map()?
map() transforms — takes a Function, replaces each element with its result. peek() observes — takes a Consumer, passes each element through unchanged.
peek() is intended for debug logging mid-pipeline. Don't use it for real side effects: it only runs when a terminal operation pulls elements, and the JVM may skip it entirely when it can prove the result doesn't need it (e.g. count()).
List<String> out = names.stream()
.peek(n -> log.debug("raw: {}", n)) // observe
.map(String::toUpperCase) // transform
.toList();
How do you sort in a stream with a Comparator? (employees by salary desc, then name)
sorted() uses natural ordering (elements must be Comparable); sorted(Comparator) takes any ordering. Build comparators declaratively with Comparator.comparing + reversed() / thenComparing.
sorted() is a stateful intermediate operation — it must buffer the whole stream before emitting anything.
List<Employee> sorted = employees.stream()
.sorted(Comparator.comparingDouble(Employee::salary).reversed()
.thenComparing(Employee::name))
.toList();
Process vs thread?
A process is a running program with its own memory space; a thread is an execution path inside a process, sharing its heap with sibling threads while keeping a private stack, program counter and registers.
- Processes are isolated — one crashing doesn't kill another; threads share state, so one bad thread can corrupt or crash the whole process.
- Thread creation/context-switching is much cheaper; inter-thread communication is just shared memory (which is exactly why synchronization is needed).
start() vs run()? What if you call start() twice?
start() asks the JVM to create a new OS thread which then calls run(). Calling run() directly is just a normal method call — everything executes on the current thread, no concurrency at all.
Calling start() twice throws IllegalThreadStateException — a thread that has left NEW can never be restarted; create a new Thread instead.
Thread t = new Thread(() ->
System.out.println(Thread.currentThread().getName()));
t.run(); // "main" — plain method call
t.start(); // "Thread-0" — real new thread
// t.start(); again -> IllegalThreadStateException
sleep() vs wait() vs yield() vs join()?
- sleep(ms) — static; pauses the current thread for a fixed time, keeps its locks.
- wait() — on an object; releases that object's lock and parks until notify/notifyAll; must be inside synchronized.
- yield() — a hint to the scheduler to let equal-priority threads run; may do nothing, no lock release. Rarely useful outside benchmarks.
- join() — current thread blocks until the target thread finishes; the standard "wait for the worker before reading its result".
Thread worker = new Thread(this::process);
worker.start();
worker.join(); // main waits for worker to die
System.out.println(result); // safe to read now
synchronized method vs block vs static synchronized — and object-level vs class-level locks?
Every object has an intrinsic monitor; so does every Class object.
- synchronized instance method — locks
this (object-level lock). All synchronized instance methods of the same object exclude each other. - synchronized block — locks any object you choose, for as few lines as possible: less contention, and you can use a private lock object outsiders can't touch.
- static synchronized — locks
MyClass.class (class-level lock), shared across all instances.
Object-level and class-level locks are independent: a static and an instance synchronized method can run at the same time.
class Counter {
private final Object lock = new Object();
private int n;
void inc() {
synchronized (lock) { n++; } // narrow scope, private lock
}
}
What are livelock and starvation?
Both are liveness failures where threads are alive but no useful work happens:
- Livelock — threads keep actively responding to each other without progressing (two threads each releasing and retrying a lock in the same rhythm, like two people side-stepping in a corridor forever). CPU is busy, nothing completes. Fix: add randomized backoff/jitter to retries.
- Starvation — a thread never gets the resource because others always win: greedy high-priority threads, or an unfair lock always granted to newer requesters. Fix: fair locks (
new ReentrantLock(true)), bounded queues, avoid priority abuse.
Explain ThreadPoolExecutor's parameters (core size, max size, queue, rejection policies).
ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler). On submit:
- Fewer than corePoolSize threads → create a new thread even if others are idle.
- Core full → queue the task.
- Queue full → grow up to maximumPoolSize.
- Max reached and queue full → the RejectedExecutionHandler fires.
Rejection policies: AbortPolicy (default, throws), CallerRunsPolicy (caller executes it — natural backpressure), DiscardPolicy, DiscardOldestPolicy.
new ThreadPoolExecutor(4, 8, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy());
submit() vs execute()?
- execute(Runnable) — from
Executor; fire-and-forget, returns void. An uncaught exception kills the worker thread and reaches its UncaughtExceptionHandler (you'll see the stack trace). - submit(Runnable/Callable) — from
ExecutorService; returns a Future. Exceptions are captured inside the Future and only surface when you call get() (as ExecutionException).
The classic bug: submit() a task, never call get() — the exception vanishes silently.
Future<Integer> f = pool.submit(() -> parse(payload)); // Callable<Integer>
try { int v = f.get(); }
catch (ExecutionException e) { log.error("task failed", e.getCause()); }
What are daemon threads?
Background service threads that don't keep the JVM alive — the JVM exits as soon as the last non-daemon (user) thread finishes, killing daemons abruptly mid-work (finally blocks may never run).
Examples: the garbage collector, JIT compiler threads. Set with setDaemon(true) before start() — after start it throws. A thread inherits daemon status from its creator.
Thread monitor = new Thread(this::pollMetrics);
monitor.setDaemon(true); // must be before start()
monitor.start();
What is the fork/join framework?
A Java 7 framework for divide-and-conquer parallelism: a task recursively forks into subtasks until small enough, computes, then joins results. Implemented by ForkJoinPool with RecursiveTask<V> (returns a value) / RecursiveAction (void).
Its key idea is work stealing: each worker has its own deque; idle workers steal tasks from the tail of busy workers' deques, keeping all cores busy without a central bottleneck.
parallelStream() and CompletableFuture run on the shared ForkJoinPool.commonPool() by default.
class SumTask extends RecursiveTask<Long> {
// if (hi - lo < THRESHOLD) sum directly;
// else { left.fork(); right.compute(); left.join(); }
}
What are the JVM runtime memory areas (heap, stack, metaspace, PC register, native stack)?
- Heap — all objects and arrays; shared by all threads; GC-managed; sized by -Xms/-Xmx.
- Java stacks — one per thread; a frame per method call holding locals, operand stack, return address. Overflow → StackOverflowError.
- Metaspace — class metadata (bytecode, method tables); native memory, replaced PermGen in Java 8.
- PC register — per thread, the address of the current bytecode instruction.
- Native method stacks — for JNI/C code.
Also: the code cache (JIT-compiled machine code) and direct/off-heap buffers (NIO).
PermGen vs Metaspace?
Both store class metadata; Java 8 replaced PermGen with Metaspace.
- PermGen (≤ Java 7) — a fixed-size region inside the JVM heap; a hardcoded default (~64–85 MB) made
OutOfMemoryError: PermGen space a routine failure on app servers with many redeploys. - Metaspace (Java 8+) — native memory, grows dynamically by default (cap with
-XX:MaxMetaspaceSize). Class metadata is freed when its classloader is collected.
Classloader leaks still cause Metaspace OOM — it just takes longer to hit.
Streams coding drills: second-highest salary, group by department, highest paid per department, List→Map.
The four employee-stream exercises that dominate Java 8 coding rounds. The building blocks: sorted + distinct + skip/limit for ranking, groupingBy for buckets, groupingBy + maxBy (or toMap with a merge) for per-group winners, and toMap with a merge function for duplicate keys.
// 1) second highest salary
Optional<Double> second = employees.stream()
.map(Employee::salary).distinct()
.sorted(Comparator.reverseOrder())
.skip(1).findFirst();
// 2) count per department
Map<String, Long> byDept = employees.stream()
.collect(Collectors.groupingBy(Employee::dept, Collectors.counting()));
// 3) highest paid per department
Map<String, Optional<Employee>> topPaid = employees.stream()
.collect(Collectors.groupingBy(Employee::dept,
Collectors.maxBy(Comparator.comparingDouble(Employee::salary))));
// 4) List -> Map<id, Employee>, duplicate keys handled
Map<Long, Employee> byId = employees.stream()
.collect(Collectors.toMap(Employee::id, e -> e, (a, b) -> b));
Streams coding drills: first non-repeating character, word/char frequency, join names.
String-processing in streams hinges on one bridge: s.chars() gives an IntStream — box to Character for grouping. Frequency = groupingBy(identity, counting()); ordered frequency needs a LinkedHashMap supplier; joining = Collectors.joining(", ").
// word frequency in a sentence
Map<String, Long> freq = Arrays.stream(sentence.toLowerCase().split("\\s+"))
.collect(Collectors.groupingBy(w -> w, Collectors.counting()));
// first non-repeating character
Character first = s.chars().mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(c -> c, LinkedHashMap::new, Collectors.counting()))
.entrySet().stream().filter(e -> e.getValue() == 1)
.map(Map.Entry::getKey).findFirst().orElse(null);
// join names
String joined = employees.stream().map(Employee::name)
.collect(Collectors.joining(", "));
Streams coding drills: duplicates, sort a map by value, flatten, partition even/odd, sum/average, nth highest.
The remaining regulars: duplicates via a seen-Set inside filter (or frequency map), map-by-value sorting via entrySet().stream().sorted(comparingByValue()) into a LinkedHashMap, flatMap for List-of-Lists, partitioningBy for the even/odd split, IntStream sum()/average(), and distinct+sorted+skip(n−1) for nth highest.
// duplicates in a list
Set<Integer> seen = new HashSet<>();
List<Integer> dups = nums.stream().filter(n -> !seen.add(n)).toList();
// sort map by value desc -> LinkedHashMap
Map<String, Integer> sorted = map.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(a, b) -> a, LinkedHashMap::new));
// flatten, partition, aggregate, nth highest
List<String> flat = listOfLists.stream().flatMap(List::stream).toList();
Map<Boolean, List<Integer>> evenOdd = nums.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
double avg = nums.stream().mapToInt(Integer::intValue).average().orElse(0);
int nth = nums.stream().distinct()
.sorted(Comparator.reverseOrder()).skip(n - 1).findFirst().orElseThrow();
Print odd/even numbers alternately with two threads (and 1..N with 3 threads).
The classic coordination exercise: a shared counter + a shared lock. Each thread waits until it's its turn (counter parity), prints, increments, and notifyAll(). The same pattern generalizes to N threads with counter % N == myIndex — which answers the 3-thread variant too.
class AlternatePrinter {
private int n = 1;
private final int max;
AlternatePrinter(int max) { this.max = max; }
synchronized void print(int parity) throws InterruptedException {
while (n <= max) {
while (n <= max && n % 2 != parity) wait(); // not my turn
if (n > max) break;
System.out.println(Thread.currentThread().getName() + ": " + n++);
notifyAll();
}
}
}
// main:
var p = new AlternatePrinter(10);
new Thread(() -> { try { p.print(1); } catch (Exception e) {} }, "odd").start();
new Thread(() -> { try { p.print(0); } catch (Exception e) {} }, "even").start();
// 3 threads printing 1..N: same monitor, condition n % 3 == myIndex
Adapter, Facade, Proxy, Template Method — and where do Spring/Angular actually use design patterns?
- Adapter — converts one interface to another so incompatible things collaborate (
Arrays.asList, Spring MVC's HandlerAdapter). - Facade — one simple entry point over a messy subsystem (
JdbcTemplate hiding raw JDBC; NgRx facade services). - Proxy — a stand-in controlling access to the real object — the Spring pattern:
@Transactional, @Async, AOP, lazy-loading Hibernate entities are all proxies. - Template Method — skeleton algorithm in a base class, steps overridden (
JdbcTemplate/RestTemplate — it's in the names).
Framework map to recite: Spring beans = singleton, BeanFactory = factory, RxJS/ApplicationEvents = observer, interceptor/filter chains = chain of responsibility, RowMapper/Comparator injection = strategy, HttpClient feature wrapping = decorator, DI itself = inversion of control.
// Template Method you already use daily:
jdbcTemplate.query(sql, (rs, i) -> new User(rs.getLong(1)));
// JdbcTemplate owns connection/statement/exception steps — you supply one step.
JUnit 5 essentials — @Test, lifecycle hooks, assertThrows, @ParameterizedTest?
Core annotations: @Test, @BeforeEach/@AfterEach (fresh per test), @BeforeAll/@AfterAll (static, once), @DisplayName, @Disabled, @Nested for grouping.
Assertions: assertEquals/assertTrue/assertAll (grouped soft asserts), and assertThrows — the modern exception test, returning the exception for message checks.
@ParameterizedTest — one test, many inputs via @ValueSource, @CsvSource, @MethodSource — the answer to "how do you avoid copy-pasted tests".
@ParameterizedTest
@CsvSource({ "racecar,true", "hello,false", "'',true" })
void palindrome(String input, boolean expected) {
assertEquals(expected, Palindrome.check(input));
}
@Test
void withdrawRejectsOverdraft() {
var ex = assertThrows(InsufficientFundsException.class,
() -> account.withdraw(1_000_000));
assertTrue(ex.getMessage().contains("balance"));
}
Why is String allowed in switch but not long?
switch compiles to constant-time jump instructions (tableswitch/lookupswitch) that operate on int. Allowed types are things reducible to int: byte/short/char/int, their wrappers, and enums (ordinal). String (Java 7+) works via a compiler trick — switch on hashCode() plus an equals() confirmation for collisions.
long/float/double don't fit the int-based bytecode (and floats can't be compared exactly) — hence not allowed. Modern pattern-matching switch (Java 21) extends to type patterns, but the primitive rule stands.
switch (command) { // compiles to hashCode switch + equals check
case "start" -> start();
case "stop" -> stop();
}
// switch (someLong) {} // compile error: incompatible types
Can main() be private? Overloaded? Final? What if there's no main at all?
- Overloaded — yes, freely; the JVM only ever launches
public static void main(String[]), other overloads are normal methods. - final — yes; final only forbids overriding, which is irrelevant for a static entry point.
static is required (no instance exists yet). - private — compiles fine, but the launcher refuses to run it: "main method not found"-style error at launch, not a compile error.
- No main — compiles fine too; running it fails at launch. (Java 21+ preview relaxes this with unnamed classes/instance main.)
public class App {
public static void main(String[] args) { main(42); } // launched
static void main(int x) {} // just an overload
}
What does "abc" + 1 + 2 print vs 1 + 2 + "abc"?
+ evaluates left to right, and its meaning flips per operand pair: once either side is a String, it's concatenation forever after.
"abc" + 1 + 2 → ("abc" + 1) + 2 → "abc12"1 + 2 + "abc" → (1 + 2) + "abc" → "3abc" — the ints add first.
System.out.println("abc" + 1 + 2); // abc12
System.out.println(1 + 2 + "abc"); // 3abc
System.out.println("" + (1 + 2)); // 3
System.out.println('a' + 1); // 98 — char + int is arithmetic!