Java Interview Prep

Java Interview Questions and Answers (2026)

Java interview questions reward understanding what's happening at the JVM level — how HashMap resolves collisions, how garbage collection actually works, what type erasure means for generics. Here are the ones that come up most often.

  1. What's the difference between JDK, JRE, and JVM?

    The JVM (Java Virtual Machine) is the runtime engine that executes bytecode — it's what makes Java "write once, run anywhere," and each OS/platform has its own JVM implementation. The JRE (Java Runtime Environment) bundles the JVM plus the core class libraries (java.lang, java.util, etc.) needed to run compiled Java programs, but it has no compiler. The JDK (Java Development Kit) is the full toolkit for developers — it includes the JRE plus the compiler (javac), debugger, and other tools like javadoc and jar. So the relationship is JDK ⊃ JRE ⊃ JVM. If you're only running a Java app you need the JRE; if you're building one, you need the JDK.

  2. What's the difference between checked and unchecked exceptions?

    Checked exceptions (subclasses of Exception but not RuntimeException, e.g. IOException, SQLException) are checked at compile time — the compiler forces you to either catch them or declare them with throws. They represent recoverable conditions external to the program, like a missing file. Unchecked exceptions (subclasses of RuntimeException, e.g. NullPointerException, IllegalArgumentException) aren't checked at compile time and usually signal programming bugs rather than recoverable conditions. Error subclasses (like OutOfMemoryError) are also unchecked and represent serious JVM-level problems you generally shouldn't try to catch. In practice, many teams avoid checked exceptions for business logic because they clutter method signatures and don't compose well with lambdas/streams.

  3. What's the difference between == and .equals()?

    == compares references for objects (do two variables point to the same object in memory?) and compares values for primitives. .equals() is a method meant to compare logical/content equality, and its default implementation in Object just falls back to ==. Classes like String, Integer, and collections override .equals() to compare contents.

    String a = new String("hi");
    String b = new String("hi");
    a == b;        // false — different objects
    a.equals(b);   // true — same content

    A classic pitfall: comparing Integer objects outside the cached range (-128 to 127) with == gives false even when values match, because autoboxing creates distinct objects outside the pool.

  4. Why are Strings immutable in Java, and what is the String pool?

    Strings are immutable — once created, their internal character array can't change; operations like concat() or replace() return new String objects. This immutability enables the String pool (or "string intern pool"), a special memory region where literal strings are cached and reused. When you write String s = "hello", the JVM checks the pool first and reuses an existing object if present, saving memory. Immutability also makes Strings safe for multithreading without synchronization, safe to use as HashMap keys (hashcode can be cached since it never changes), and prevents security issues like a malicious caller mutating a String after it's been validated (e.g., a file path or DB connection string).

    String s1 = "hello";
    String s2 = "hello";
    s1 == s2; // true — same pooled object
  5. How does HashMap handle collisions internally?

    HashMap stores entries in an array of buckets, where a key's hashCode() is run through an internal hash-spreading function to pick a bucket index. Two keys with different hash codes can still land in the same bucket (a collision) if the index computation collides, or two keys can even share a hash code. Historically each bucket held a linked list of entries, and lookup within a bucket meant walking the list and comparing with .equals(). Since Java 8, if a bucket's list grows beyond a threshold (8 entries) and the table is large enough, it's converted to a red-black tree, turning worst-case lookup from O(n) to O(log n). This matters interview-wise: a poor hashCode() implementation (e.g., always returning 0) degrades a HashMap to a linked list performance-wise.

  6. When would you use an abstract class vs an interface?

    Use an abstract class when you want to share common state (instance fields) and partial implementation among closely related classes, and you're modeling an "is-a" relationship with a single inheritance chain — a class can extend only one abstract class. Use an interface to define a contract/capability that unrelated classes can implement, since a class can implement multiple interfaces. Since Java 8, interfaces can have default and static methods with bodies, which narrowed the gap — you can now evolve an interface without breaking implementers.

    interface Payable {
        double amount();
        default String formatted() { return "$" + amount(); }
    }

    Rule of thumb: abstract classes for shared implementation + state, interfaces for capability contracts, and prefer interfaces for public API design since they're more flexible.

  7. How does garbage collection work in Java at a high level?

    The JVM's heap is divided generationally — Young Generation (further split into Eden and two Survivor spaces) and Old Generation. Most objects die young, so minor GCs run frequently on Eden, copying surviving objects into Survivor spaces and eventually promoting long-lived objects to Old Gen after enough survived collections. Old Gen is collected less often but more expensively (major/full GC). Collectors like G1 (the modern default) divide the heap into regions and prioritize collecting regions with the most garbage first to minimize pause times, rather than strictly following the generational young/old split. The GC identifies unreachable objects via reachability analysis from GC roots (stack references, static fields, etc.) — not reference counting — which is why circular references don't cause leaks in Java the way they can in naive refcounted systems.

  8. What is type erasure and why does it matter for generics?

    Java generics are implemented via type erasure: generic type information exists only at compile time for type-checking, and the compiler strips it out, replacing type parameters with their bounds (or Object if unbounded) in the compiled bytecode. This means List<String> and List<Integer> are the same class at runtime — you can't do if (list instanceof List<String>) or create a generic array new T[10] directly. It also means overloaded methods that only differ by generic type parameter won't compile (they'd erase to the same signature). Erasure was chosen for backward compatibility with pre-generics Java bytecode and libraries. The practical impact: reflection can't recover generic type arguments at runtime without extra tricks like passing a Class<T> token or using TypeToken patterns.

  9. What's the difference between implementing Runnable and extending Thread?

    Extending Thread means your class overrides run() and directly is a thread; implementing Runnable means you define the task separately and pass it to a Thread (or an ExecutorService). Runnable is generally preferred because Java doesn't support multiple inheritance — extending Thread uses up your one superclass slot — and because it decouples the task from the execution mechanism, which fits better with thread pools.

    Runnable task = () -> System.out.println("running");
    new Thread(task).start();
    ExecutorService pool = Executors.newFixedThreadPool(4);
    pool.submit(task);

    synchronized on a method or block ensures mutual exclusion using an object's intrinsic lock (monitor), preventing race conditions when multiple threads mutate shared state, at the cost of blocking contending threads.

  10. How is the Java Collections Framework organized?

    At the top are two root interfaces: Collection (for groups of elements: List, Set, Queue) and Map (key-value pairs, which is not a Collection). List (ArrayList, LinkedList) preserves insertion order and allows duplicates/indexed access. Set (HashSet, LinkedHashSet, TreeSet) disallows duplicates — HashSet is backed by a HashMap internally, TreeSet keeps sorted order via a red-black tree. Queue/Deque (ArrayDeque, PriorityQueue, LinkedList) support FIFO/LIFO/priority-based access. Map implementations mirror the Set hierarchy: HashMap, LinkedHashMap, TreeMap. Most implementations aren't thread-safe by default — you'd use Collections.synchronizedList(), or better, java.util.concurrent classes like ConcurrentHashMap and CopyOnWriteArrayList for concurrent access without external locking.

  11. What are common pitfalls with autoboxing?

    Autoboxing automatically converts between primitives and their wrapper classes (intInteger), which is convenient but has traps. First, == on boxed types compares references, not values, and only small cached Integers (-128 to 127) behave predictably with ==. Second, unboxing a null wrapper throws NullPointerException:

    Integer count = null;
    int x = count; // NPE at unboxing

    Third, autoboxing in loops (e.g., summing into an Integer accumulator) creates excessive object churn and hurts performance versus using primitives directly. Fourth, mixing primitives and wrappers in ternaries or overload resolution can trigger surprising unboxing/NPEs or pick an unexpected overload. The rule of thumb: use primitives for local computation and loops, and only box when a Collection or generic API requires it.

  12. What are functional interfaces and how do lambdas relate to them?

    A functional interface is an interface with exactly one abstract method (it can have default/static methods too), and it's the target type a lambda expression compiles against — the @FunctionalInterface annotation just makes the compiler enforce this. Java 8 shipped a standard set in java.util.function: Function<T,R>, Predicate<T>, Consumer<T>, Supplier<T>, etc. A lambda is essentially syntactic sugar that gets compiled (via invokedynamic and the LambdaMetafactory, not anonymous classes) into an implementation of that single method.

    Predicate<String> isEmpty = s -> s.isEmpty();
    Function<Integer, Integer> square = x -> x * x;
    list.stream().filter(isEmpty).forEach(System.out::println);

    This underpins the Streams API, where lambdas passed to map, filter, reduce are all just functional interface implementations.

Practicing for a real interview? Get live AI help with your answers, in the browser, free to start.

Start a free session