Ch. 07

Java

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

20 interview questions0 quiz questions0 notes
your progress0%

Top 20 Java interview questions most asked first

  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?

esc