☕ Java
Collection Interface
Collection<E> is the root interface of the main collection hierarchy, extending Iterable<E>. It defines the common operations that all collection types must support: adding elements, removing elements, checking containment, querying size, clearing, converting to an array, and bulk operations. List, Set, and Queue all extend Collection. Map does not extend Collection because a map operates on key-value pairs rather than individual elements.
Collection Interface Methods
The Collection interface defines fifteen methods that every collection must implement. These methods form the universal language for working with any collection regardless of its concrete type — if you know Collection's methods, you can work with ArrayList, HashSet, LinkedList, or any custom collection without learning a new API.
The modification methods are add(E e), remove(Object o), addAll(Collection<? extends E> c), removeAll(Collection<?> c), retainAll(Collection<?> c), and clear(). The query methods are size(), isEmpty(), contains(Object o), and containsAll(Collection<?> c). The conversion methods are toArray() and the typed toArray(T[] a). The iteration methods are iterator() (inherited from Iterable), forEach(), and stream().
The parameter type of remove() and contains() is Object rather than E — the generic type parameter. This is intentional and reflects a design decision made for backward compatibility. It means you can ask if a List<String> contains an Integer without a compile error — the answer will always be false (unless equals() is overridden in a bizarre way), but the call itself is valid. This is considered a type system imperfection but it is permanent in Java because changing it would break countless existing programs.
All mutating methods are optional operations. A collection implementation can choose to throw UnsupportedOperationException for any mutating method. This is used by unmodifiable collections returned by Collections.unmodifiableList() and List.of(). The optional operation contract means you should always be prepared for mutating operations to throw when working with collections from unknown sources.
Java
// ── Collection<E> method reference: ──────────────────────────────────
//
// int size()
// boolean isEmpty()
// boolean contains(Object o)
// boolean containsAll(Collection<?> c)
// boolean add(E e)
// boolean addAll(Collection<? extends E> c)
// boolean remove(Object o)
// boolean removeAll(Collection<?> c)
// boolean retainAll(Collection<?> c)
// void clear()
// Object[] toArray()
// <T> T[] toArray(T[] a)
// Iterator<E> iterator() (from Iterable)
// default void forEach(Consumer<? super E> action) (from Iterable)
// default Stream<E> stream()
// default Stream<E> parallelStream()
// ── Working with the Collection interface: ────────────────────────────
Collection<String> c = new ArrayList<>(
Arrays.asList("Alice", "Bob", "Charlie", "Dave"));
// Query methods:
System.out.println("Size: " + c.size()); // 4
System.out.println("Empty: " + c.isEmpty()); // false
System.out.println("Has Alice: " + c.contains("Alice")); // true
System.out.println("Has Eve: " + c.contains("Eve")); // false
// Modification:
c.add("Eve");
System.out.println("After add: " + c.size()); // 5
c.remove("Bob");
System.out.println("After remove: " + c); // [Alice, Charlie, Dave, Eve]
// Bulk operations:
Collection<String> toAdd = List.of("Frank", "Grace");
c.addAll(toAdd);
System.out.println("After addAll: " + c.size()); // 7
Collection<String> toRemove = List.of("Frank", "Grace", "NotInList");
c.removeAll(toRemove);
System.out.println("After removeAll: " + c); // [Alice, Charlie, Dave, Eve]
// retainAll — keep only elements in the given collection:
c.retainAll(List.of("Alice", "Charlie", "Unknown"));
System.out.println("After retainAll: " + c); // [Alice, Charlie]
// containsAll:
System.out.println(c.containsAll(List.of("Alice", "Charlie"))); // true
System.out.println(c.containsAll(List.of("Alice", "Dave"))); // false
// clear:
c.clear();
System.out.println("After clear: " + c.isEmpty()); // trueThe add() and remove() Return Values
Both add() and remove() return boolean, and the meaning of these return values is important but often overlooked. For add(), the return value indicates whether the collection was changed by the operation. For a List, add() always returns true because lists allow duplicates and always accept the element. For a Set, add() returns false if the element was already present — the set did not change because it refuses duplicates. This return value is the mechanism by which you can tell whether a Set addition was a genuinely new element.
For remove(), the return value indicates whether the collection contained the element and was modified. If the element was not present, remove() returns false and the collection is unchanged. If the element was present and removed, remove() returns true. This is more nuanced for List, which may contain duplicates: remove(Object o) removes only the first occurrence and returns true; subsequent calls to remove the same value will remove further occurrences one at a time.
The removeIf() default method (Java 8+) is a powerful evolution of remove(). It accepts a Predicate and removes all elements for which the predicate returns true, returning true if any elements were removed. This replaces the common pattern of using an iterator to remove elements that match a condition with a single expressive method call.
The addAll() and removeAll() return values similarly indicate whether the collection changed. These are useful for detecting whether any elements were actually added or removed when working with sets — if addAll() returns false for a set, all the elements were already present.
Java
// ── add() return value — meaningful for Set: ─────────────────────────
Set<String> set = new HashSet<>();
System.out.println(set.add("Alice")); // true — added successfully
System.out.println(set.add("Bob")); // true — added successfully
System.out.println(set.add("Alice")); // false — already present, not added
List<String> list = new ArrayList<>();
System.out.println(list.add("Alice")); // true — always true for List
System.out.println(list.add("Alice")); // true — duplicates allowed
// ── remove() return value: ────────────────────────────────────────────
List<String> items = new ArrayList<>(Arrays.asList("a", "b", "a", "c"));
System.out.println(items.remove("a")); // true — first 'a' removed
System.out.println(items); // [b, a, c]
System.out.println(items.remove("z")); // false — 'z' was not present
// Remove by index (List-specific) vs remove by value:
List<String> letters = new ArrayList<>(Arrays.asList("x", "y", "z"));
letters.remove(1); // remove at index 1 — removes "y"
System.out.println(letters); // [x, z]
letters.remove("x"); // remove by value — removes "x"
System.out.println(letters); // [z]
// ── removeIf() — functional predicate removal: ────────────────────────
List<Integer> numbers = new ArrayList<>(
Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
numbers.removeIf(n -> n % 2 == 0); // remove all even numbers
System.out.println(numbers); // [1, 3, 5, 7, 9]
List<String> names = new ArrayList<>(
Arrays.asList("Alice", "Bob", "Charlie", "Dave", "Eve"));
boolean removed = names.removeIf(name -> name.length() > 4);
System.out.println("Removed: " + removed); // true
System.out.println(names); // [Bob, Dave, Eve]
// ── addAll() return — check if set changed: ──────────────────────────
Set<Integer> existing = new HashSet<>(Set.of(1, 2, 3));
System.out.println(existing.addAll(Set.of(1, 2))); // false — no new elements
System.out.println(existing.addAll(Set.of(3, 4, 5))); // true — 4 and 5 addedConverting Collections to Arrays and Streams
Collection provides two array conversion methods and two stream creation methods. These bridges between the collections world, the array world, and the stream world are essential for interfacing with APIs that expect different types.
toArray() returns Object[] — an untyped array. This is almost never what you want because you lose the type information. toArray(T[] a) is the correct form — it returns a typed array. The argument serves two purposes: it provides the type token for the returned array, and if it is large enough, the collection's elements are placed into it and it is returned (saving an allocation). If the array is too small, a new array of the right type and size is returned.
The modern idiom since Java 11 is toArray(IntFunction<T[]> generator): list.toArray(String[]::new). This is cleaner than the old idiom of passing a zero-length array: list.toArray(new String[0]). Both are correct; the method reference form is more concise and equally efficient.
stream() returns a sequential Stream over the collection's elements. parallelStream() returns a potentially parallel Stream. Streams provide the full power of the Stream API — filter, map, reduce, collect, sort, limit, distinct, and many more operations — applied to the collection's elements. Streams do not modify the underlying collection; they produce new streams or terminal results. Streams are lazy — intermediate operations are not executed until a terminal operation is invoked.
Java
// ── toArray() — two forms: ────────────────────────────────────────────
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// Old form (pre-Java 11): toArray(T[] a)
String[] arr1 = names.toArray(new String[0]); // zero-length type hint
String[] arr2 = names.toArray(new String[names.size()]); // pre-sized (no allocation)
System.out.println(Arrays.toString(arr1)); // [Alice, Bob, Charlie]
// Modern form (Java 11+): toArray(IntFunction<T[]>)
String[] arr3 = names.toArray(String[]::new); // method reference
System.out.println(Arrays.toString(arr3)); // [Alice, Bob, Charlie]
// Avoid toArray() without type argument:
Object[] wrong = names.toArray(); // Object[] — loses type information
// String[] cast = (String[]) names.toArray(); // ClassCastException at runtime!
// ── stream() — bridge to Stream API: ─────────────────────────────────
Collection<String> words = List.of("hello", "world", "java",
"programming", "fun");
// Filter and collect:
List<String> longWords = words.stream()
.filter(w -> w.length() > 4)
.collect(Collectors.toList());
System.out.println(longWords); // [hello, world, java, programming]
// Map and join:
String upper = words.stream()
.map(String::toUpperCase)
.collect(Collectors.joining(", "));
System.out.println(upper); // HELLO, WORLD, JAVA, PROGRAMMING, FUN
// Count with condition:
long count = words.stream()
.filter(w -> w.contains("a"))
.count();
System.out.println("Words with 'a': " + count); // 3 (java, programming, fun... actually: java, programming = 2... let's count: java ✓, programming ✓, fun → no 'a'. so 2. But let's re-check the list: hello no, world no, java yes, programming yes, fun no → count = 2)
// Sum lengths:
int totalLength = words.stream()
.mapToInt(String::length)
.sum();
System.out.println("Total length: " + totalLength);
// ── parallelStream() — parallel processing: ──────────────────────────
List<Integer> numbers = new ArrayList<>();
for (int i = 1; i <= 1_000_000; i++) numbers.add(i);
long sum = numbers.parallelStream()
.mapToLong(Integer::longValue)
.sum();
System.out.println("Sum: " + sum); // 500000500000
// ── Collection to Set (deduplication): ───────────────────────────────
List<Integer> withDupes = Arrays.asList(1, 2, 2, 3, 3, 3, 4);
Set<Integer> unique = new HashSet<>(withDupes);
System.out.println(unique); // [1, 2, 3, 4]
// Using stream:
List<Integer> distinctList = withDupes.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(distinctList); // [1, 2, 3, 4]Bulk Operations and Set Algebra
The bulk operations addAll(), removeAll(), retainAll(), and containsAll() implement set algebra operations when applied to Set collections. addAll() computes the union. removeAll() computes the set difference. retainAll() computes the intersection. containsAll() tests subset membership. These operations are not exclusive to sets — they work on any Collection — but their mathematical set properties only hold precisely for Set (which has no duplicates).
These bulk operations modify the receiver collection and return a boolean indicating whether the collection changed. They are particularly useful for set operations in algorithms, for filtering based on membership in another collection, and for maintaining collections of active/inactive items.
A critical warning about bulk operations: the methods accept Collection<?> (wildcard type) for the argument. This means removeAll() will accept any collection regardless of its type parameter. If you accidentally call removeAll() with a collection of the wrong type, it will silently do nothing (because contains() will return false for all elements of the wrong type). This is a common source of bugs — always ensure the element types are compatible.
Java 8 added removeIf() to Collection as a more flexible alternative to removeAll() for predicate-based removal. Java 9 added the immutable collection factories List.of(), Set.of(), and Map.of() which complement the bulk operations by providing efficient immutable collections to pass to containsAll() and similar methods.
Java
// ── Set algebra with Set collections: ────────────────────────────────
Set<String> setA = new HashSet<>(Set.of("a", "b", "c", "d"));
Set<String> setB = new HashSet<>(Set.of("c", "d", "e", "f"));
// Union — A ∪ B: all elements from both sets
Set<String> union = new HashSet<>(setA);
union.addAll(setB);
System.out.println("Union: " + new TreeSet<>(union)); // [a, b, c, d, e, f]
// Intersection — A ∩ B: only elements in both sets
Set<String> intersection = new HashSet<>(setA);
intersection.retainAll(setB);
System.out.println("Intersection: " + intersection); // [c, d]
// Difference — A B: elements in A but not in B
Set<String> difference = new HashSet<>(setA);
difference.removeAll(setB);
System.out.println("Difference A-B: " + difference); // [a, b]
// Subset check — is {c, d} a subset of A?
System.out.println("Is subset: " +
setA.containsAll(Set.of("c", "d"))); // true
System.out.println("Is subset: " +
setA.containsAll(Set.of("c", "e"))); // false — 'e' not in A
// ── Bulk operations on List — duplicates respected: ───────────────────
List<Integer> listA = new ArrayList<>(Arrays.asList(1, 2, 2, 3, 4, 4));
List<Integer> listB = new ArrayList<>(Arrays.asList(2, 4));
listA.removeAll(listB); // removes ALL occurrences of 2 and 4
System.out.println(listA); // [1, 3]
// ── retainAll as a filter: ────────────────────────────────────────────
List<String> employees = new ArrayList<>(
Arrays.asList("Alice", "Bob", "Charlie", "Dave", "Eve"));
Set<String> activeEmployees = Set.of("Alice", "Charlie", "Eve");
employees.retainAll(activeEmployees); // keep only active employees
System.out.println(employees); // [Alice, Charlie, Eve]
// ── containsAll for validation: ───────────────────────────────────────
Set<String> required = Set.of("READ", "WRITE", "EXECUTE");
Set<String> granted = Set.of("READ", "WRITE", "EXECUTE", "DELETE");
boolean hasAllRequired = granted.containsAll(required);
System.out.println("Has all required permissions: " + hasAllRequired); // trueRelated Topics in Collections Framework
Collections Overview
The Java Collections Framework (JCF) is a unified architecture for representing and manipulating groups of objects. It provides a set of interfaces that define the operations a collection must support, a set of abstract classes that provide partial implementations, and a set of concrete implementations optimised for different use cases. Every Java developer uses collections daily — lists for sequences, sets for uniqueness, maps for key-value pairs, and queues for ordering — and choosing the right implementation for the right use case is one of the most fundamental practical skills in Java.
Iterable
Iterable<E> is the root interface of the Java Collections hierarchy. Any class that implements Iterable can be used in a for-each loop. It declares a single abstract method: iterator(), which returns an Iterator<E> that the for-each loop uses to traverse the elements. Implementing Iterable is all that is required to make a custom data structure work with Java's enhanced for loop, the Stream API, and any method that accepts an Iterable.