Java core interview questions: OOP, strings, collections, exceptions, multithreading, memory and JVM internals.
30 questions
Three layers of the Java platform:
.class files). It is platform specific, which is how Java achieves Write Once, Run Anywhere.javac, jar and debugging tools; needed to compile Java code.Memory-wise: JVM inside JRE, JRE inside JDK.
Java source (.java) is compiled by javac not into native machine code, but into platform-neutral bytecode (.class). The bytecode is the same everywhere; only the JVM is platform specific. On each OS you install the JVM for that platform, and it interprets/JIT-compiles the same bytecode into native instructions.
So the code is portable while the runtime is not - that is the whole point of the JVM.
All exceptions and errors derive from Throwable:
RuntimeException; they do not need to be declared.
Java forbids a class from extending more than one class, which eliminates the classic diamond problem where a class inherits the same method from two parents.
However, a class can implement many interfaces, and since Java 8 interfaces can have default methods. This reintroduces a diamond: two interfaces declare the same default method. The rule is that the class must override the conflicting method, or explicitly pick one with InterfaceName.super.method().

String is immutable - every change creates a new object, which is slow in loops. It is safe to share and use as a map key.
StringBuffer is mutable and thread-safe (all methods are synchronized), but synchronization costs performance.
StringBuilder is also mutable but not thread-safe, so it is faster. In single-threaded code always prefer StringBuilder; use StringBuffer only when a String must be shared across threads.
A HashMap is an array of buckets plus a hash function. put(key, value) computes hash(key) % capacity to pick a bucket, then stores an entry there. If two keys map to the same bucket, a collision occurs and entries are linked in a list. get walks the chain comparing keys with equals.
Since Java 8, when a bucket reaches 8 entries the list is converted to a red-black tree, improving worst-case lookup from O(n) to O(log n). The tree is demoted back to a list when the bucket drops below 6 entries.
Important: keys must implement hashCode and equals correctly, otherwise lookups break.
volatile guarantees visibility: every read sees the latest write from another thread. It does not guarantee atomicity - count++ on a volatile field is still three operations (read, add, write).
synchronized gives both visibility and exclusivity: only one thread can enter the block, so compound operations like count++ are safe. Use a lock or an atomic class (AtomicInteger) for counters, and volatile only for flags/status values.
Stack holds method call frames: local variables and references, per thread. It is fast and freed when the method returns (StackOverflowError when too deep).
Heap holds all objects and arrays, shared by all threads. It is managed by the garbage collector and split into generations (young/old).
Metaspace (since Java 8) holds class metadata; it replaced PermGen and uses native memory. Garbage in the heap is reclaimed when objects are no longer reachable.
== compares references for objects (whether both variables point to the same object) and primitive values for primitives. equals() is a method that compares the logical content of two objects and can be overridden.
So new Integer(10) == new Integer(10) is false, while a.equals(b) can be true. Always override hashCode() together with equals() - equal objects must have equal hash codes.
The contract is that two objects which are equal must have the same hashCode. Data structures like HashMap and HashSet first compare hash codes, then use equals() to confirm. If you only override equals() with the same hash, lookups break: contains() and get() may not find equal objects.
An abstract class can have state (fields), constructors, concrete methods and single inheritance. An interface traditionally declares only method signatures, but since Java 8 interfaces can have default and static methods, and since Java 9 private methods.
Use an abstract class for a common base with shared implementation/state; use an interface for a contract that many unrelated classes can implement (a class can implement multiple interfaces but extend only one class).
Checked exceptions (IOException, SQLException) are checked at compile time; the compiler forces you to handle them with try/catch or declare them with throws. They represent recoverable conditions outside your control.
Unchecked exceptions (subclasses of RuntimeException: NullPointerException, IllegalArgumentException) are checked only at runtime; nothing forces you to declare them. They usually indicate programming bugs.
The finally block executes always after try/catch completes - whether an exception was thrown or not - and is used to release resources (close files, connections). It is skipped only if the JVM exits (System.exit) or the thread is killed while in the try block.
Best practice: use try-with-resources for AutoCloseable resources, which calls close() automatically and is cleaner than finally.
Yes, you can, but it is discouraged: if a finally block returns a value, that value overrides any return value from the try block, and a thrown exception from the try block is silently swallowed. This makes bugs very hard to find - never return from finally.
Two ways: extend Thread and override run(), or implement Runnable and pass it to a Thread. Prefer Runnable because Java has single inheritance - implementing keeps your class free to extend something else, and it decouples the task from the thread.
For higher level concurrency prefer ExecutorService, which manages a thread pool for you.
Thread.sleep(ms) pauses the current thread for the given time; it does not release any monitor you hold and is for timing/pacing. Object.wait() releases the monitor and puts the thread into the waiting state until notify()/notifyAll() or a timeout - and it must be called from inside a synchronized block.
notify() wakes up a single arbitrarily chosen thread waiting on that object's monitor; notifyAll() wakes up all of them, and they then compete for the lock. Use notifyAll() when more than one thread could be waiting on different conditions, otherwise a signal may be missed.
A deadlock happens when two or more threads hold locks and each waits for a lock held by another, so none can proceed. Best practices to avoid it: acquire locks in a consistent global order, keep critical sections small, use timeouts (tryLock with a timeout) and avoid nested locks where possible.
Stack is Last-In-First-Out (push/pop). Queue is First-In-First-Out (offer/poll). PriorityQueue removes elements by priority order rather than insertion order - the head is the smallest element by natural or comparator ordering.
ArrayDeque is a good general deque; LinkedList also implements both List and Queue.
ArrayList is backed by a resizable array: O(1) random access (get), but insertion/removal in the middle is O(n) because elements shift. LinkedList is a doubly-linked list: O(n) random access (must traverse), but O(1) insertion/removal at the ends.
In practice ArrayList is almost always faster and uses less memory; LinkedList only wins for frequent add/remove at both ends.
HashSet uses a hash table: O(1) add/contains, unsorted, requires equals()/hashCode(). TreeSet is a red-black tree: O(log n) operations, always sorted either by natural ordering or a Comparator, and requires Comparable/Comparator.
Use HashSet for speed, TreeSet when you need the elements in sorted order.
Iterator can traverse a collection forward and remove elements during iteration, and works with any Collection. ListIterator is only for List: it can also go backward (previous), gives the current index, and can add/set elements.
It is thrown when a collection is modified structurally (add/remove) while it is being iterated and the iterator detects the change via its modCount check. The fail-fast iterator then throws.
Avoid it by using Iterator.remove() during iteration, using concurrent collections (CopyOnWriteArrayList, ConcurrentHashMap) or collecting changes and applying them after the loop.
Hashtable synchronizes every method, serializing all access and hurting throughput. ConcurrentHashMap uses fine-grained locking - since Java 8, carefully locked buckets plus CAS - so reads are mostly lock-free and different buckets can be updated concurrently.
It also does not allow null keys/values and its iterators are weakly consistent rather than fail-fast.
throw is a statement that actually raises an exception object: throw new MyException();. throws is part of a method signature declaring which checked exceptions the method may throw: void read() throws IOException. You throw one exception at a time; you can declare several in throws.
The heap is typically divided into generations for efficient garbage collection: Young Generation (Eden plus two Survivor spaces, where new objects are allocated and minor GCs happen), Old (Tenured) Generation (objects that survived many minor GCs), and beyond the heap, Metaspace for class metadata (Java 8+).
Most objects die young, so GC focuses on the young generation; long-lived objects are promoted to the old generation and collected only by major GCs.
GC uses reachability analysis from GC roots (local variables on threads, static fields, JNI references). Any object not reachable from a root is considered garbage and eligible for collection. You can request collection with System.gc() but it is only a hint to the JVM.
Never rely on finalize() for cleanup - use try-with-resources or explicit close() instead.
Comparable defines natural ordering inside the class itself via compareTo(this, other); used like Collections.sort(list). Comparator is an external object defining ordering between two arbitrary objects, allowing multiple or alternative sort orders without touching the class.
Comparator is preferred for flexibility; it can be a lambda: list.sort((a,b) -> a.age - b.age).
A static method belongs to the class, is called on the class name, has no this reference and can only access static members. An instance method belongs to an object, is called on a specific instance, and can access instance state and override member methods polymorphically.
Primitives are passed by value - the method gets a copy, so changes inside do not affect the caller. Object references are also passed by value, but the value is a reference; reassigning the parameter does not affect the caller, but mutating the object the reference points to is visible to the caller.