The Collection Interface
Objective
Understand the Collection interface: the generic root upon which the Collections Framework is built. Any class that defines a collection must implement it (directly or through a subinterface like List or Set), and because Collection extends Iterable, every collection can be cycled through with a for-each loop.
Use Cases
- Writing a method that accepts any kind of group of objects —
ArrayList,HashSet,ArrayDeque, ... — by declaring the parameter asCollection<E>instead of a specific implementation. - Adding or removing elements one at a time (
add,remove) or against another collection in bulk (addAll,removeAll,retainAll). - Checking membership or overlap between two collections without iterating manually (
contains,containsAll). - Converting a collection to an array to interoperate with array-based APIs (
toArray). - Processing elements as a
Stream(stream,parallelStream) instead of writing an explicit loop. - Recognizing, before calling a mutator, that a collection produced by a factory like
List.of()is unmodifiable.
Deep Dive
Collection extends Iterable
javainterface Collection<E>Here, E specifies the type of objects the collection will hold. Collection extends Iterable, so only classes that implement Collection (directly or transitively) can be cycled through by a for-each loop, and any class implementing it is forced to supply an iterator().
Adding elements: add and addAll
javaCollection<String> names = new ArrayList<>();
names.add("Ann"); // boolean add(E obj)
Collection<String> more = new ArrayList<>(List.of("Bob", "Cid"));
names.addAll(more); // boolean addAll(Collection<? extends E> c)add() returns false if the collection doesn't allow duplicates and the object is already a member (e.g., a Set); otherwise it returns true. addAll() adds every element of c to the invoking collection and returns true if the invoking collection changed as a result.
Removing elements: remove, removeAll, retainAll, removeIf, clear
javanames.remove("Ann"); // remove one specific object
names.removeAll(List.of("Bob")); // remove every element also in c
names.retainAll(List.of("Cid")); // keep only elements also in c
names.removeIf(n -> n.length() > 3); // default method; remove those matching a Predicate
names.clear(); // empty the collection completelyremoveAll() computes a set difference (invoking collection minus c); retainAll() computes an intersection (only what's shared with c). Both return true if the invoking collection changed.
Querying: contains, containsAll, isEmpty, size, equals
javanames.contains("Cid"); // true if Cid is a member
names.containsAll(List.of("Cid", "Ann")); // true if all of these are members
names.isEmpty();
names.size();Two collections can be compared for equality with equals(), but the precise meaning of "equal" depends on the implementing subinterface — List cares about element order, Set doesn't.
Iterating and streaming
javaIterator<String> it = names.iterator(); // manual traversal
Stream<String> s = names.stream(); // default method
Stream<String> ps = names.parallelStream(); // default method, may run in parallel
Spliterator<String> sp = names.spliterator(); // default methodstream() returns a stream that uses the invoking collection as its source; parallelStream() returns one that, if possible, splits its source across parallel operations.
Converting to an array: toArray()
toArray() has three forms:
javaObject[] a1 = names.toArray(); // Object[] toArray()
String[] a2 = names.toArray(new String[0]); // <T> T[] toArray(T[] array)
String[] a3 = names.toArray(String[]::new); // default <T> T[] toArray(IntFunction<T[]> gen), JDK 11+The first form always returns Object[]. The second returns an array typed to match the array passed in — but that type parameter T is independent of E, so the compiler accepts an array of the wrong element type, and the mismatch only surfaces at runtime:
javaCollection<String> names = List.of("Ann", "Bob");
Integer[] wrongType = names.toArray(new Integer[0]); // compiles; ArrayStoreException at runtimeUnmodifiable collections
Factory methods like List.of() return a fixed collection whose contents cannot be changed. Calling any mutator on one throws UnsupportedOperationException rather than silently doing nothing:
javaCollection<String> fixed = List.of("Ann", "Bob");
fixed.add("Cid"); // UnsupportedOperationExceptionTrade-offs
removeAllvs.retainAllread almost identically but do opposite things —removeAll(c)keeps only elements not found inc(difference), whileretainAll(c)keeps only elements also found inc(intersection). Reaching for the wrong one by habit swaps the result to its complement instead of raising an error.Optional methods fail at runtime, not compile time —
add,remove, and the other mutators are declared byCollection, but an implementation is free to reject them. Calling one on an unmodifiable collection compiles fine and throws only when executed:javaCollection<String> fixed = List.of("a", "b"); fixed.add("c"); // UnsupportedOperationExceptionObject-typed queries trade static safety for ClassCastException —
contains,remove, and similar methods acceptObject, notE, so passing a value of an incompatible type compiles without complaint and only fails when the collection actually tries to compare it:javaCollection<String> set = new TreeSet<>(List.of("a", "b")); set.contains(42); // ClassCastException: Integer cannot be compared to StringtoArray(T[])compiles for any component type, correct or not — because its type parameter isn't tied to the collection's element typeE, an array of an incompatible type is accepted at compile time and only fails when elements are actually copied into it:javaCollection<String> names = List.of("Ann", "Bob"); Integer[] wrongType = names.toArray(new Integer[0]); // ArrayStoreException