Java Interview Questions & Answers
Core Java interview questions covering language fundamentals, collections, concurrency, and JVM internals -- the questions hiring managers actually ask for backend and full-stack roles.
20 Questions
~30 min read
Beginner: 3
Intermediate: 11
Advanced: 6
Coding 5
== compares references (or primitive values); .equals() compares logical content, if overridden.
Detailed Answer
For primitives, == compares actual values. For objects, == checks whether two references point to the same object in memory, while .equals() (when meaningfully overridden, as in String or Integer) compares the objects' contents. Two separate String objects with the same characters are == false but .equals() true.
Best Practices
Override equals() alongside hashCode() together, never one without the other.
Common Mistakes
Using == to compare boxed types like Integer or String and getting lucky due to caching (e.g. Integer caching -128..127), then being surprised when it breaks outside that range.
ArrayList is backed by a resizable array (fast random access); LinkedList is a doubly-linked list (fast insert/remove at the ends).
Detailed Answer
ArrayList offers O(1) get(index) but O(n) insertion/removal in the middle since elements shift. LinkedList offers O(1) insertion/removal once you have a reference to the node, but O(n) random access since it must traverse from an end. In practice ArrayList is the default choice; LinkedList is rarely faster in real workloads because of pointer-chasing cache misses.
Best Practices
Default to ArrayList unless you've measured a specific insertion/removal pattern that benefits from LinkedList.
Common Mistakes
Assuming LinkedList is generally faster for insertions -- modern CPUs favor ArrayList's contiguous memory even for many inserts, since LinkedList's node overhead and cache-unfriendliness often dominate.
HashMap is unsynchronized and allows one null key/many null values; Hashtable is synchronized (legacy) and disallows nulls.
Detailed Answer
Hashtable is a legacy class from Java 1.0 where every method is synchronized, making it thread-safe but slow under concurrency. HashMap is not synchronized and permits a single null key and multiple null values. For thread-safe maps today, prefer ConcurrentHashMap over Hashtable -- it offers much better concurrent throughput via lock striping.
Best Practices
Use ConcurrentHashMap instead of Hashtable for new code that needs thread safety.
String is immutable; StringBuilder is a mutable, unsynchronized character sequence; StringBuffer is the same as StringBuilder but synchronized (thread-safe, slower).
Detailed Answer
Every String concatenation creates a new object because String is immutable, which is wasteful in a loop. StringBuilder mutates an internal buffer in place, making it much faster for repeated concatenation, but it isn't thread-safe. StringBuffer offers the same mutable API with synchronized methods, at a performance cost that's rarely worth it in single-threaded code.
Best Practices
Use StringBuilder for string building in loops; reach for StringBuffer only if the same builder instance is genuinely shared across threads.
Common Mistakes
Concatenating strings with + inside a loop, which silently creates a new StringBuilder (or String) per iteration in older bytecode patterns and adds up to real overhead at scale.
Streams let you express filter/map/reduce pipelines over a collection declaratively instead of writing manual loops, and can run sequentially or in parallel.
Detailed Answer
A stream pipeline like list.stream().filter(x -> x.isActive()).map(x -> x.getName()).collect(Collectors.toList()) reads as a description of the transformation rather than imperative loop-and-accumulate code. Streams are lazy -- intermediate operations like filter/map don't run until a terminal operation like collect/forEach/reduce triggers the pipeline -- which lets the JVM optimize the whole chain.
Best Practices
Keep stream pipelines free of side effects (don't mutate external state inside map/filter); use parallelStream() only after measuring, since it isn't automatically faster.
Common Mistakes
Overusing streams for simple loops where a plain for loop would be clearer and equally performant, or calling parallelStream() reflexively and getting worse performance due to thread-pool overhead on small collections.
Conceptual 12
JVM runs bytecode; JRE is the JVM plus core libraries needed to run Java programs; JDK is the JRE plus development tools (compiler, debugger).
Detailed Answer
The JVM (Java Virtual Machine) is the runtime engine that executes compiled .class bytecode and is platform-specific. The JRE (Java Runtime Environment) bundles the JVM with the standard class libraries needed to run applications. The JDK (Java Development Kit) is the full toolkit for developers -- it includes the JRE plus javac, jdb, and other build/debug tools.
Overloading is same method name with different parameters in the same class (compile-time); overriding is a subclass redefining a superclass method with the same signature (runtime).
Detailed Answer
Overloading is resolved at compile time based on the argument types/count -- it's a form of static polymorphism. Overriding happens between a superclass and subclass with an identical method signature, and which implementation runs is decided at runtime based on the actual object type (dynamic dispatch), which is how Java achieves runtime polymorphism.
Common Mistakes
Believing overloaded methods are chosen based on the declared reference type at runtime rather than the compile-time argument types -- overload resolution is static, not dynamic.
An abstract class can hold state and partial implementation and supports single inheritance; an interface defines a contract (plus default methods since Java 8) and supports multiple inheritance of type.
Detailed Answer
Abstract classes can have constructors, instance fields, and a mix of implemented and unimplemented methods, but a class can only extend one abstract class. Interfaces historically only declared method signatures, but since Java 8 they can include default and static methods; a class can implement many interfaces. Choose an abstract class when subclasses share common state/behavior, and an interface when you're defining a capability multiple unrelated classes can support.
The JVM automatically reclaims memory for objects with no remaining reachable references, typically using a generational heap and one of several collectors (G1, ZGC, etc.).
Detailed Answer
Most JVMs split the heap into a young generation (where most objects die quickly, collected via fast minor GCs) and an old generation (for long-lived objects, collected less often via major/full GCs). A garbage collector traces reachability from GC roots (stack references, static fields, etc.); anything unreachable becomes eligible for collection. Modern collectors like G1 or ZGC aim to keep pause times short and predictable even on large heaps.
Best Practices
Avoid manually calling System.gc() in application code -- it's only a hint and rarely helps; instead tune collector flags and heap size based on measured GC logs.
Common Mistakes
Assuming setting an object reference to null is required for garbage collection in every case -- it only matters when the reference would otherwise keep the object reachable longer than intended (e.g. in a long-lived collection).
Encapsulation, inheritance, polymorphism, and abstraction -- implemented via access modifiers, extends/implements, method overriding/overloading, and abstract classes/interfaces respectively.
Detailed Answer
Encapsulation bundles data and behavior together and restricts direct access via private fields with public getters/setters. Inheritance lets a class reuse and extend another's behavior via extends. Polymorphism lets the same method call behave differently depending on the actual object type, achieved through overriding and interfaces. Abstraction hides implementation detail behind a simpler interface, achieved through abstract classes and interfaces.
The Java Memory Model (JMM) defines how threads interact through memory; volatile guarantees visibility of writes across threads and prevents instruction reordering around that variable.
Detailed Answer
Without synchronization, one thread's write to a shared variable isn't guaranteed to be visible to another thread due to CPU caching and compiler reordering. Declaring a field volatile forces every read to go to main memory and every write to flush immediately, and it establishes a happens-before relationship that prevents the JIT/CPU from reordering instructions across that access. volatile guarantees visibility and ordering, but not atomicity -- volatile int counter++ is still not thread-safe.
Best Practices
Use volatile for simple flags (e.g. a shutdown boolean); use AtomicInteger/AtomicReference or explicit locks when you need atomic read-modify-write.
Common Mistakes
Assuming volatile makes compound operations like increment thread-safe -- it only guarantees visibility, not atomicity.
Checked exceptions must be declared or caught at compile time (extend Exception, not RuntimeException); unchecked exceptions don't require this (extend RuntimeException).
Detailed Answer
Checked exceptions represent recoverable conditions the caller is expected to handle -- the compiler forces a try/catch or a throws declaration (e.g. IOException). Unchecked exceptions (RuntimeException and its subclasses, like NullPointerException or IllegalArgumentException) typically represent programming errors and don't require explicit handling, though they can still be caught.
Best Practices
Reserve checked exceptions for conditions callers can realistically recover from; overusing them tends to push developers toward swallowing exceptions just to satisfy the compiler.
Common Mistakes
Catching a broad Exception (or worse, Throwable) just to suppress a compiler error, which can silently swallow real bugs alongside the expected checked exception.
If two objects are equal per equals(), they must return the same hashCode(); the reverse isn't required.
Detailed Answer
Hash-based collections like HashMap and HashSet rely on hashCode() to pick a bucket and equals() to confirm identity within that bucket. If you override equals() without overriding hashCode() consistently, two "equal" objects can land in different buckets and a HashSet will treat them as distinct, breaking lookups and deduplication.
Best Practices
Always override both together, and generate them from the same set of fields.
Common Mistakes
Overriding equals() alone (common with IDE-generated code or Lombok misconfiguration), which silently breaks HashMap/HashSet behavior for that type.
synchronized is a built-in, simpler mutual-exclusion mechanism; ReentrantLock offers more control -- tryLock, timed locking, interruptible waits, and fairness policies.
Detailed Answer
synchronized blocks/methods are easier to reason about and are automatically released even on exceptions, but they're all-or-nothing: a thread blocks indefinitely waiting for the lock. ReentrantLock (from java.util.concurrent.locks) lets you attempt a lock with a timeout, respond to interruption while waiting, and choose a fair ordering policy, at the cost of manually calling unlock() in a finally block.
Best Practices
Prefer synchronized for simple cases; reach for ReentrantLock only when you need its extra capabilities, and always unlock() in a finally block.
Common Mistakes
Forgetting to release a ReentrantLock in a finally block, which can deadlock the application if an exception is thrown while the lock is held.
Dependency injection supplies an object's collaborators from the outside rather than having it construct them itself, decoupling components and making them easier to test and reconfigure.
Detailed Answer
Instead of a class calling `new SomeService()` internally, the dependency is passed in (via constructor, setter, or field) by a container like Spring. This inverts control of object creation, letting you swap implementations (e.g. a mock in tests, a different provider in production) without touching the consuming class, and it makes wiring explicit rather than hidden inside constructors.
Best Practices
Prefer constructor injection over field injection -- it makes dependencies explicit and required, and makes classes easier to unit test without a DI container.
Fail-fast iterators (e.g. ArrayList's) throw ConcurrentModificationException if the collection is structurally modified during iteration; fail-safe iterators (e.g. CopyOnWriteArrayList's) iterate over a snapshot and don't throw.
Detailed Answer
Fail-fast iterators detect concurrent structural modification via a modCount check and throw immediately to surface the bug rather than risk undefined behavior. Fail-safe iterators (used by concurrent collections like CopyOnWriteArrayList or ConcurrentHashMap) work against a separate snapshot or use internal synchronization, so they tolerate concurrent modification but may not reflect the very latest updates during that iteration.
Common Mistakes
Modifying a list directly inside a for-each loop instead of using the iterator's own remove() method, or a proper CopyOnWriteArrayList/ConcurrentHashMap when concurrent mutation during iteration is expected.
Generics give compile-time type safety for collections and classes (e.g. List); type erasure means that type information is removed at compile time, so it doesn't exist at runtime.
Detailed Answer
Generics let the compiler catch type mismatches before runtime (e.g. adding an Integer to a List won't compile). But due to type erasure, the compiled bytecode doesn't retain the generic type parameter -- List and List are both just List at runtime, which is why you can't do `new T[]` or check `if (obj instanceof List)` directly.
Common Mistakes
Assuming you can overload two methods that differ only by generic type parameter (e.g. process(List) vs process(List)) -- after erasure they have the same signature and won't compile.
Performance 1
Autoboxing wraps primitives in objects (e.g. int to Integer) automatically, which adds allocation overhead and can hide costly operations inside tight loops.
Detailed Answer
Every autobox creates a new wrapper object (outside the small cached range for Integer, -128 to 127), which means extra heap allocation and GC pressure. In a hot loop -- e.g. summing into a boxed Long inside a stream or a Map counter -- this overhead adds up. Using primitive collections or primitive accumulator variables avoids the churn.
Best Practices
Prefer primitive types in performance-sensitive loops and be deliberate about where boxed types (used for generics, nullability) are actually needed.
Behavioral 1
A strong answer profiles first to find the real bottleneck, makes a targeted change, and measures again -- rather than guessing.
Detailed Answer
Interviewers are listening for a methodical process: identifying the bottleneck with a profiler or benchmarking rather than assuming, making one change at a time (e.g. replacing a linear search with a HashMap lookup, batching database calls, or reducing unnecessary object allocation in a hot loop), and validating the improvement with real measurements before and after. Bonus points for mentioning trade-offs considered (readability vs. speed) and how the fix was verified in production.
Best Practices
Always measure before and after a performance change; don't optimize code that isn't demonstrably a bottleneck.
Common Mistakes
Optimizing the first thing that looks slow rather than the thing that's actually measured to be slow -- premature optimization that doesn't move the needle.
Scenario-Based 1
The simplest robust approach is an enum singleton, or a static holder class (initialization-on-demand holder idiom) for lazy, thread-safe initialization without explicit synchronization.
Detailed Answer
A plain lazy-initialized singleton with a null check is not thread-safe -- two threads can both pass the null check and create two instances. Double-checked locking with a volatile field fixes this but is easy to get subtly wrong. The two idioms considered best practice are: (1) an enum with a single constant, which the JVM guarantees is instantiated exactly once and is serialization-safe by default, or (2) a private static holder class, whose static field is only initialized the first time it's referenced, relying on the JVM's class-loading guarantees for thread safety without any manual locking.
Best Practices
Prefer the static holder idiom or an enum singleton over double-checked locking, since both give correctness without hand-rolled synchronization.
Common Mistakes
Writing double-checked locking without marking the instance field volatile, which can let another thread see a partially constructed object due to instruction reordering.
No questions match your filters.
Related Skills
Continue Your Career Journey
Java Developer Roadmap
Java Developer Salary Guide
Java Developer Resume Resume Template
Java Developer Mock Interview Mock Interview