Ch. 07

Java interview questions & answers

Core Java for backend interviews: OOP, equals/hashCode, collections internals, the JVM and GC, concurrency, streams and modern Java features.

58 interview questions20 quiz questions27 notes
your progress0%

Notes in this chapter

Filter all notes →
read ✓Java · hard

Java Generics Wildcards and PECS

Use ? extends T for producers and ? super T for consumers, and understand why List<String> is not a List<Object>.

~2 min readread →
read ✓Java · easy

Java ArrayList Capacity and Performance

How ArrayList grows its backing array by 1.5x, why add is amortized O(1), and when ensureCapacity or a different list actually helps.

~8 min readread →
read ✓Java · mid

Java Comparators and Ordered Collections

Why TreeSet and TreeMap drop elements when a Comparator disagrees with equals, and how to write comparators that obey the full contract.

~6 min readread →
read ✓Java · hard

Java CompletableFuture Error Composition

How exceptions travel through CompletableFuture chains, when to use exceptionally, handle or whenComplete, and why causes arrive wrapped.

~7 min readread →
read ✓Java · mid

Java Exceptions and Recovery Boundaries

Checked vs unchecked exceptions in Java, where to catch, when to wrap with a cause, and how to avoid swallowing failures at the wrong layer.

~6 min readread →
read ✓Java · hard

Java Executors and Task Lifecycle

How shutdown, shutdownNow and awaitTermination differ in ExecutorService, and the safe pattern for stopping thread pools without losing work.

~6 min readread →
read ✓Java · hard

Java Garbage Collection and Retained Objects

How Java memory leaks happen despite garbage collection: GC roots, reachability, retained size, and finding the reference chain in a heap dump.

~7 min readread →
read ✓Java · easy

Java HashMap Collisions and Key Quality

How HashMap works internally: hash spreading, bucket index, collisions, load factor resizing and the Java 8 switch from chains to trees.

~7 min readread →
read ✓Java · easy

Java Immutable Objects and Defensive Copies

How to make a class immutable in Java: final fields, no setters, defensive copies of mutable inputs and outputs, and safe publication.

~6 min readread →
read ✓Java · hard

Java Time Types and Zone-Aware Decisions

When to use Instant, LocalDateTime, ZonedDateTime and OffsetDateTime, and how daylight saving gaps and overlaps break naive date code.

~6 min readread →
read ✓Java · mid

Java Optional as an Absence Contract

When to return Optional in Java, why get and isPresent checks miss the point, and why Optional fields, parameters and collections are a smell.

~6 min readread →
read ✓Java · easy

Java Records and Shallow Immutability

What Java records generate, why they are only shallowly immutable, how compact constructors copy and validate, and where records do not fit.

~6 min readread →
read ✓Java · mid

Java Streams and Lazy Pipelines

Why Java stream intermediate operations do nothing until a terminal operation runs, how elements flow one by one, and where laziness bites.

~6 min readread →
read ✓Java · mid

Java Stream Reuse and Collection Ownership

Why reusing a Java stream throws IllegalStateException, how to share data with a Supplier or a collection, and what toList returns.

~6 min readread →
read ✓Java · easy

Java String Equality and Identity

Java String equals vs ==: why == compares references, how the string pool and compile-time constants make it seem to work, and safe comparisons.

~6 min readread →
read ✓Java · mid

Java Thread Visibility and Happens-Before

Why one Java thread may never see another's write, what volatile guarantees, and how happens-before rules make publication safe.

~7 min readread →
read ✓Java · mid

Java Try-With-Resources and Cleanup

How try-with-resources closes resources in reverse order, keeps the primary exception with suppressed close failures, and beats finally.

~6 min readread →
read ✓Java · hard

Java Virtual Threads and Blocking Workloads

When Java 21 virtual threads help, why they do not speed up CPU-bound work, and how pinning, pooling and resource limits trip teams up.

~7 min readread →

58 Java interview questions study by subtopic

58 questions
  1. 1.What are the four pillars of object-oriented programming, and how does Java support each one?easy

    The four pillars are encapsulation, inheritance, polymorphism and abstraction.

    • Encapsulation: bundle state with the behaviour that works on it, and hide the state behind an API. In Java that means private fields exposed through methods that can enforce invariants.
    • Inheritance: a class reuses and extends another with extends. Java allows single inheritance of classes, but a class can implement many interfaces.
    • Polymorphism: one reference type, many behaviours. Compile-time polymorphism is method overloading; runtime polymorphism is overriding, where the JVM picks the method from the object's actual class (dynamic dispatch).
    • Abstraction: expose what something does, not how. Interfaces and abstract classes let callers depend on a contract such as List rather than on ArrayList.

    I'd also mention that composition is often preferred over inheritance, because deep class hierarchies couple classes tightly.

    What interviewers listen for
    • Encapsulation: private state behind methods
    • Inheritance: extends, single class inheritance only
    • Polymorphism: overloading (compile time) vs overriding (runtime)
    • Abstraction via interfaces and abstract classes
    • Favour composition over inheritance

    Likely follow-up: Why does Java not support multiple inheritance of classes? · Is encapsulation the same as abstraction?

  2. 2.What is the difference between the JDK, the JRE and the JVM?easy

    They nest inside each other.

    • The JVM (Java Virtual Machine) runs bytecode. It loads and verifies classes, manages memory with a garbage collector, and executes code with an interpreter plus a JIT compiler. Each platform has its own JVM, which is exactly what makes .class files portable: "write once, run anywhere".
    • The JRE (Java Runtime Environment) is the JVM plus the standard class libraries (java.lang, java.util and so on): everything needed to run a program.
    • The JDK (Java Development Kit) is the JRE plus development tools: javac, jar, javadoc, jshell, jlink, and diagnostic tools like jcmd.

    One modern nuance: since JDK 11, Oracle no longer ships a separate JRE. Instead, jlink (Java 9+) builds a trimmed runtime image containing only the modules your application needs.

    What interviewers listen for
    • JVM executes bytecode and is platform-specific
    • JRE = JVM + standard class libraries
    • JDK = JRE + tools like javac and jar
    • Bytecode is portable: write once, run anywhere
    • jlink builds custom runtime images

    Likely follow-up: Is the JVM itself platform-independent? · What does the JIT compiler do?

  3. 3.What is the difference between == and equals()? What does this code print?easy

    == compares values for primitives but references for objects: it's true only if both sides point to the very same object. equals() compares logical equality, and what that means is up to the class.

    Object.equals is just ==, so a class only gets value semantics if it overrides it. String, the wrapper types, collections and records all do.

    So the snippet prints false, true, false, true. s2 is a new object, so == is false while equals is true. The Integer case is the classic trap: autoboxing uses a cache that's only guaranteed for -128 to 127, so with default JVM settings 1000 produces two different boxes.

    Rules of thumb: use == for primitives, enums and deliberate identity checks; use equals for everything else, or Objects.equals(a, b) when either side may be null.

    String s1 = "java";
    String s2 = new String("java");
    System.out.println(s1 == s2);        // ?
    System.out.println(s1.equals(s2));   // ?
    
    Integer x = 1000, y = 1000;
    System.out.println(x == y);          // ?
    System.out.println(x.equals(y));     // ?
    What interviewers listen for
    • == compares references for objects, values for primitives
    • equals compares logical content when overridden
    • Default Object.equals is identity
    • Integer cache only guaranteed for -128 to 127
    • Use Objects.equals for null-safe comparison

    Likely follow-up: Why is == safe for enums? · What happens if you override equals but not hashCode?

  4. 4.Why is String immutable in Java, and what is the string pool?easy

    A String can't change once created: methods like toUpperCase() or concat() return new strings, and the class is final, so no subclass can add mutability. The main reasons:

    • String pool: literals are interned, so every "hello" in a program can share one instance. Sharing is only safe because nobody can modify it.
    • Security: strings hold class names, file paths, URLs and credentials. If they were mutable, a value could change after it had been validated.
    • Thread safety: immutable objects can be shared across threads without synchronization.
    • Hashing: the hash code is cached after first use, which makes strings fast, reliable HashMap keys.

    The pool is a JVM-managed table of canonical strings. String literals and compile-time constants go there automatically, and intern() looks up or adds others. Since Java 7 it lives on the main heap rather than in PermGen, so unused pooled strings can be garbage-collected.

    What interviewers listen for
    • Methods return new strings; the class is final
    • Immutability makes pooling and sharing safe
    • Security: values cannot change after validation
    • Thread-safe, and the hash code is cached
    • The pool lives on the heap since Java 7

    Likely follow-up: Why is a char[] sometimes preferred over a String for passwords? · What does intern() do?

  5. 5.How does HashMap work internally? Walk me through put and get.mid

    A HashMap is an array of buckets. Each bucket holds a chain of nodes with the key, value, hash and a next pointer.

    • Hashing: put calls key.hashCode() and spreads it by XOR-ing the high 16 bits into the low 16. The index is (n - 1) & hash, which works because the capacity n is always a power of two.
    • Collisions: an empty bucket takes the node directly. Otherwise the chain is walked, comparing hash and then equals(): a match replaces the value, else a node is appended.
    • Treeification (Java 8+): when a chain grows past 8 nodes and the table has at least 64 buckets, it becomes a red-black tree, so a bad hash degrades lookups to O(log n) rather than O(n). A smaller table is resized instead.
    • Resizing: default capacity is 16 with load factor 0.75, so once size exceeds 12 the table doubles. Each entry either stays at index i or moves to i + oldCapacity.

    get repeats the hash and index steps, then matches with equals(). One null key is allowed.

    What interviewers listen for
    • Array of buckets; index is (n - 1) & hash
    • Collisions chained, matched by hash then equals
    • Chains past 8 nodes treeify (table size at least 64)
    • Load factor 0.75; the table doubles on resize
    • One null key allowed; not thread-safe

    Likely follow-up: Why must the capacity be a power of two? · What happens if a key is mutated after insertion?

  6. 6.What is the contract between equals() and hashCode(), and what breaks if you override only one of them?mid

    equals must be reflexive, symmetric, transitive and consistent, and x.equals(null) must return false. The link to hashCode:

    • If two objects are equal, they must return the same hash code.
    • Unequal objects may share a hash code (a collision), but fewer collisions mean faster hash tables.
    • The hash code must stay stable while the fields used by equals don't change.

    Hash-based collections depend on this: they use the hash to find the bucket and only then call equals. In the snippet, Point overrides equals but inherits the identity-based hashCode, so two equal points almost certainly have different hash codes and contains returns false.

    The fix is to override both from the same fields, for example Objects.hash(x, y), or to use a record, which generates both. And keep keys immutable: mutating a field after insertion leaves the entry filed under its old hash, where lookups can't find it.

    class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
    
        @Override public boolean equals(Object o) {
            return o instanceof Point p && x == p.x && y == p.y;
        }
        // hashCode() not overridden!
    }
    
    Set<Point> set = new HashSet<>();
    set.add(new Point(1, 2));
    System.out.println(set.contains(new Point(1, 2)));  // almost always false
    What interviewers listen for
    • Equal objects must have equal hash codes
    • Equal hash codes do not imply equality
    • equals: reflexive, symmetric, transitive, consistent, null-safe
    • Override both from the same fields
    • Mutable keys break hash-based collections

    Likely follow-up: Why is using getClass() versus instanceof in equals debated? · What does a record generate for you?

  7. 7.What is the difference between an abstract class and an interface, now that interfaces can have default, static and private methods?easy

    Since Java 8 the line is blurrier, but the core differences remain:

    • State: an abstract class can have instance fields and constructors. An interface can only have constants (implicitly public static final), never instance state.
    • Inheritance: a class extends one abstract class but can implement many interfaces.
    • Access: abstract class members can use any access level. Interface methods are public, except private helper methods, allowed since Java 9.
    • Evolution: Java 8 added default methods, so an interface can grow without breaking implementers, and static methods for utilities, which are called on the interface and not inherited by implementing classes.

    I use an interface for a capability or contract that unrelated classes share, like Comparable or Runnable. I use an abstract class when related classes share state and a partly implemented template, such as a base class that manages a connection and lets subclasses fill in one step.

    What interviewers listen for
    • Abstract classes can hold state and constructors
    • One superclass, many interfaces
    • default and static interface methods since Java 8
    • private interface methods since Java 9
    • Interface = capability; abstract class = shared base

    Likely follow-up: What happens if a class inherits the same default method from two interfaces? · Can an interface have a constructor?

  8. 8.What is the difference between method overloading and method overriding? What does this print?easy

    Overloading means several methods with the same name but different parameter lists. The compiler picks one at compile time from the static types of the arguments. The return type alone can't distinguish overloads.

    Overriding means a subclass redefines an inherited instance method with the same signature. The JVM picks the implementation at runtime from the object's actual class: dynamic dispatch.

    So the snippet prints Woof, then animal. a.speak() dispatches on the runtime type, Dog, but greet(a) was resolved at compile time, when the type of a is Animal.

    Overriding rules: the return type may be covariant (a subtype), access can't be narrowed, the method can't throw broader checked exceptions, and static, private and final methods can't be overridden. A static method with the same signature hides the parent's instead. Always add @Override so the compiler catches signature mistakes.

    class Animal { void speak() { System.out.println("..."); } }
    class Dog extends Animal {
        @Override void speak() { System.out.println("Woof"); }
    }
    
    static void greet(Animal a) { System.out.println("animal"); }
    static void greet(Dog d)    { System.out.println("dog"); }
    
    Animal a = new Dog();
    a.speak();   // ?
    greet(a);    // ?
    What interviewers listen for
    • Overloading: same name, different parameters, compile time
    • Overriding: same signature in a subclass, runtime dispatch
    • Overload choice uses the static argument types
    • Covariant returns; no narrower access or broader checked exceptions
    • Static methods are hidden, not overridden

    Likely follow-up: Can you overload main? · Why are fields not polymorphic?

  9. 9.When would you use ArrayList versus LinkedList?easy

    ArrayList is backed by a resizable array. get(i) is O(1), appending is amortized O(1), and when the array is full it grows by about 50% and copies the elements over. Inserting or removing in the middle is O(n) because later elements shift, but that shift is a fast bulk copy over contiguous memory.

    LinkedList is a doubly linked list that also implements Deque. Adding or removing at either end, or at an iterator's position, is O(1), but get(i) is O(n) because it walks from the nearer end. Every element costs a separate node object with two extra pointers, and nodes are scattered in memory, which hurts CPU cache performance.

    In practice ArrayList wins almost every real workload, even ones that look insert-heavy, because finding the position in a linked list is itself O(n). For a queue or stack I'd choose ArrayDeque rather than LinkedList.

    What interviewers listen for
    • ArrayList: O(1) random access, amortized O(1) append
    • ArrayList grows by about 50% when full
    • LinkedList: O(1) at the ends, O(n) indexed access
    • LinkedList has per-node overhead and poor locality
    • Prefer ArrayList; use ArrayDeque for queues and stacks

    Likely follow-up: When would LinkedList actually be the better choice? · What is the default capacity of an ArrayList?

  10. 10.What is the difference between checked and unchecked exceptions?easy

    Checked exceptions are subclasses of Exception that aren't RuntimeExceptions, such as IOException or SQLException. The compiler forces you to handle them: catch them or declare them with throws. They model recoverable conditions outside the program's control.

    Unchecked exceptions are RuntimeException and its subclasses, like NullPointerException, IllegalArgumentException or IndexOutOfBoundsException. They usually signal programming bugs, and the compiler doesn't require handling. Errors such as OutOfMemoryError and StackOverflowError are unchecked too; they signal serious problems that applications generally shouldn't catch.

    The hierarchy has Throwable at the top, with Exception and Error under it. One rule worth knowing: an overriding method can't declare broader checked exceptions than the method it overrides. Many modern APIs and frameworks, Spring included, favour unchecked exceptions, because checked ones add boilerplate and don't compose well with lambdas.

    What interviewers listen for
    • Checked: compiler enforces catch or throws
    • Unchecked: RuntimeException and its subclasses
    • Error is unchecked and usually not caught
    • Hierarchy: Throwable → Exception / Error
    • Overrides cannot throw broader checked exceptions

    Likely follow-up: When would you create a custom checked exception? · How do you handle a checked exception inside a lambda?

  11. 11.What is the difference between final, finally and finalize()?easy

    They're unrelated apart from the spelling.

    • final is a modifier. A final variable can be assigned only once; for a reference, the reference is fixed but the object it points to can still change. A final method can't be overridden, and a final class, like String, can't be subclassed.
    • finally is a block after try/catch that runs whether the try block completes normally, returns or throws. It's meant for cleanup, though try-with-resources is usually better. It's skipped only in extreme cases, such as System.exit(), a JVM crash, or a try block that never finishes.
    • finalize() is a method on Object that the garbage collector might call before reclaiming an object. There's no guarantee when, or even whether, it runs; it slows GC and can resurrect objects. It was deprecated in Java 9 and deprecated for removal in Java 18. Use try-with-resources or java.lang.ref.Cleaner instead.
    What interviewers listen for
    • final: no reassignment, overriding or subclassing
    • A final reference can still point to a mutable object
    • finally runs after try/catch, for cleanup
    • finalize is unreliable and deprecated for removal
    • Prefer try-with-resources or Cleaner

    Likely follow-up: What happens if both the try block and the finally block contain a return?

  12. 12.What is the difference between String, StringBuilder and StringBuffer?easy

    String is immutable, so every "modification" creates a new object. Building a string with += in a loop creates many intermediate strings and copies the characters again each time, which is O(n²) overall.

    StringBuilder and StringBuffer are both mutable character buffers with the same API: append, insert, reverse, deleteCharAt and so on. The difference is synchronization:

    • StringBuffer (Java 1.0) has synchronized methods, so it's thread-safe but pays for locking on every call.
    • StringBuilder (Java 5) is the unsynchronized equivalent and the right default.

    You almost never share a string buffer between threads, so StringBuffer is mostly legacy. For simple one-line concatenation, plain + is fine: the compiler optimizes it, and since Java 9 it compiles to an invokedynamic call backed by StringConcatFactory. It's loops where you should reach for StringBuilder explicitly.

    What interviewers listen for
    • String is immutable; += in a loop is wasteful
    • Both builders are mutable with the same API
    • StringBuffer is synchronized, StringBuilder is not
    • Default to StringBuilder
    • Simple + concatenation is optimized by the compiler

    Likely follow-up: Why does sb1.equals(sb2) return false for two builders with the same content?

  13. 13.What were the most important features introduced in Java 8?easy

    Java 8 (2014) was the biggest update since generics, and it brought a functional style to Java:

    • Lambda expressions and functional interfaces, with Function, Predicate, Supplier, Consumer and friends in java.util.function.
    • Method references such as String::length.
    • The Streams API for declarative, lazily evaluated, optionally parallel processing of collections.
    • Default and static methods in interfaces, which let the JDK add methods like forEach and stream() to existing interfaces without breaking every implementation.
    • Optional for return values that may be absent.
    • The java.time API: immutable, thread-safe dates and times replacing Date and Calendar.
    • CompletableFuture for composing asynchronous work.

    Under the hood, PermGen was replaced by Metaspace, and HashMap buckets with many collisions started converting into balanced trees.

    What interviewers listen for
    • Lambdas and functional interfaces
    • Streams API and method references
    • Default and static interface methods
    • Optional and the java.time API
    • Metaspace replaced PermGen

    Likely follow-up: Why were default methods needed to add streams? · What was wrong with java.util.Date?

  14. 14.Compare HashMap, Hashtable and ConcurrentHashMap. How does ConcurrentHashMap achieve thread safety?hard
    • HashMap isn't thread-safe: concurrent writes can lose updates or corrupt its internal structure. It allows one null key and null values.
    • Hashtable is a legacy Java 1.0 class whose methods are all synchronized on the table, so only one thread can use it at a time, even for reads. It rejects null keys and values. Collections.synchronizedMap has the same single-lock design.
    • ConcurrentHashMap is built for concurrency. Since Java 8 it no longer uses segments: reads don't lock, inserting into an empty bucket uses a CAS, and other updates lock only the first node of that bucket. Threads even help each other with resizing. Its iterators are weakly consistent and never throw ConcurrentModificationException.

    ConcurrentHashMap also rejects null: a get returning null would be ambiguous between "absent" and "mapped to null", and you can't lock the map to check. For compound updates, use its atomic methods like computeIfAbsent or merge instead of get-then-put.

    What interviewers listen for
    • HashMap: fast, not thread-safe, allows null
    • Hashtable: every method synchronized, legacy, no null
    • CHM: CAS plus per-bucket locking, reads without locks
    • CHM iterators are weakly consistent
    • Use compute or merge for atomic compound updates

    Likely follow-up: How did ConcurrentHashMap work in Java 7? · Is size() on a ConcurrentHashMap exact?

  15. 15.What does the static keyword mean, and where can you use it?easy

    static means the member belongs to the class rather than to any instance.

    • Static fields: one copy shared by all instances, such as a counter or a static final constant.
    • Static methods: called on the class, like Math.max. They have no this, so they can't use instance fields or methods directly. They can't be overridden, only hidden, because the call is bound at compile time.
    • Static blocks: run once, when the class is initialized, to set up complex static state.
    • Static nested classes: unlike inner classes, they hold no reference to an enclosing instance, which saves memory and avoids accidental leaks.
    • Static imports: import static java.lang.Math.max; lets you write max(a, b).

    Pitfalls: mutable static state is effectively a global variable, which makes code hard to test and needs synchronization when threads share it. And a static collection that only grows is a classic memory leak, since it lives as long as the class itself.

    What interviewers listen for
    • Belongs to the class, not an instance
    • Static methods have no this
    • Static methods are hidden, not overridden
    • Static blocks run once at class initialization
    • Static nested classes hold no outer reference

    Likely follow-up: Why is main static? · Can you call a static method through an instance reference?

  16. 16.Is Java pass-by-value or pass-by-reference? What does this print?easy

    Java is always pass-by-value. For primitives, the value is copied. For objects, the value that gets copied is the reference, so the method receives its own copy of a pointer to the same object.

    That explains both lines. In reassign, the parameter sb is pointed at a brand-new object, but that only changes the method's local copy; the caller's s still refers to the original, so it prints hello. In mutate, the copy still points at the same object, so append changes the object the caller sees, and it prints hello world.

    A handy test: if Java were pass-by-reference, you could write a swap(a, b) method that swaps two of the caller's variables. You can't. People loosely say "objects are passed by reference", but the precise phrasing is "object references are passed by value".

    static void reassign(StringBuilder sb) { sb = new StringBuilder("new"); }
    static void mutate(StringBuilder sb)   { sb.append(" world"); }
    
    StringBuilder s = new StringBuilder("hello");
    reassign(s);
    System.out.println(s);   // ?
    mutate(s);
    System.out.println(s);   // ?
    What interviewers listen for
    • Java is always pass-by-value
    • For objects, the reference is copied
    • Mutating the object is visible to the caller
    • Reassigning the parameter is not
    • A swap(a, b) method cannot work

    Likely follow-up: How would you return two values from a method?

  17. 17.Explain the Java Collections Framework hierarchy.easy

    At the root is Iterable, extended by Collection, which has three main sub-interfaces:

    • List: ordered, index-based, allows duplicates. ArrayList, LinkedList.
    • Set: no duplicates. HashSet (unordered), LinkedHashSet (insertion order), and TreeSet, which implements SortedSet and NavigableSet.
    • Queue: holds elements for processing, usually FIFO. PriorityQueue orders by priority, and the Deque sub-interface, implemented by ArrayDeque and LinkedList, works at both ends.

    Map is a separate hierarchy: it stores key-value pairs and doesn't extend Collection. Implementations include HashMap, LinkedHashMap, and TreeMap through SortedMap and NavigableMap.

    Java 21 added sequenced collections: SequencedCollection, SequencedSet and SequencedMap give ordered collections uniform methods such as getFirst(), getLast() and reversed(). Also, don't confuse Collection, the interface, with Collections, the utility class of static helpers like sort and unmodifiableList.

    What interviewers listen for
    • Iterable → Collection → List, Set, Queue
    • Map is separate; it does not extend Collection
    • Sorted variants: TreeSet and TreeMap
    • Java 21 added the sequenced collection interfaces
    • Collections is a utility class

    Likely follow-up: Why does Map not extend Collection? · Which collections allow null elements?

  18. 18.Explain the four access levels in Java.easy

    From most to least restrictive:

    • private: visible only inside the top-level class that declares it, including its nested classes.
    • package-private (no modifier): visible to any class in the same package.
    • protected: the same package, plus subclasses in other packages, which can reach it through inheritance.
    • public: visible everywhere, subject to module exports since Java 9.

    Top-level classes can only be public or package-private, and a source file can have at most one public top-level class, whose name must match the file name. Interface methods are implicitly public unless declared private.

    Two interview favourites: an overriding method can't reduce visibility, so you can't override a public method as protected; and protected is wider than package-private, not narrower.

    Good practice is to start with the most restrictive level that works: private fields, and package-private classes unless they're part of your public API.

    What interviewers listen for
    • private → package-private → protected → public
    • No modifier means package-private
    • protected adds subclasses in other packages
    • Overrides cannot reduce visibility
    • Start with the most restrictive level

    Likely follow-up: Can a subclass in another package access a protected member through a parent-type reference?

  19. 19.What is the difference between Comparable and Comparator?easy

    Comparable (in java.lang) defines a class's natural ordering through a single method, compareTo(T other), implemented by the class itself. String, Integer and LocalDate implement it, and it's what Collections.sort(list), TreeMap and TreeSet use by default.

    Comparator (in java.util) is a separate object with compare(a, b). It lets you define any number of orderings, for classes you don't control or when there's no single natural order. Since Java 8 it has factories and combinators: comparing, comparingInt, thenComparing, reversed, nullsFirst.

    Both return a negative number, zero, or a positive number. Two tips: use Integer.compare(a, b) rather than a - b, which can overflow; and keep compareTo consistent with equals, because sorted collections use the comparison, not equals, to decide whether two elements are duplicates.

    record Person(String name, int age) implements Comparable<Person> {
        public int compareTo(Person o) { return Integer.compare(age, o.age); }
    }
    
    List<Person> people = new ArrayList<>(List.of(new Person("Ann", 30), new Person("Bob", 25)));
    Collections.sort(people);                               // natural order: by age
    people.sort(Comparator.comparing(Person::name));        // by name
    people.sort(Comparator.comparingInt(Person::age)
            .reversed()
            .thenComparing(Person::name));                  // age desc, then name
    What interviewers listen for
    • Comparable: natural order via compareTo, inside the class
    • Comparator: external, many orderings, compare(a, b)
    • Chain with comparing, thenComparing, reversed
    • Avoid subtraction; use Integer.compare
    • Sorted collections treat a comparison of 0 as a duplicate

    Likely follow-up: What happens if compareTo is inconsistent with equals in a TreeSet?

  20. 20.What is the difference between fail-fast and fail-safe iterators?mid

    Fail-fast iterators, used by ArrayList, HashMap and most java.util collections, throw ConcurrentModificationException if the collection is structurally modified after the iterator was created, by anything other than the iterator itself. They track a modCount counter and check it on each next(). The check is best-effort: it exists to catch bugs, not to provide thread safety.

    The classic bug is removing inside a for-each loop, as in the snippet. The fixes are Iterator.remove() or removeIf.

    Fail-safe iterators (the JDK docs say snapshot or weakly consistent) never throw. CopyOnWriteArrayList iterates over a snapshot of the array taken when the iterator was created, so it doesn't see later changes. ConcurrentHashMap iterators are weakly consistent: they may or may not reflect updates made during iteration. The costs are copying on every write for copy-on-write lists, and possibly stale views.

    List<String> names = new ArrayList<>(List.of("a", "b", "c"));
    for (String n : names) {
        if (n.equals("a")) names.remove(n);    // throws ConcurrentModificationException
    }
    
    names.removeIf(n -> n.equals("a"));        // fix 1 (Java 8+)
    for (Iterator<String> it = names.iterator(); it.hasNext(); ) {
        if (it.next().equals("a")) it.remove(); // fix 2
    }
    
    List<String> cow = new CopyOnWriteArrayList<>(List.of("a", "b", "c"));
    for (String n : cow) cow.remove(n);        // no exception: iterates a snapshot
    What interviewers listen for
    • Fail-fast throws ConcurrentModificationException
    • Detected through a modCount check, best-effort only
    • Remove via Iterator.remove() or removeIf
    • Snapshot iterators: CopyOnWriteArrayList
    • Weakly consistent iterators: ConcurrentHashMap

    Likely follow-up: Does removing the second-to-last element in a for-each loop throw? · When is CopyOnWriteArrayList a good fit?

  21. 21.How do you design an immutable class in Java?mid

    An immutable object's state can't change after construction. The recipe:

    • Make the class final (or give it only private constructors), so no subclass can add mutable state or override methods.
    • Make every field private final, assigned exactly once in the constructor.
    • Provide no setters. "Modifying" methods return a new instance, the way String and LocalDate do.
    • Make defensive copies of mutable inputs and of any mutable fields you expose, or store an unmodifiable copy with List.copyOf, as in the snippet.
    • Don't let this escape during construction.

    The payoff: immutable objects are inherently thread-safe, safe to share and cache, and safe as map keys because their hash code never changes. Records give you most of this for free, but they're only shallowly immutable: a record holding an ArrayList still needs a defensive copy in its compact constructor.

    public final class Employee {
        private final String name;
        private final List<String> skills;
    
        public Employee(String name, List<String> skills) {
            this.name = Objects.requireNonNull(name);
            this.skills = List.copyOf(skills);   // defensive, unmodifiable copy
        }
    
        public String name() { return name; }
        public List<String> skills() { return skills; }   // safe to hand out
        public Employee withName(String n) { return new Employee(n, skills); }
    }
    What interviewers listen for
    • final class, private final fields, no setters
    • Defensive copies of mutable inputs and outputs
    • "Modifying" methods return new instances
    • Thread-safe and safe as map keys
    • Records are only shallowly immutable

    Likely follow-up: Why is Collections.unmodifiableList alone not enough? · How would you make a record with a list field immutable?

  22. 22.How do you implement a thread-safe singleton? What is wrong with naive double-checked locking?hard

    There are several correct options:

    • Eager initialization: private static final Config INSTANCE = new Config();. It's thread-safe because the JVM initializes a class exactly once, under a lock. Simple, but not lazy.
    • Initialization-on-demand holder, shown in the snippet: the nested Holder class isn't initialized until getInstance() first touches it, so you get laziness and thread safety with no locking on the hot path.
    • Enum singleton: Joshua Bloch's recommendation in Effective Java. The JVM guarantees one instance, and neither reflection nor deserialization can create a second one.
    • Double-checked locking: check, lock, check again. It's only correct if the field is volatile. Without it, reordering can publish the reference before the constructor finishes, so another thread may see a half-built object.

    A synchronized getInstance() is also correct, but it takes a lock on every call. In real applications I'd usually let a DI container such as Spring manage the single instance instead.

    // Lazy and thread-safe with no locking: Holder is initialized on first use
    public final class Config {
        private Config() {}
    
        private static class Holder {
            static final Config INSTANCE = new Config();
        }
    
        public static Config getInstance() { return Holder.INSTANCE; }
    }
    
    // Simplest of all, and safe against reflection and serialization
    public enum Registry { INSTANCE; }
    What interviewers listen for
    • An eager static final field is the simplest
    • Holder idiom: lazy through class initialization
    • Enum singleton resists reflection and serialization
    • Double-checked locking needs a volatile field
    • Prefer container-managed singletons in applications

    Likely follow-up: How can serialization break a classic singleton, and how does readResolve fix it?

  23. 23.What is the difference between intermediate and terminal stream operations, and why are streams called lazy? What does this print?mid

    Intermediate operations such as filter, map, flatMap, sorted, distinct and limit return a new Stream and are lazy: they only describe the pipeline. Terminal operations such as collect, forEach, reduce, count, findFirst and anyMatch trigger execution and produce a result or a side effect. Without a terminal operation, nothing runs at all.

    Elements also flow through the pipeline one at a time, not stage by stage. So the snippet prints filter a, filter bb, map bb, and stops: findFirst is short-circuiting, so "ccc" and "dddd" are never touched.

    Other points worth making: a stream can be consumed only once, and reusing it throws IllegalStateException. Stateful operations keep state across elements; sorted, for example, must see every element before it emits any. And pipeline lambdas should be free of side effects; the prints here are only for demonstration.

    Optional<String> first = Stream.of("a", "bb", "ccc", "dddd")
        .filter(s -> {
            System.out.println("filter " + s);
            return s.length() > 1;
        })
        .map(s -> {
            System.out.println("map " + s);
            return s.toUpperCase();
        })
        .findFirst();
    What interviewers listen for
    • Intermediate ops return a Stream and are lazy
    • Terminal ops trigger the whole pipeline
    • Elements flow through one at a time
    • Short-circuiting ops like findFirst stop early
    • A stream can be consumed only once

    Likely follow-up: What is the difference between map and flatMap? · Why is peek mostly for debugging?

  24. 24.What is a functional interface, and how do lambdas relate to it?mid

    A functional interface has exactly one abstract method. default and static methods don't count, and neither do abstract methods that redeclare public Object methods like equals. The @FunctionalInterface annotation is optional, but it makes the compiler enforce the rule.

    A lambda is a concise implementation of that one method, and its type comes from the target context: s -> s.isEmpty() can be a Predicate<String> or a Function<String, Boolean>. The core interfaces in java.util.function:

    • Function<T, R>: takes a T, returns an R
    • Predicate<T>: takes a T, returns a boolean
    • Consumer<T>: takes a T, returns nothing
    • Supplier<T>: takes nothing, returns a T
    • plus BiFunction, UnaryOperator, BinaryOperator, and primitive versions like IntPredicate that avoid boxing

    A lambda can capture local variables only if they're effectively final. Unlike in an anonymous class, this inside a lambda refers to the enclosing instance, and lambdas are linked at runtime through invokedynamic rather than compiled to a class file each.

    What interviewers listen for
    • Exactly one abstract method
    • @FunctionalInterface is optional but enforced
    • A lambda takes its type from the target context
    • Know Function, Predicate, Consumer and Supplier
    • Captured locals must be effectively final

    Likely follow-up: Why must captured variables be effectively final? · Is Comparator a functional interface even though it declares equals?

  25. 25.Explain the JVM memory areas: heap, stack, metaspace and the rest.mid
    • Heap: shared by all threads; it holds every object and array. The garbage collector manages it, and generational collectors split it into a young generation (Eden plus survivor spaces) and an old generation. Exhausting it gives OutOfMemoryError: Java heap space.
    • Stack: one per thread. Each method call pushes a frame holding local variables, intermediate results and return information. Locals hold primitives directly and objects as references into the heap. Recursing too deeply throws StackOverflowError.
    • Metaspace (Java 8+): class metadata, such as the structure and bytecode of methods, kept in native memory rather than on the heap. It replaced PermGen and is unbounded by default unless you set -XX:MaxMetaspaceSize.
    • PC register: per thread, the address of the bytecode instruction being executed.
    • Native method stack: per thread, for native code.

    HotSpot also keeps a code cache for JIT-compiled machine code. The one-line version: objects live on the heap, while a method's primitives and references live in its frame on that thread's stack.

    What interviewers listen for
    • Heap: shared, objects and arrays, garbage-collected
    • Stack: per thread, frames with locals and references
    • Metaspace replaced PermGen in Java 8
    • Metaspace lives in native memory
    • StackOverflowError vs OutOfMemoryError

    Likely follow-up: What does escape analysis let the JIT do with short-lived objects? · What happens when Metaspace fills up?

  26. 26.How does garbage collection work in Java, and what is the generational hypothesis?mid

    The GC reclaims objects that are no longer reachable from GC roots: local variables on thread stacks, static fields, live threads and JNI references. Because it traces reachability instead of counting references, a cycle of objects pointing at each other is still collected once nothing outside reaches it.

    The weak generational hypothesis says most objects die young, so generational collectors split the heap:

    • Young generation: new objects are allocated in Eden. A frequent, cheap minor GC copies the few survivors into a survivor space, and objects that survive enough collections are promoted to the old generation.
    • Old generation: long-lived objects, collected less often and at greater cost.

    Collections include phases where application threads are paused (stop-the-world); modern collectors do most of their work concurrently to keep those pauses short. HotSpot offers Serial, Parallel (throughput-oriented), G1 (the default on typical server hardware since Java 9) and ZGC (low latency). System.gc() is only a hint that the JVM may ignore.

    What interviewers listen for
    • Collects objects unreachable from GC roots
    • Tracing handles reference cycles
    • Generational hypothesis: most objects die young
    • Minor GCs in the young gen; survivors get promoted
    • G1 is the default collector since Java 9

    Likely follow-up: What are GC roots? · How would you tune GC for a latency-sensitive service?

  27. 27.How do you create a thread in Java, and what is the difference between Runnable and Callable?easy

    You can extend Thread and override run(), or, better, pass a Runnable to a Thread, which separates the task from the thread that runs it. In real code you'd usually submit tasks to an ExecutorService rather than create threads by hand.

    • Runnable: void run(). It returns nothing and can't throw checked exceptions.
    • Callable<V>: V call() throws Exception. It returns a value and may throw checked exceptions.

    Submitting a Callable to an executor gives you a Future<V>. future.get() blocks until the result is ready, and if the task threw, get() throws an ExecutionException that wraps the original.

    A classic gotcha: call start(), not run(). start() creates a new thread, which then calls run(); calling run() directly just executes it on the current thread. And calling start() twice on the same Thread throws IllegalThreadStateException.

    What interviewers listen for
    • Prefer a Runnable over extending Thread
    • Runnable returns nothing, no checked exceptions
    • Callable returns a value and can throw
    • Executors hand back a Future for the result
    • start() creates a thread; run() does not

    Likely follow-up: What are the possible states of a thread? · How do you stop a thread cleanly?

  28. 28.What is the difference between synchronized and ReentrantLock?mid

    Both provide mutual exclusion and memory visibility, and both are reentrant: a thread that holds the lock can acquire it again.

    synchronized uses the object's built-in monitor. It's block-structured, so the lock is always released when the block exits, even on an exception, and it works with wait/notify. It's simple and hard to misuse.

    ReentrantLock (java.util.concurrent.locks, Java 5) is an explicit lock that you must release in a finally block. In return it adds:

    • tryLock(), optionally with a timeout, to avoid waiting forever
    • lockInterruptibly(), so a waiting thread can be interrupted
    • an optional fairness policy that favours the longest-waiting thread
    • multiple Condition objects instead of a single wait set

    Performance is comparable on modern JVMs, so I default to synchronized and switch to ReentrantLock when I need those features. For read-heavy data, ReentrantReadWriteLock or StampedLock can help. One more factor: in Java 21, a virtual thread that blocks inside synchronized pins its carrier thread; Java 24 removed that limitation.

    What interviewers listen for
    • Both are reentrant and guarantee visibility
    • synchronized releases the lock automatically
    • ReentrantLock needs unlock() in finally
    • Adds tryLock, interruptible waits, fairness, conditions
    • Default to synchronized unless you need the extras

    Likely follow-up: What is a ReadWriteLock good for? · What does "fair" mean for a lock, and what does it cost?

  29. 29.What does volatile guarantee, and what is the happens-before relationship?hard

    volatile gives a field two guarantees. Visibility: a write by one thread is seen by subsequent reads in other threads, rather than lingering in a register or cache. Ordering: the compiler and CPU can't reorder memory operations around a volatile access in ways that would break that visibility. Without volatile, the loop in the snippet might never see running = false, because the JIT is allowed to hoist the read out of the loop.

    It does not make compound actions atomic: count++ on a volatile field is still a read-modify-write race. Use AtomicInteger or a lock for that.

    Happens-before is the Java Memory Model's visibility rule: if action A happens-before B, then B sees A's effects. Key edges are program order within a thread, unlocking a monitor before a later lock of it, a volatile write before a later read of that field, Thread.start() before the new thread's actions, and a thread's actions before another thread returns from join() on it. The relation is transitive, so everything written before a volatile write is visible to a thread that reads it.

    class Worker implements Runnable {
        private volatile boolean running = true;   // without volatile, run() may never stop
    
        public void run() {
            while (running) {
                // do work
            }
        }
    
        public void stop() { running = false; }     // called from another thread
    }
    What interviewers listen for
    • Visibility of writes across threads
    • Prevents harmful reordering around the access
    • Not atomic: count++ is still a race
    • Happens-before defines what a read is guaranteed to see
    • Edges: locks, volatile, start/join, transitivity

    Likely follow-up: Why does double-checked locking need volatile? · When would you use AtomicInteger instead?

  30. 30.What is a deadlock, and how do you prevent one in Java?mid

    A deadlock is when two or more threads each hold a lock another one needs, so they all wait forever. In the snippet, one thread locks a and waits for b, while the other locks b and waits for a.

    It requires four conditions at once: mutual exclusion, hold and wait, no preemption and circular wait. Prevention means breaking one of them:

    • Lock ordering: always acquire locks in the same global order, as safeTransfer does. This breaks circular wait and is the most common fix.
    • Timeouts: ReentrantLock.tryLock(timeout, unit) lets a thread back off and retry instead of waiting forever.
    • Smaller critical sections: hold as few locks as possible, and never call unknown code such as callbacks while holding one.
    • Higher-level tools: concurrent collections, atomics or message passing often avoid multiple locks entirely.

    To diagnose one, take a thread dump with jstack or jcmd <pid> Thread.print: HotSpot reports "Found one Java-level deadlock" along with the threads and locks involved. ThreadMXBean.findDeadlockedThreads() does the same programmatically.

    // Thread 1: transfer(a, b)   Thread 2: transfer(b, a)   -> can deadlock
    void transfer(Account from, Account to, int amount) {
        synchronized (from) {
            synchronized (to) { from.debit(amount); to.credit(amount); }
        }
    }
    // Fix: acquire locks in one global order, e.g. by account id
    void safeTransfer(Account from, Account to, int amount) {
        Account first  = from.id() < to.id() ? from : to;
        Account second = first == from ? to : from;
        synchronized (first) {
            synchronized (second) { from.debit(amount); to.credit(amount); }
        }
    }
    What interviewers listen for
    • Threads wait forever for locks held by each other
    • Four conditions, including circular wait
    • Consistent global lock ordering prevents it
    • tryLock with a timeout as a fallback
    • Diagnose with a thread dump

    Likely follow-up: What is the difference between deadlock, livelock and starvation?

  31. 31.How do ExecutorService and thread pools work, and how would you configure one for production?mid

    An ExecutorService decouples submitting tasks from running them: you call submit() or invokeAll(), and a pool of reusable worker threads executes the tasks. That avoids creating a thread per task and caps concurrency.

    ThreadPoolExecutor is configured with a core size, a maximum size, a keep-alive time, a work queue and a rejection policy. A new task starts a thread if fewer than the core number are running; otherwise it's queued; only when the queue is full does the pool grow towards the maximum; beyond that the task is rejected (AbortPolicy by default, or CallerRunsPolicy for back-pressure).

    The Executors factories hide risks: newFixedThreadPool uses an unbounded queue and newCachedThreadPool can create unbounded threads, so in production I build a ThreadPoolExecutor with a bounded queue. Size CPU-bound pools near the core count and I/O-bound pools larger. Always shut pools down with shutdown() and awaitTermination(); since Java 19, ExecutorService is AutoCloseable, so try-with-resources works too.

    What interviewers listen for
    • Reuses worker threads and caps concurrency
    • Core threads first, then the queue, then grow to max
    • Rejection policy applies when queue and pool are full
    • Executors factories can be unbounded
    • Always shut the pool down

    Likely follow-up: What does CallerRunsPolicy do, and why is it useful? · How do you size a pool for I/O-bound work?

  32. 32.What is Optional for, and how should and shouldn't you use it?mid

    Optional<T> (Java 8) is a container that holds either a value or nothing. It's designed mainly as a return type for methods that may legitimately have no result, so the absence shows up in the signature and callers are pushed to handle it instead of hitting a NullPointerException.

    Idiomatic use is functional: map, flatMap and filter to transform, then orElse, orElseGet, orElseThrow or ifPresentOrElse (Java 9) to finish. A common trap is that orElse(x) always evaluates its argument, while orElseGet(supplier) only calls it when the Optional is empty, which matters when the default is expensive.

    What to avoid:

    • calling get() without checking; prefer orElseThrow() (Java 10), which states the intent
    • Optional fields, parameters or collection elements; it isn't Serializable and adds an allocation
    • Optional<List<T>>; return an empty list instead
    • ever returning null from a method declared to return Optional
    String city = userRepo.findById(id)       // returns Optional<User>
        .map(User::address)
        .map(Address::city)
        .orElse("Unknown");
    
    User user = userRepo.findById(id)
        .orElseThrow(() -> new NotFoundException(id));
    
    opt.orElse(loadDefault());          // loadDefault() runs even if opt has a value
    opt.orElseGet(this::loadDefault);   // runs only when opt is empty
    What interviewers listen for
    • Mainly a return type for "maybe no result"
    • Chain map/flatMap/filter, then an orElse variant
    • orElse is eager, orElseGet is lazy
    • Avoid bare get(); prefer orElseThrow()
    • Not for fields, parameters or collections

    Likely follow-up: What is the difference between map and flatMap on an Optional?

  33. 33.How does try-with-resources work, and what are suppressed exceptions?easy

    try-with-resources (Java 7) declares resources in parentheses after try and closes them automatically when the block exits, normally or through an exception. Each resource must implement AutoCloseable, which Closeable extends.

    Key details:

    • Resources are closed in reverse order of declaration: out before in in the snippet.
    • They're closed before any catch or finally block of the same statement runs.
    • If the body throws and close() also throws, the body's exception is the one propagated, and the close exception is attached to it as a suppressed exception, available through getSuppressed(). With a hand-written finally, the close exception would silently replace the original.
    • Since Java 9 you can list an existing effectively final variable: try (in) { ... }.

    It replaces the error-prone pattern of null checks and nested try/finally blocks, so it's the standard way to handle streams, JDBC connections and other resources.

    try (BufferedReader in = Files.newBufferedReader(Path.of("in.txt"));
         BufferedWriter out = Files.newBufferedWriter(Path.of("out.txt"))) {
        String line;
        while ((line = in.readLine()) != null) {
            out.write(line.toUpperCase());
            out.newLine();
        }
    } catch (IOException e) {
        // runs after both resources are closed: out first, then in
        System.err.println("Copy failed: " + e.getMessage());
    }
    What interviewers listen for
    • Resources must implement AutoCloseable
    • Closed automatically, in reverse declaration order
    • Closed before catch and finally run
    • Close failures become suppressed exceptions
    • Java 9 allows effectively final variables

    Likely follow-up: How would you write your own resource class for try-with-resources?

  34. 34.What is type erasure in Java generics, and what limitations does it cause?mid

    Generics (Java 5) let you parameterize types, like List<String>, so the compiler catches type errors and you don't need casts. They're implemented by type erasure: the compiler checks the types, then erases them, replacing each type parameter with its bound (Object if unbounded), inserting casts where needed, and generating bridge methods so overriding still works. That kept generic code compatible with older, pre-generics libraries.

    Because type arguments don't exist at runtime:

    • List<String> and List<Integer> share one runtime class, so getClass() can't tell them apart.
    • You can't write new T(), new T[10] or T.class.
    • Overloads like process(List<String>) and process(List<Integer>) clash, because they have the same erasure.
    • Primitives can't be type arguments: List<int> isn't allowed, so values get boxed.
    • A static field can't use the class's type parameter.

    The usual workaround is passing a Class<T> token, as in repository.find(User.class).

    What interviewers listen for
    • Compile-time type safety without casts
    • Erasure replaces type parameters with their bounds
    • Erasure kept compatibility with pre-generics code
    • No new T(), generic arrays or T.class
    • Pass a Class<T> token when you need the type

    Likely follow-up: What is a bridge method? · Why can you not create an array of List<String>?

  35. 35.Explain generic wildcards and the PECS rule. When do you use ? extends T versus ? super T?hard

    Generics are invariant: a List<Integer> is not a List<Number>, even though Integer is a Number. If it were, you could add a Double to a list of integers through the Number view. Wildcards restore flexibility safely:

    • ? extends T (upper bound): the list holds some subtype of T. You can read elements as T, but you can't add anything except null, because the exact type is unknown.
    • ? super T (lower bound): the list holds some supertype of T. You can add T values, but reading only gives you Object.
    • ?: an unknown type, for code that doesn't care, like counting elements.

    PECS, from Effective Java, means Producer Extends, Consumer Super: if a parameter produces values you read, use extends; if it consumes values you write, use super. The textbook example is Collections.copy(List<? super T> dest, List<? extends T> src). Put wildcards in parameters, not return types, which would force callers to deal with them.

    static double sum(Collection<? extends Number> nums) {   // producer: we only read
        double total = 0;
        for (Number n : nums) total += n.doubleValue();
        return total;
    }
    
    static void fillWithInts(List<? super Integer> sink) {   // consumer: we only write
        for (int i = 0; i < 3; i++) sink.add(i);
    }
    
    List<Integer> ints = new ArrayList<>();
    List<Number> nums = new ArrayList<>();
    fillWithInts(ints); fillWithInts(nums);     // both compile
    sum(ints); sum(nums);                       // both compile, yet "nums = ints" would not
    What interviewers listen for
    • Generics are invariant
    • ? extends T: read as T, cannot add
    • ? super T: can add T, reads give Object
    • Producer Extends, Consumer Super
    • Wildcards in parameters, not return types

    Likely follow-up: Why are arrays covariant while generics are not? · Why is Collections.max declared the way it is?

  36. 36.How does HashSet work internally, and how does it compare with LinkedHashSet and TreeSet?mid

    A HashSet is a thin wrapper around a HashMap. Each element is stored as a key, and every value is the same shared dummy object. add(e) is essentially map.put(e, PRESENT) == null: if the key already existed, put returns the old value and add returns false.

    So everything about HashMap applies: average O(1) add, remove and contains, one null element allowed, no ordering guarantee, and correctness that depends entirely on the elements' hashCode and equals.

    • LinkedHashSet is backed by a LinkedHashMap, which threads a doubly linked list through the entries, so iteration follows insertion order at a small memory cost.
    • TreeSet is backed by a TreeMap, a red-black tree. Elements stay sorted by natural ordering or a Comparator, operations are O(log n), and duplicates are detected with the comparison, not equals.

    The classic bug: mutating an element's fields after adding it changes its hash code, so contains and remove can no longer find it.

    What interviewers listen for
    • Backed by a HashMap; elements are the keys
    • Every value is a shared dummy object
    • O(1) average; relies on hashCode and equals
    • LinkedHashSet keeps insertion order
    • TreeSet is sorted and uses compareTo

    Likely follow-up: Why does Set.add return a boolean?

  37. 37.How many objects does new String("hello") create? What does this code print?mid

    new String("hello") involves up to two String objects. The literal "hello" refers to a pooled instance, created the first time that literal is resolved and reused afterwards, and new always creates a separate object on the heap. If the literal is already pooled, only one new object is created.

    So the snippet prints true, false, true, true:

    • a == b: identical literals refer to the same pooled instance.
    • a == c: new always yields a distinct object.
    • a == d: intern() returns the canonical pooled instance.
    • a == e: part is a final variable initialized with a constant, so part + "lo" is a compile-time constant expression. It's folded to "hello" and interned. Remove final and the concatenation happens at runtime, creating a new object, so it would print false.

    This is exactly why strings are compared with equals(): identity depends on how each string was produced.

    String a = "hello";
    String b = "hello";
    String c = new String("hello");
    String d = c.intern();
    final String part = "hel";
    String e = part + "lo";
    
    System.out.println(a == b);   // ?
    System.out.println(a == c);   // ?
    System.out.println(a == d);   // ?
    System.out.println(a == e);   // ?
    What interviewers listen for
    • The literal is pooled; new makes a separate object
    • Up to two objects, one if already pooled
    • intern() returns the pooled instance
    • Constant expressions are folded and interned
    • Runtime concatenation creates new objects

    Likely follow-up: When would calling intern() yourself be a good idea?

  38. 38.How does TreeMap work, and when would you choose it over HashMap?mid

    TreeMap is a red-black tree, a self-balancing binary search tree that keeps keys sorted by natural ordering (Comparable) or by a Comparator passed to the constructor. get, put, remove and containsKey are O(log n), versus O(1) on average for HashMap.

    You choose it when order matters. As a NavigableMap it offers:

    • firstKey(), lastKey(), pollFirstEntry()
    • floorKey(k), ceilingKey(k), lowerKey(k), higherKey(k) for nearest-match lookups
    • range views: headMap, tailMap, subMap, descendingMap

    Typical uses are leaderboards, time series ("latest price at or before this timestamp"), scheduling and range queries.

    Gotchas: keys are compared with compareTo or compare, not equals, so an ordering inconsistent with equals can merge keys you think are distinct. With natural ordering, a null key throws NullPointerException. And it isn't thread-safe; ConcurrentSkipListMap is the concurrent sorted alternative.

    What interviewers listen for
    • Red-black tree with sorted keys
    • O(log n) operations vs O(1) average for HashMap
    • NavigableMap: floor, ceiling and range views
    • Key identity uses the comparison, not equals
    • ConcurrentSkipListMap for concurrent sorted maps

    Likely follow-up: How would you find the entry with the closest key at or below a timestamp?

  39. 39.How does class loading work in the JVM? Explain the built-in class loaders and parent delegation.hard

    A class goes through loading (finding its bytes and creating a Class object), linking (verifying the bytecode, preparing static fields with default values, and resolving symbolic references, often lazily) and initialization (running static initializers and static field assignments). Initialization is lazy: it happens on first active use, such as new, a static method call or reading a non-constant static field, and the JVM guarantees it runs once, thread-safely.

    Since Java 9 there are three built-in loaders:

    • Bootstrap: part of the JVM itself; loads core modules like java.base.
    • Platform (formerly the extension loader): other platform modules, such as java.sql.
    • Application (system): your class path and module path.

    With parent delegation, a loader first asks its parent and loads the class itself only if the parent can't. That stops application code from replacing core classes like java.lang.String. A class's runtime identity is its name plus its defining loader, which is how app servers and plugin systems isolate versions, and why you can get a ClassCastException between two "identical" classes.

    What interviewers listen for
    • Loading, linking (verify, prepare, resolve), initialization
    • Initialization is lazy and runs once
    • Bootstrap, platform and application loaders
    • Parent delegation protects core classes
    • Class identity is name plus defining loader

    Likely follow-up: What is the difference between ClassNotFoundException and NoClassDefFoundError? · Why would you write a custom class loader?

  40. 40.What are the SOLID principles? Give a Java example of each.mid
    • S, Single Responsibility: a class should have one reason to change. An OrderService that also renders PDFs and sends emails should be split so each concern can change independently.
    • O, Open/Closed: open for extension, closed for modification. Add a new PaymentMethod implementation instead of another branch in a giant switch.
    • L, Liskov Substitution: a subtype must work wherever its parent is expected without surprising callers. The classic violation is Square extends Rectangle, where setting the width silently changes the height.
    • I, Interface Segregation: prefer small, focused interfaces. Don't force a read-only repository to implement delete() just to throw UnsupportedOperationException.
    • D, Dependency Inversion: high-level code depends on abstractions, not concrete classes. OrderService receives a PaymentGateway interface through its constructor, which is exactly what dependency injection in Spring does, and makes the class easy to test with a fake.

    I'd add that these are guidelines, not laws: applied dogmatically, they produce lots of tiny classes and needless indirection.

    What interviewers listen for
    • Single responsibility: one reason to change
    • Open/closed: extend without modifying
    • Liskov: subtypes honour the parent contract
    • Interface segregation: small, focused interfaces
    • Dependency inversion: depend on abstractions

    Likely follow-up: How does Spring support dependency inversion? · Which SOLID principle does the strategy pattern embody?

  41. 41.What is CompletableFuture, and how does it improve on a plain Future?hard

    CompletableFuture (Java 8) is a Future you can compose. A plain Future only lets you block with get(); a CompletableFuture lets you attach what happens next, so you build a non-blocking pipeline.

    • Start: supplyAsync (returns a value) or runAsync. Without an executor argument they use the common ForkJoinPool, a poor place for blocking I/O, so pass your own pool.
    • Transform: thenApply is like map; thenCompose is like flatMap, for a step that itself returns a future; thenAccept consumes the result.
    • Combine: thenCombine merges two independent futures, as in the snippet; allOf and anyOf handle many.
    • Errors: exceptionally supplies a fallback, handle sees either the result or the exception, and whenComplete observes both without changing the result.
    • Timeouts (Java 9+): orTimeout and completeOnTimeout.

    join() throws an unchecked CompletionException, while get() throws checked exceptions. Non-Async stages may run on whichever thread completed the previous stage, so use the ...Async variants with an executor for heavy work.

    ExecutorService pool = Executors.newFixedThreadPool(8);
    
    CompletableFuture<User> user =
        CompletableFuture.supplyAsync(() -> userClient.fetch(id), pool);
    CompletableFuture<List<Order>> orders =
        CompletableFuture.supplyAsync(() -> orderClient.fetch(id), pool);
    
    CompletableFuture<Dashboard> dashboard = user
        .thenCombine(orders, Dashboard::new)       // both run in parallel; merge results
        .orTimeout(2, TimeUnit.SECONDS)            // Java 9+
        .exceptionally(ex -> Dashboard.empty());   // fallback on any failure
    
    Dashboard d = dashboard.join();
    What interviewers listen for
    • Composable, non-blocking alternative to Future
    • thenApply maps, thenCompose flat-maps
    • thenCombine and allOf for parallel work
    • exceptionally and handle for errors
    • Pass your own executor for blocking tasks

    Likely follow-up: What is the difference between thenApply and thenApplyAsync? · How would you cancel a chain of stages?

  42. 42.What are virtual threads, how do they work, and when should you use them?hard

    Virtual threads (final in Java 21) are lightweight threads managed by the JVM rather than the OS. They're cheap enough to create one per task, so you can run hundreds of thousands, even millions, at once.

    A virtual thread runs by being mounted on one of a small pool of platform carrier threads, a ForkJoinPool sized to the number of cores by default. When it blocks on I/O, a lock or sleep, the JVM unmounts it and the carrier picks up another virtual thread. So plain, blocking, thread-per-request code gets the scalability of async code without callbacks or reactive chains.

    Things to know:

    • They help I/O-bound workloads with many concurrent tasks, not CPU-bound ones.
    • Don't pool them: create one per task, and limit concurrency against a database with a Semaphore.
    • Pinning: in Java 21, blocking inside synchronized or native code keeps the carrier occupied. Java 24 fixed this for synchronized; on 21, prefer ReentrantLock around blocking calls.
    • Heavy ThreadLocal use gets expensive with millions of threads; scoped values are the intended replacement.
    try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
        for (int i = 0; i < 10_000; i++) {
            int id = i;
            executor.submit(() -> {
                Thread.sleep(Duration.ofSeconds(1));   // blocking is cheap here
                return id;
            });
        }
    }   // close() waits for every task to finish
    
    Thread.ofVirtual().start(() -> System.out.println("hi"));
    Thread.startVirtualThread(() -> System.out.println("hi"));
    What interviewers listen for
    • JVM-managed and cheap: one thread per task
    • Unmount from carrier threads when blocking
    • Help I/O-bound, not CPU-bound, work
    • Never pool them; limit with a Semaphore
    • Java 21 pins the carrier inside synchronized

    Likely follow-up: How do virtual threads compare with reactive programming? · What is structured concurrency?

  43. 43.What is a record, what does the compiler generate for it, and what are its limitations?mid

    A record (final in Java 16) is a concise, transparent carrier for immutable data. From the header record Money(BigDecimal amount, String currency) the compiler generates:

    • a private final field for each component
    • a canonical constructor
    • accessors named after the components: amount(), not getAmount()
    • equals, hashCode and toString based on all components

    Records are implicitly final and extend java.lang.Record, so they can't extend another class, but they can implement interfaces and declare static fields, methods and nested types. They can't declare extra instance fields.

    A compact constructor, like the one in the snippet, validates or normalizes arguments without repeating the assignments. Records are only shallowly immutable, so a List component should be copied with List.copyOf there. They're a great fit for DTOs, value objects, map keys and multiple return values, and they work with record patterns in switch (Java 21). They're a poor fit for JPA entities, which need mutability and a no-arg constructor.

    public record Money(BigDecimal amount, String currency) {
        public Money {                                   // compact canonical constructor
            Objects.requireNonNull(currency);
            if (amount.signum() < 0) throw new IllegalArgumentException("negative");
        }
        public Money add(Money other) {
            return new Money(amount.add(other.amount), currency);
        }
    }
    
    Money m = new Money(new BigDecimal("9.99"), "USD");
    m.amount();                // accessor: amount(), not getAmount()
    System.out.println(m);     // e.g. Money[amount=9.99, currency=USD]
    What interviewers listen for
    • Final in Java 16; a transparent immutable data carrier
    • Generates constructor, accessors, equals, hashCode, toString
    • Implicitly final; can implement interfaces
    • Compact constructor for validation
    • Only shallowly immutable

    Likely follow-up: Can a record have a non-canonical constructor? · Why are records a poor fit for JPA entities?

  44. 44.How does Java serialization work, and what does the transient keyword do?hard

    Built-in serialization turns an object graph into bytes with ObjectOutputStream.writeObject and rebuilds it with ObjectInputStream.readObject. A class opts in by implementing the marker interface Serializable, and every non-transient field must itself be serializable, or you get a NotSerializableException.

    • transient fields are skipped: passwords, caches, or things like a thread or a connection that make no sense elsewhere. After deserialization they hold default values (null, 0, false). Static fields aren't serialized either; they belong to the class.
    • serialVersionUID identifies the class version. If you don't declare one, it's computed from the class structure, so a harmless change can cause an InvalidClassException when reading old data. Declare it explicitly.
    • Deserialization doesn't run the constructors of serializable classes; only the no-arg constructor of the first non-serializable superclass runs.
    • Hooks: writeObject/readObject for custom formats, readResolve to keep singletons unique, and Externalizable for full control.

    Deserializing untrusted data is a well-known security risk, so modern code prefers JSON or Protobuf, and uses ObjectInputFilter when Java serialization is unavoidable.

    What interviewers listen for
    • Opt in with the Serializable marker interface
    • transient fields are skipped and restored as defaults
    • Declare serialVersionUID explicitly
    • Constructors of serializable classes are not run
    • Deserializing untrusted data is a security risk

    Likely follow-up: How can serialization break a singleton? · How are records deserialized differently from ordinary classes?

  45. 45.Can a Java program leak memory despite garbage collection? What are the common causes, and how do you find a leak?hard

    Yes. The GC only frees unreachable objects, so a Java leak means objects that are still referenced but will never be used again. Typical causes:

    • Static or long-lived collections that only grow, like a map used as a cache with no eviction. Use a bounded cache instead.
    • Listeners and callbacks that are registered but never removed.
    • ThreadLocal values in thread pools: pooled threads don't die, so values stay until you call remove().
    • Inner classes and lambdas that capture an outer object and outlive it.
    • Mutable keys in a HashMap: once a key's hash changes, the entry can't be found or removed, but it's still referenced.
    • Unclosed resources, and class loader leaks in app servers, which pin Metaspace across redeploys.

    To find one, check whether heap usage after full GCs keeps climbing. Then take a heap dump (jcmd <pid> GC.heap_dump heap.hprof, or -XX:+HeapDumpOnOutOfMemoryError) and analyse it in Eclipse MAT or VisualVM, looking for the objects with the largest retained size and their paths to GC roots.

    What interviewers listen for
    • A leak is reachable but no longer needed
    • Growing static collections and unbounded caches
    • Unremoved listeners and ThreadLocals in pools
    • Heap after full GC keeps rising
    • Heap dump plus MAT: retained size, GC root paths

    Likely follow-up: How would a WeakHashMap help, and what are its pitfalls?

  46. 46.What is the JIT compiler, and how does HotSpot decide what to compile?hard

    javac compiles source to platform-neutral bytecode. At runtime, HotSpot starts by interpreting that bytecode while profiling it: counting method calls and loop iterations, and recording which types and branches actually occur. Code that gets hot is compiled to native machine code by the Just-In-Time compiler and kept in the code cache.

    HotSpot uses tiered compilation, on by default since Java 8:

    • C1, the client compiler, compiles quickly with light optimization, so code gets faster early.
    • C2, the server compiler, recompiles the hottest code with aggressive, profile-guided optimization.

    Key optimizations are inlining (the biggest win), escape analysis (objects that never escape a method may avoid heap allocation entirely), loop optimizations, lock elision, and speculative devirtualization: if a call site has only ever seen one implementation, it's called directly. If an assumption later breaks, say a new subclass is loaded, the JVM deoptimizes that code back to the interpreter and can recompile it.

    That's why Java needs warm-up, and why microbenchmarks need a harness like JMH.

    What interviewers listen for
    • Interpret first, profile, then compile hot code
    • Tiered compilation: C1 compiles fast, C2 optimizes
    • Inlining and escape analysis are key
    • Speculative optimizations with deoptimization
    • Warm-up matters; benchmark with JMH

    Likely follow-up: Why can a microbenchmark without JMH be misleading? · How does ahead-of-time compilation such as GraalVM Native Image differ?

  47. 47.What are method references, and what are the different kinds?easy

    A method reference is shorthand for a lambda that just calls an existing method: list.forEach(System.out::println) means the same as list.forEach(x -> System.out.println(x)). Like a lambda, it needs a functional interface as its target type, and the compiler matches the method's signature against that interface's single abstract method.

    The four kinds:

    • Static: Integer::parseInt stands for s -> Integer.parseInt(s).
    • Bound instance: System.out::println calls the method on one specific object, which is evaluated once, when the reference is created.
    • Unbound instance: String::toUpperCase takes the receiver as its first argument, s -> s.toUpperCase(). With two parameters, String::equalsIgnoreCase becomes (a, b) -> a.equalsIgnoreCase(b).
    • Constructor: ArrayList::new, or int[]::new for arrays.

    I use them when they read more clearly than the lambda, which is usually true for simple delegation. If the lambda does more than one call or reorders arguments, a lambda is clearer.

    Function<String, Integer> parse = Integer::parseInt;          // static method
    Consumer<String> print = System.out::println;                 // bound to one object
    Function<String, String> upper = String::toUpperCase;         // unbound: s -> s.toUpperCase()
    BiPredicate<String, String> same = String::equalsIgnoreCase;  // (a, b) -> a.equalsIgnoreCase(b)
    Supplier<List<String>> newList = ArrayList::new;              // constructor
    IntFunction<int[]> newArray = int[]::new;                     // array constructor
    What interviewers listen for
    • Shorthand for a lambda that calls one method
    • Needs a functional interface as its target type
    • Kinds: static, bound, unbound and constructor
    • Unbound: the receiver becomes the first argument
    • A bound receiver is evaluated once

    Likely follow-up: What happens if the bound receiver is null when the reference is created?

  48. 48.When should you use parallel streams, and what are the pitfalls?hard

    parallelStream() or .parallel() splits the source with a Spliterator and processes the pieces on the common ForkJoinPool, whose default parallelism is one less than the number of cores, because the calling thread helps too. It only pays off when the work outweighs the cost of splitting, scheduling and merging.

    Good candidates:

    • large inputs with CPU-heavy, stateless, independent work per element
    • sources that split cheaply: arrays, ArrayList, IntStream.range; not LinkedList or Stream.iterate
    • associative operations such as sum, reduce and collect

    Pitfalls:

    • Shared mutable state: adding to a plain ArrayList from forEach is a race; use collect instead.
    • Blocking I/O: it ties up the common pool, which the whole JVM shares with other parallel streams and default async CompletableFuture tasks.
    • Ordering: forEach order becomes non-deterministic, and forEachOrdered, findFirst or limit on ordered streams add overhead.
    • Small inputs: usually slower than sequential.

    My rule: default to sequential, and go parallel only after measuring a real speed-up.

    What interviewers listen for
    • Runs on the common ForkJoinPool
    • Needs large, CPU-bound, stateless work
    • The source must split well: arrays, ArrayList
    • Never mutate shared state or block
    • Measure before switching to parallel

    Likely follow-up: Why does LinkedList parallelize poorly?

  49. 49.How do you group, partition and aggregate data with Collectors?mid

    collect is the terminal operation that accumulates a stream into a result, and Collectors provides the recipes:

    • groupingBy(classifier) builds a Map<K, List<T>>. An optional second argument is a downstream collector applied to each group: counting(), averagingDouble, summingInt, mapping, maxBy, or another groupingBy for nested maps. A three-argument form lets you pick the map type, such as TreeMap::new for sorted keys.
    • partitioningBy(predicate) splits into a Map<Boolean, List<T>> that always has both true and false keys.
    • toMap(keyFn, valueFn) throws IllegalStateException on a duplicate key unless you pass a merge function, as the snippet does with Double::sum. It also throws NullPointerException for a null value.
    • joining concatenates strings with an optional delimiter, prefix and suffix.
    • teeing (Java 12) combines the results of two collectors.

    For a plain list, Java 16's stream.toList() returns an unmodifiable list, while Collectors.toList() makes no promise about mutability (in practice it's an ArrayList).

    record Employee(String name, String dept, double salary) {}
    
    Map<String, List<Employee>> byDept = staff.stream()
        .collect(Collectors.groupingBy(Employee::dept));
    Map<String, Long> headcount = staff.stream()
        .collect(Collectors.groupingBy(Employee::dept, Collectors.counting()));
    Map<String, Double> avgPay = staff.stream()
        .collect(Collectors.groupingBy(Employee::dept, Collectors.averagingDouble(Employee::salary)));
    Map<Boolean, List<Employee>> seniors = staff.stream()
        .collect(Collectors.partitioningBy(e -> e.salary() > 100_000));
    Map<String, Double> payByName = staff.stream()
        .collect(Collectors.toMap(Employee::name, Employee::salary, Double::sum));
    String names = staff.stream().map(Employee::name).collect(Collectors.joining(", "));
    What interviewers listen for
    • groupingBy with downstream collectors
    • partitioningBy always has true and false keys
    • toMap needs a merge function for duplicate keys
    • joining for building strings
    • stream.toList() returns an unmodifiable list

    Likely follow-up: How would you group into a TreeMap of sets?

  50. 50.What problem does the Builder pattern solve, and how would you implement one in Java?mid

    Builder solves constructing objects with many parameters, especially optional ones. The alternatives are poor: telescoping constructors (Pizza(size), Pizza(size, cheese), Pizza(size, cheese, olives) and so on) are hard to read and easy to call with arguments in the wrong order, while the JavaBeans style, a no-arg constructor plus setters, leaves the object mutable and possibly half-initialized.

    The classic implementation, shown in the snippet, is a static nested Builder:

    • required parameters go in the builder's constructor
    • optional ones are fluent methods that return this
    • build() validates the combination and calls a private constructor, so the product can be immutable

    The call site reads like named arguments: new Pizza.Builder("large").cheese().olives().build(). The JDK uses the idea in HttpClient.newBuilder(), HttpRequest.newBuilder() and Stream.builder(), and Lombok's @Builder generates one for you. The cost is extra code, so for two or three parameters a constructor, a static factory or a record is simpler.

    public final class Pizza {
        private final String size;
        private final boolean cheese, olives;
        private Pizza(Builder b) { size = b.size; cheese = b.cheese; olives = b.olives; }
    
        public static class Builder {
            private final String size;           // required
            private boolean cheese, olives;      // optional
            public Builder(String size) { this.size = Objects.requireNonNull(size); }
            public Builder cheese() { cheese = true; return this; }
            public Builder olives() { olives = true; return this; }
            public Pizza build() { return new Pizza(this); }
        }
    }
    What interviewers listen for
    • For objects with many optional parameters
    • Avoids telescoping constructors and mutable JavaBeans
    • Static nested builder with fluent methods
    • build() validates and returns an immutable object
    • Overkill for just a few parameters

    Likely follow-up: How would you enforce required fields at compile time?

  51. 51.Explain the Factory and Strategy patterns. How do they differ, and how are they used together?mid

    Factory is a creational pattern: it hides which concrete class gets instantiated behind a method that returns an interface type. A caller asks for "a notifier for this channel" and doesn't know or care that it got an SmsNotifier. Variants include:

    • a static factory method, like List.of, Optional.of or NumberFormat.getInstance()
    • the Factory Method pattern, where a subclass decides which product to create
    • Abstract Factory, which creates families of related objects

    Strategy is a behavioural pattern: a family of interchangeable algorithms behind one interface, chosen at runtime. Comparator is the JDK's classic strategy: sort stays the same while the ordering varies. Since Java 8, a strategy is often just a lambda.

    The difference is what varies: a factory varies object creation, a strategy varies behaviour. They combine naturally: a factory, often a Map<PaymentType, PaymentStrategy> or a Spring-injected list of beans, picks the right strategy, and the caller just runs it. Both replace sprawling if/switch blocks and follow the open/closed principle.

    What interviewers listen for
    • Factory hides which class is instantiated
    • Strategy swaps algorithms behind one interface
    • Factory is creational; strategy is behavioural
    • Comparator is a built-in strategy
    • A factory often selects the strategy

    Likely follow-up: How would you register strategies automatically in Spring?

  52. 52.What is the Observer pattern, and where does it show up in Java?mid

    Observer defines a one-to-many dependency: a subject keeps a list of observers and notifies them all when its state changes, without knowing their concrete types. It decouples the thing that changes from the things that react, and it's the foundation of event-driven code.

    In Java it shows up as:

    • listeners in UI toolkits, such as Swing's ActionListener, and java.beans.PropertyChangeListener
    • application events in Spring, with ApplicationEventPublisher and @EventListener
    • reactive streams: java.util.concurrent.Flow (Java 9), Project Reactor and RxJava are observer plus back-pressure

    The old java.util.Observer and Observable have been deprecated since Java 9, partly because Observable is a class rather than an interface and its event model is very limited.

    Pitfalls worth mentioning: the lapsed listener problem, where forgotten registrations cause memory leaks; one slow or throwing observer blocking the rest when notification is synchronous; unspecified notification order; and thread safety, commonly handled by keeping observers in a CopyOnWriteArrayList.

    What interviewers listen for
    • A subject notifies its observers of changes
    • Decouples the source from the reactions
    • Listeners, Spring events, reactive streams
    • java.util.Observer deprecated since Java 9
    • Unregister listeners to avoid leaks

    Likely follow-up: How is publish/subscribe different from the Observer pattern?

  53. 53.What happens when a class inherits the same default method from two interfaces? What are the resolution rules?hard

    Default methods (Java 8) bring back a limited form of the diamond problem: if two interfaces provide the same default method, which one does the class get? Java resolves it with three rules, in order:

    • Classes win: a method declared in the class or inherited from a superclass beats any interface default.
    • The most specific interface wins: if B extends A and both define the default, B's version is used.
    • Otherwise it's a compile error: the class must override the method itself. Inside the override it can call a particular parent's version with InterfaceName.super.method(), as Duck does.

    There's no ambiguity about state, because interfaces have no instance fields, which is why Java allows this but not multiple inheritance of classes.

    Related details: static interface methods aren't inherited, so you always call them as Flyer.helper(); private interface methods (Java 9) let several defaults share code without exposing it; and a default method can't override an Object method such as equals or toString.

    interface Flyer   { default String move() { return "fly"; } }
    interface Swimmer { default String move() { return "swim"; } }
    
    class Duck implements Flyer, Swimmer {
        @Override
        public String move() {              // required: otherwise a compile error
            return Flyer.super.move() + " and " + Swimmer.super.move();
        }
    }
    
    new Duck().move();   // "fly and swim"
    What interviewers listen for
    • Class methods beat interface defaults
    • The more specific interface wins
    • Otherwise the class must override the method
    • Pick a parent with Interface.super.method()
    • Interfaces have no state, so the diamond stays manageable

    Likely follow-up: Why can a default method not implement toString()?

  54. 54.What are sealed classes and interfaces, and why are they useful?hard

    A sealed class or interface (final in Java 17) restricts which classes may extend or implement it, through a permits clause. It sits between final, which allows no subclasses, and fully open inheritance.

    Rules:

    • Every permitted subclass must be declared final, sealed or non-sealed. Records are implicitly final, so they qualify automatically; non-sealed reopens a branch, as Rectangle does in the snippet.
    • Permitted subclasses must be in the same module, or in the same package if you don't use modules.
    • If they're all in the same source file, you can omit permits and the compiler infers it.

    The big payoff comes with pattern matching: because the compiler knows every possible subtype, a switch over a sealed type can be exhaustive without a default (Java 21). If someone later adds a Triangle, every such switch stops compiling until it's handled, instead of silently falling into a default at runtime. Sealed interfaces plus records give Java algebraic data types, a good fit for results like success-or-failure and for AST nodes.

    sealed interface Shape permits Circle, Square, Rectangle {}
    record Circle(double r) implements Shape {}
    record Square(double side) implements Shape {}
    non-sealed class Rectangle implements Shape {     // open to further subclassing
        double w, h;
    }
    
    static double area(Shape s) {
        return switch (s) {                            // exhaustive: no default needed
            case Circle c    -> Math.PI * c.r() * c.r();
            case Square sq   -> sq.side() * sq.side();
            case Rectangle r -> r.w * r.h;
        };
    }
    What interviewers listen for
    • Restricts subclasses with permits
    • Final in Java 17
    • Subclasses must be final, sealed or non-sealed
    • Enables exhaustive switches without default
    • Sealed interfaces plus records model sum types

    Likely follow-up: How is a sealed interface different from an enum?

  55. 55.How has switch evolved in modern Java? Explain switch expressions and pattern matching for switch.hard

    It happened in steps:

    • Switch expressions (Java 14): switch can produce a value, and the arrow form case A, B -> has no fall-through, so there are no forgotten breaks. A block body returns its value with yield. A switch expression must be exhaustive; over an enum, covering every constant is enough.
    • Pattern matching for instanceof (Java 16): if (obj instanceof String s) tests and binds in one step, with no cast.
    • Pattern matching for switch (Java 21): cases can be type patterns such as case Integer i, refined with guards (when i > 100), and case null handles null explicitly instead of throwing NullPointerException.
    • Record patterns (Java 21): case Point(int x, int y) deconstructs a record in place, and patterns can nest.

    The compiler also checks dominance: the guarded case Integer i when ... must come before the plain case Integer i, or the code won't compile. Combined with sealed types, this gives exhaustive, type-safe branching that replaces instanceof chains and many visitor-pattern hierarchies.

    record Point(int x, int y) {}
    
    static String describe(Object obj) {
        return switch (obj) {
            case null                   -> "null";
            case Integer i when i > 100 -> "big int " + i;
            case Integer i              -> "int " + i;
            case String s               -> "string of length " + s.length();
            case Point(int x, int y)    -> "point at " + x + "," + y;   // record pattern
            default                     -> "something else";
        };
    }
    What interviewers listen for
    • Switch expressions return values; arrows never fall through
    • yield returns a value from a block
    • Type patterns and when guards in Java 21
    • Record patterns deconstruct records
    • Dominance and exhaustiveness are checked at compile time

    Likely follow-up: What happens if the selector is null and there is no case null?

  56. 56.What is var in Java, and where can and can't you use it?easy

    var (Java 10) is local variable type inference: the compiler infers the variable's static type from its initializer. var list = new ArrayList<String>(); declares an ArrayList<String> exactly as if you'd written the type out. Java stays statically typed; nothing becomes dynamic at runtime.

    Where it's allowed:

    • local variables with an initializer
    • for loop indexes and enhanced for variables
    • try-with-resources declarations
    • lambda parameters, since Java 11, mainly so they can be annotated

    Where it isn't: fields, method parameters, return types, variables without an initializer, var x = null;, and array initializers like var a = {1, 2};. A lambda or method reference alone can't initialize a var either, because there's no target type.

    var isn't a full keyword, so old code using it as a variable or method name still compiles. I use it when the type is obvious from the right-hand side, and avoid it when it hides useful information, as in var result = service.process();.

    What interviewers listen for
    • Local variable type inference, added in Java 10
    • Still statically typed at compile time
    • Only locals with initializers, loops, try-with-resources
    • Not for fields, parameters or return types
    • Use it when the type is obvious

    Likely follow-up: What type does var list = new ArrayList<>(); infer?

  57. 57.What are the key changes in Java 11, 17 and 21, the LTS releases after Java 8?mid

    Each LTS release bundles everything since the previous one:

    • Java 11 (2018): var (from Java 10), the standard java.net.http.HttpClient, new String methods (isBlank, strip, lines, repeat), Files.readString and writeString, launching a single source file directly with java Main.java, and removal of the Java EE and CORBA modules from the JDK.
    • Java 17 (2021): switch expressions, text blocks, records, pattern matching for instanceof, sealed classes, helpful NullPointerException messages that name the null variable, and strong encapsulation of JDK internals.
    • Java 21 (2023): virtual threads, pattern matching for switch, record patterns, sequenced collections, and generational ZGC.

    Java 25 (September 2025) is the latest LTS. Frameworks pushed upgrades too: Spring Boot 3 requires Java 17, which is why many teams jumped from 8 or 11 straight to 17 or 21. When migrating, the usual pain points are removed Java EE modules like JAXB and code that relied on JDK internals.

    What interviewers listen for
    • 11: HttpClient, new String methods, Java EE modules removed
    • 17: records, sealed classes, text blocks, switch expressions
    • 21: virtual threads, switch patterns, sequenced collections
    • Each LTS bundles the releases before it
    • Spring Boot 3 requires Java 17

    Likely follow-up: What would you check when migrating a service from Java 8 to 21?

  58. 58.How do the G1 and ZGC garbage collectors work at a high level, and how would you choose between them?hard

    G1 (Garbage-First), the default collector since Java 9, splits the heap into many equal-sized regions, each serving as Eden, survivor, old, or "humongous" space for very large objects. It marks live objects concurrently, then collects the regions with the most garbage first, hence the name. You give it a pause-time goal with -XX:MaxGCPauseMillis (200 ms by default), and it sizes each collection to try to meet that goal. Copying survivors out of regions, called evacuation, happens in stop-the-world pauses.

    ZGC (-XX:+UseZGC, production-ready since Java 15) does almost all of its work concurrently, including relocating objects, using coloured pointers and load barriers so application threads keep running while objects move. Pauses are typically under a millisecond and don't grow with heap size, even for multi-terabyte heaps. Java 21 added a generational mode, which became the only mode in Java 24.

    Choosing: G1 is a well-balanced default for most services. ZGC suits latency-sensitive systems and very large heaps, at the cost of some throughput and extra CPU. Parallel GC is still the pick for pure batch throughput.

    What interviewers listen for
    • G1: regions, collects the most-garbage regions first
    • G1 targets a pause goal, 200 ms by default
    • ZGC relocates objects concurrently
    • ZGC pauses are tiny and independent of heap size
    • G1 for balance, ZGC for latency, Parallel for throughput

    Likely follow-up: What is a humongous object in G1? · What would you look for first in GC logs?

Prefer multiple choice? All 20 Java MCQs with answers →

esc