Java remains one of the most commonly asked technical interview domains, spanning core language fundamentals, JVM internals, and collections. Below are the questions candidates see most often, with concise answers to anchor your prep.
The JVM (Java Virtual Machine) executes bytecode and provides platform independence — it's technically a specification, with implementations like HotSpot doing the actual translation of bytecode into native instructions for the underlying OS/CPU. The JRE (Java Runtime Environment) is the JVM plus the core class libraries needed to run compiled Java applications. The JDK (Java Development Kit) is the full package: everything the JRE provides plus development tools like the compiler (`javac`), debugger, and `javadoc` — it's what you install to write and build Java code, not just run it. One nuance worth knowing: since Java 11, Oracle stopped shipping a separate standalone JRE installer, so in practice you just install the JDK and it covers both running and building.
Java source code compiles to bytecode (.class files) rather than machine code. That bytecode runs on any device with a compatible JVM, which handles the translation to native instructions for the underlying OS/CPU. The slogan "write once, run anywhere" refers to this separation between compiled bytecode and the platform-specific JVM implementation.
== compares references for objects (whether two variables point to the same memory location) and values for primitives. .equals() is a method that compares logical/content equality, and its behavior depends on whether the class overrides it — String and the wrapper classes override it to compare values, while the default Object.equals() falls back to reference comparison.
An abstract class can have both abstract and concrete methods, constructors, and instance state, but a class can only extend one abstract class. An interface (pre-Java 8) could only declare method signatures and public static final constants; since Java 8 it can include default and static methods, and since Java 9 even private methods for internal code sharing, but it still can't hold instance state, and a class can implement multiple interfaces. Use abstract classes when you want to share implementation and state among closely related classes, and interfaces to define a contract across otherwise unrelated classes.
The stack stores method call frames, local variables, and object references, and is thread-local with automatic LIFO cleanup on method return. The heap stores actual objects and is shared across all threads, managed by the garbage collector. Stack overflows typically come from deep/infinite recursion; heap issues (OutOfMemoryError) come from retaining too many live object references.
The JVM's garbage collector automatically reclaims heap memory occupied by objects that are no longer reachable from any live reference (GC roots). Modern collectors like G1 use a generational approach — objects start in a young generation (Eden/Survivor spaces) and get promoted to the old generation if they survive multiple collections — because most objects die young, so collecting the young generation frequently is cheap and effective.
Encapsulation (bundling data and methods, restricting direct access via access modifiers), Inheritance (a class acquiring fields/methods from a parent class via extends), Polymorphism (a single interface/method behaving differently depending on the runtime object — via overriding or overloading), and Abstraction (exposing only essential behavior through abstract classes or interfaces while hiding implementation detail).
ArrayList is backed by a dynamic array, giving O(1) random access (get/set by index) but O(n) insertion/deletion in the middle since elements must shift. LinkedList is a doubly-linked list, giving O(1) insertion/deletion 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 unless you're doing frequent insertions/deletions at arbitrary positions.
Checked exceptions (like IOException) extend Exception and must be either caught or declared with throws — the compiler enforces handling because they represent recoverable conditions external to the program, like file I/O. Unchecked exceptions extend RuntimeException (like NullPointerException or IllegalArgumentException) and aren't enforced at compile time, since they usually indicate programming bugs rather than conditions you'd design around.
Strings in Java are immutable — once created, a String object's contents can never change; operations like concatenation return a new String rather than modifying the original. Because of this immutability, the JVM maintains a String pool (part of the heap since Java 7) where string literals are interned and reused: writing `String a = "hi"` and `String b = "hi"` gives you two references to the same pooled object. This is only safe because Strings can't change underneath you, and it saves memory since duplicate literals aren't recreated. Using `new String("hi")` bypasses the pool and creates a distinct object on the heap, which is why `==` comparisons on Strings are unreliable and you should use `.equals()`. Immutability also makes Strings inherently thread-safe and safe to use as HashMap keys, since their hash code can be computed once and cached without ever going stale.
Java's contract requires that if two objects are equal according to `.equals()`, they must return the same value from `hashCode()` — the reverse isn't required, so unequal objects can share a hash code (a collision), just not the other way around. Hash-based collections like HashMap and HashSet rely on this: they use `hashCode()` to pick a bucket and then `.equals()` to confirm an actual match within that bucket. If you override `.equals()` without overriding `hashCode()`, you break this contract — two 'equal' objects could land in different buckets, and a HashSet would treat them as distinct even though your `.equals()` says they're the same. As a rule, always override both together, base them on the same set of fields, and keep `hashCode()` consistent for a given object as long as those fields don't change.
HashMap stores entries in an array of buckets, where the bucket index is derived from the key's `hashCode()` (after an internal hash-spreading step) modulo the array size. Each bucket holds a linked list of entries that hash to the same index; since Java 8, if a bucket's list grows past a threshold (8 entries) and the table is large enough, that bucket converts to a red-black tree for O(log n) lookups instead of O(n). When the number of entries exceeds the load factor (default 0.75) times the current capacity, the table resizes — typically doubling — and all entries are rehashed into the new array. Lookups, insertions, and deletions are O(1) on average with a well-distributed hash function, but degrade toward O(n) or O(log n) under heavy collisions, which is why a good `hashCode()` implementation matters.
The `static` keyword means a member belongs to the class itself rather than to any individual instance. A static field is shared across all instances and exists as long as the class is loaded, a static method can be called without creating an object (and can't access instance fields or use `this`), and a static nested class doesn't hold an implicit reference to an enclosing instance. Static initializer blocks run once, when the class is first loaded by the JVM, which makes them useful for one-time setup of static state. A common use case is utility/helper classes made up entirely of static methods, like `Collections` or `Math`, since there's no meaningful per-instance state to track.
`final` means different things depending on where it's applied. On a variable, it means the reference (or primitive value) can only be assigned once — for objects, that only locks the reference itself, not the object's internal state, which is why a `final List` can still have elements added to it. On a method, it prevents subclasses from overriding it, useful for locking down behavior that should never change. On a class, it prevents any subclassing at all — `String` and the wrapper classes are final for exactly this reason, partly to preserve their immutability guarantees.
Overloading happens at compile time: a class defines multiple methods with the same name but different parameter lists (different number, type, or order of parameters), and the compiler picks the right one based on the arguments at the call site. Overriding happens at runtime: a subclass provides its own implementation of a method it inherited, with the same signature, and the JVM decides which version to invoke based on the actual runtime type of the object (dynamic dispatch) rather than the reference type. Overloading is resolved statically and can vary the return type freely, while overriding requires the same signature and a covariant (or identical) return type, and the access modifier can't be more restrictive than the overridden method. In short, overloading is about having multiple entry points into the same class, while overriding is about customizing inherited behavior in a subclass.
`synchronized` is Java's built-in mechanism for mutual exclusion: it ensures only one thread at a time can execute a block of code or method guarded by a given lock (monitor), which prevents race conditions on shared mutable state. You can synchronize an instance method (locking on `this`), a static method (locking on the class object), or an arbitrary block (locking on any object you choose), with narrower blocks generally preferred to reduce contention. Beyond mutual exclusion, synchronized also establishes a happens-before relationship, guaranteeing that changes made inside a synchronized block by one thread are visible to another thread that later acquires the same lock. The tradeoff is throughput: synchronized blocks serialize access, so overly broad locking can turn a multithreaded program into something that behaves like it's single-threaded at that bottleneck.
`volatile` guarantees visibility, not atomicity: writes to a volatile variable by one thread are immediately visible to other threads, because reads and writes go directly to main memory rather than being cached in a thread-local CPU register or cache line. It also prevents certain compiler and CPU reorderings around that variable, which matters for correctness in some lock-free patterns. What it does not do is make compound operations atomic — something like `count++` on a volatile int is still a read-modify-write with a race condition, because it's three separate operations under the hood. Use volatile for simple flags or status variables read and written independently by multiple threads, and use `synchronized` or `java.util.concurrent.atomic` classes like `AtomicInteger` when you need atomic compound operations.
A functional interface is an interface with exactly one abstract method (it can still have default and static methods), and it's the target type that lambda expressions and method references get assigned to — `Runnable`, `Comparator`, and `Function<T, R>` are all examples. A lambda expression, like `(a, b) -> a + b`, is a concise way to provide an implementation of that single abstract method without writing a full anonymous class, and the `@FunctionalInterface` annotation is an optional compile-time check that the interface still qualifies. The `java.util.function` package ships a standard set of these — `Function`, `Predicate`, `Supplier`, `Consumer`, and their variants — so you rarely need to declare your own. This is the foundation the Stream API is built on: methods like `.filter()`, `.map()`, and `.reduce()` all take functional interfaces as arguments, letting you express data transformations declaratively instead of with explicit loops.
`Optional<T>` is a container object introduced in Java 8 that either holds a non-null value or is empty, and it exists to make the possibility of 'no value' explicit in a method's return type instead of relying on returning null and hoping callers remember to check. Instead of an implicit contract that a method might return null, `Optional` forces callers to explicitly unwrap it via methods like `.isPresent()`, `.orElse()`, `.orElseThrow()`, or `.map()`, making the absence-handling visible in the code rather than a runtime surprise. It composes well with streams — `.map()` and `.filter()` on an Optional let you chain transformations without manual null checks at every step. The main caveat is that Optional is intended for return types, not for fields, method parameters, or collections — using it there tends to add ceremony without the benefit it's designed for.
try-with-resources is a syntax introduced in Java 7 for automatically closing resources — like file streams, sockets, or database connections — that implement the `AutoCloseable` interface. You declare the resource inside the parentheses of the try statement, and the JVM guarantees `.close()` is called at the end of the block, even if an exception is thrown, without needing an explicit `finally` block. This replaces the old pattern of manually closing resources in `finally`, which was verbose and easy to get wrong — especially when closing itself could throw, or when multiple resources needed closing in reverse order. You can declare multiple resources separated by semicolons, and they're closed automatically in the reverse of their declaration order.