Generics let classes, interfaces, and methods operate on parameterized types, providing compile-time type safety and eliminating most casts. List<String> names = new ArrayList<>() lets the compiler reject a stray add(42) at compile time instead of producing a ClassCastException at run time. The diamond operator <> lets the compiler infer type arguments from the left-hand side, avoiding the redundancy of new ArrayList<String>() on the right, and since Java 9 the diamond is also permitted when creating anonymous inner classes. Type erasure is the process by which the Java compiler removes generic type information at compile time, replacing type parameters with their bounds or Object; List<String> and List<Integer> therefore reduce to the same List class at run time, ensuring backward compatibility with pre-generics code. Heap pollution occurs when a non-reifiable varargs array (e.g. T...) is stored, returned, or iterated unsafely, mixing the array's runtime component (Object[]) with the compile-time type; @SafeVarargs (Java 7+) asserts that a method's varargs array will not be exposed to heap pollution, applied to static, final, or (since Java 9) private methods, while @SuppressWarnings("unchecked") silences a specific compiler warning at a particular site.
Variance is expressed through bounded wildcards. PECS—the Producer Extends, Consumer Super rule—summarizes the discipline: when a method only reads from a parameter that is a producer, use ? extends T; when a method only writes into a parameter that is a consumer, use ? super T. The standard copy(List<? extends T> src, List<? super T> dest) pattern reads from src and writes to dest while keeping both signatures flexible. Upper-bounded type parameters like <T extends Number> restrict which types can stand in for T to Number or its subclasses; the unbounded wildcard <?> simply accepts any type. Raw types—using a generic class without its type arguments, like List instead of List<String>—disable the compiler's checks and are kept only for backward compatibility; always use parameterized types instead. The Collections Framework provides a unified architecture for storing and manipulating groups of objects: List supports ordered, indexable sequences that allow duplicates (ArrayList, LinkedList); Set forbids duplicates (HashSet, TreeSet); Map associates keys with values (HashMap, TreeMap, LinkedHashMap); and Queue orders elements in FIFO (PriorityQueue, LinkedList as a queue).
ArrayList is backed by a dynamic array—\(O(1)\) random access but \(O(n)\) insertions/deletions in the middle—while LinkedList is a doubly-linked list with \(O(1)\) head/tail mutations and \(O(n)\) random access; LinkedList also implements Deque. HashMap and HashSet back themselves with a hash table offering \(O(1)\) average lookups and no ordering; TreeMap and TreeSet use red-black trees giving \(O(\log n)\) operations and sorted traversal by natural order or custom Comparator. HashMap's internals illustrate how the framework trades simplicity for performance: an array of buckets, a key's hashCode() determines its bucket index, collisions are resolved with linked lists (and, since Java 8, with balanced trees when a bucket exceeds eight entries), and the load factor of 0.75 controls when the table resizes. LinkedHashMap augments a hash table with a doubly-linked list to preserve insertion order; in access-order mode it powers LRU caches via the removeEldestEntry override. ConcurrentHashMap replaces the global locks of legacy Hashtable with fine-grained bucket locks (and CAS for reads since Java 8) while providing weakly consistent iterators that never throw ConcurrentModificationException. EnumSet and EnumMap deserve special mention: they back themselves with single-word bitmasks and ordinal-indexed arrays respectively, giving them enormous speed and memory advantages whenever the keys are enum constants.
The contract between equals() and hashCode() is the cornerstone of correct collection behavior. The contract demands reflexivity, symmetry, transitivity, consistency, and that x.equals(null) is false; critically, if x.equals(y) returns true, then x.hashCode() must equal y.hashCode()—violating this rule causes entries to be lost in HashMap, HashSet, and Hashtable. For ordering, Comparable<T> defines natural order via compareTo(T o), while Comparator<T> defines custom order external to the class, supports multiple sorting strategies with thenComparing, and is the right choice when you cannot modify the class. For sorted collections, Comparable.compareTo() recommends (does not require) consistency with equals: a TreeSet will not deduplicate objects that compareTo to zero but differ by equals, while a HashSet would—precisely the famous "BigDecimal with different scale" gotcha. The Iterator interface provides hasNext(), next(), and remove(), and is the only safe way to mutate a collection while iterating; iterators are either fail-fast (throw ConcurrentModificationException on structural modification, as ArrayList and HashMap) or weakly consistent (work on a snapshot, as ConcurrentHashMap and CopyOnWriteArrayList). Two idioms dominate comparison choices in collections: List.sort(Comparator) mutates a list in place and is usually faster, while stream.sorted() fits inside larger pipelines and never modifies the source. The Map.compute family (computeIfAbsent, computeIfPresent, merge) handles atomic conditional updates, and modern conveniences such as removeIf (mutates in place, no allocation), Predicate.not(...), Map.getOrDefault, and ConcurrentHashMap's atomic operations let you avoid the classic get-then-check null-then-put pattern. Helper factories likewise have subtle differences: subList returns a view onto the original list (mutations are shared and structural changes outside the view are undefined), new ArrayList(collection) creates an independent copy, Arrays.asList(array) returns a fixed-size view backed by the original array (set() works, add()/remove() throw), and List.of(...) returns a truly immutable list (no nulls, may intern duplicates).