Java Collection Interface
Java Collection Interface
java.util.Collection<E> is the root interface for all single-element data structures in the Java Collections Framework. Every ArrayList, HashSet, LinkedList, TreeSet, and ArrayDeque you have ever used implements this interface. Understanding it tells you exactly which operations are guaranteed on any collection object, regardless of what concrete class is behind it.
Most developers know ArrayList and HashMap well before they understand Collection. That gap causes real problems — methods declared with ArrayList as the parameter type instead of List or Collection, code that cannot be swapped from one collection to another without a rewrite, and interview answers that reveal shallow knowledge. This article closes that gap.
What Is the Collection Interface?
Collection<E> is an interface in java.util that extends java.lang.Iterable<E>. It defines the minimum set of operations that every group-of-elements container must support. It does not cover Map — key-value storage has its own separate Map<K,V> interface hierarchy.
The diagram below shows where Collection sits in the hierarchy and what extends from it.
java.lang.Iterable<E> ← supports for-each loop
└── java.util.Collection<E> ← ROOT of all single-element collections
│
├── java.util.List<E>
│ ├── ArrayList (resizable array)
│ ├── LinkedList (doubly-linked nodes)
│ └── Vector (legacy, synchronized)
│
├── java.util.Set<E>
│ ├── HashSet (hash table, no order)
│ ├── LinkedHashSet (hash table + insertion order)
│ └── TreeSet (Red-Black tree, sorted)
│
└── java.util.Queue<E>
├── PriorityQueue (min-heap, priority order)
└── Deque<E>
└── ArrayDeque (resizable circular array)
java.util.Map<K,V> ← SEPARATE branch — does NOT extend Collection
Collection inherits iterator() from Iterable and adds the group-management methods. Every List, Set, and Queue automatically inherits all of these through the hierarchy.
Every Method Defined by Collection
MODIFICATION METHODS (optional — may throw UnsupportedOperationException): boolean add(E e) boolean addAll(Collection<? extends E> c) boolean remove(Object o) boolean removeAll(Collection<?> c) boolean retainAll(Collection<?> c) void clear() QUERY METHODS (always supported): int size() boolean isEmpty() boolean contains(Object o) boolean containsAll(Collection<?> c) CONVERSION METHODS: Object[] toArray() <T> T[] toArray(T[] a) Iterator<E> iterator() ← inherited from Iterable JAVA 8+ DEFAULT METHODS (always available, can be overridden): Stream<E> stream() Stream<E> parallelStream() void forEach(Consumer<? super E> action) boolean removeIf(Predicate<? super E> filter) Spliterator<E> spliterator()
Modification methods are marked optional because some implementations — List.of(), Collections.unmodifiableList() — legitimately do not support them. Calling add() on an unmodifiable collection throws UnsupportedOperationException, not a compile error. This design choice lets read-only views implement the same interface as mutable collections.
When to Use Collection as a Type
The single most important practical decision around Collection is knowing when to use it as a variable or parameter type versus using List, Set, or the concrete class.
WIDEST USEFUL TYPE — what to declare on the LEFT side Iterable<T> use when: method only needs for-each iteration Collection<T> use when: method needs add/remove/contains/size + iteration List<T> use when: callers need get(index), indexOf, or subList Set<T> use when: the uniqueness contract must be visible to callers Queue<T> use when: callers use offer/poll/peek FIFO semantics Map<K,V> use when: key-value access is the contract NEVER declare: ArrayList<T> as a variable or parameter type (couples to implementation) HashSet<T> as a variable or parameter type HashMap<K,V> as a variable or parameter type unless you need methods specific to that class (e.g., ArrayDeque.push())
Declaring parameters as Collection<T> lets the caller pass an ArrayList, HashSet, TreeSet, LinkedList, or any future custom class that implements Collection. Declaring the parameter as ArrayList<T> locks the caller out of every other choice for no benefit.
Internal Working
Collection<E> is a pure interface — it has no fields, no state, and no internal structure. Its internal working is entirely in the concrete classes that implement it. What Collection defines is the contract: the method signatures and their behavioural guarantees.
The contract rules that every implementing class must follow:
EQUALS AND HASHCODE CONTRACT WITH COLLECTIONS
Collection does not override equals() or hashCode() itself.
Each sub-interface does:
List<E>: equals() compares element-by-element, in order
[A,B,C].equals([A,B,C]) → true
[A,B,C].equals([A,C,B]) → false (order matters)
Set<E>: equals() compares regardless of order, just membership
{A,B,C}.equals({C,B,A}) → true
{A,B}.equals({A,B,C}) → false (different size)
Queue<E>: equals() is typically identity-based (Object.equals)
most Queue implementations do not override it
MODIFICATION CONTRACT
add(e) returns:
true → the collection was changed by this call
false → the collection already contained e (Set case)
or the element was rejected for another reason
remove(o) returns:
true → the element was found and removed
false → the element was not present
These return values exist so callers can detect whether the
collection actually changed — important for Set operations.
The diagram below shows how a method that accepts Collection<T> works identically with any implementing class.
void printAll(Collection<String> items):
|
items.iterator() called
|
┌──────────────┼──────────────┐
│ │ │
ArrayList HashSet TreeSet
iterator iterator iterator
(index-based) (bucket scan) (in-order tree)
│ │ │
all produce Iterator<String> with hasNext/next
│ │ │
same loop body runs for all three
Core Operations with Examples
add, addAll, and the Return Value
add() returns true if the collection changed and false if it did not. For List, this always returns true (duplicates accepted). For Set, it returns false when the element already exists. Testing this return value is how you detect duplicates in a Set without calling contains() first.
// File: CollectionAddDemo.java
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class CollectionAddDemo {
public static void main(String[] args) {
// List — add() always returns true; duplicates accepted
Collection<String> list = new ArrayList<>();
System.out.println("=== add() on ArrayList ===");
System.out.println("add(Java) : " + list.add("Java")); // true
System.out.println("add(Spring) : " + list.add("Spring")); // true
System.out.println("add(Java) : " + list.add("Java")); // true (duplicate OK)
System.out.println("Contents : " + list);
System.out.println("Size : " + list.size());
// Set — add() returns false for duplicates
Collection<String> set = new HashSet<>();
System.out.println("\n=== add() on HashSet ===");
System.out.println("add(Java) : " + set.add("Java")); // true
System.out.println("add(Spring) : " + set.add("Spring")); // true
System.out.println("add(Java) : " + set.add("Java")); // false — duplicate
System.out.println("Contents : " + set);
System.out.println("Size : " + set.size());
// addAll — adds a whole collection at once
System.out.println("\n=== addAll ===");
Collection<String> base = new ArrayList<>();
base.addAll(List.of("Kotlin", "Go", "Rust"));
System.out.println("After addAll: " + base);
System.out.println("addAll returns changed?: " + base.addAll(List.of("C++", "Go")));
System.out.println("After second addAll: " + base); // duplicates in List are allowed
}
}Output:
=== add() on ArrayList ===
add(Java) : true
add(Spring) : true
add(Java) : true
Contents : [Java, Spring, Java]
Size : 3
=== add() on HashSet ===
add(Java) : true
add(Spring) : true
add(Java) : false — duplicate
Contents : [Spring, Java]
Size : 2
=== addAll ===
After addAll: [Kotlin, Go, Rust]
addAll returns changed?: true
After second addAll: [Kotlin, Go, Rust, C++, Go]
contains, containsAll, and remove
contains(Object o) checks membership using equals(). remove(Object o) removes the first element where equals() returns true and returns whether the collection changed.
// File: CollectionQueryDemo.java
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class CollectionQueryDemo {
public static void main(String[] args) {
Collection<String> skills = new ArrayList<>(
List.of("Java", "Spring", "MySQL", "Redis", "Docker", "Kafka")
);
// contains — uses equals() for comparison
System.out.println("=== contains and containsAll ===");
System.out.println("contains(Java) : " + skills.contains("Java"));
System.out.println("contains(Python) : " + skills.contains("Python"));
Collection<String> javaStack = List.of("Java", "Spring", "MySQL");
Collection<String> pythonStack = List.of("Python", "Django");
System.out.println("containsAll(javaStack) : " + skills.containsAll(javaStack));
System.out.println("containsAll(pythonStack): " + skills.containsAll(pythonStack));
// remove — removes first occurrence, returns whether collection changed
System.out.println("\n=== remove ===");
System.out.println("Before: " + skills);
System.out.println("remove(Redis) : " + skills.remove("Redis")); // true
System.out.println("remove(MongoDB) : " + skills.remove("MongoDB")); // false — not present
System.out.println("After : " + skills);
// removeAll — removes all elements that appear in the given collection
System.out.println("\n=== removeAll ===");
skills.removeAll(List.of("Docker", "Kafka"));
System.out.println("After removeAll(Docker, Kafka): " + skills);
// retainAll — keeps only elements present in the given collection
System.out.println("\n=== retainAll ===");
Collection<String> keepList = List.of("Java", "Spring");
skills.retainAll(keepList);
System.out.println("After retainAll(Java, Spring): " + skills);
}
}Output:
=== contains and containsAll ===
contains(Java) : true
contains(Python) : false
containsAll(javaStack) : true
containsAll(pythonStack): false
=== remove ===
Before: [Java, Spring, MySQL, Redis, Docker, Kafka]
remove(Redis) : true
remove(MongoDB) : false
After : [Java, Spring, MySQL, Docker, Kafka]
=== removeAll ===
After removeAll(Docker, Kafka): [Java, Spring, MySQL]
=== retainAll ===
After retainAll(Java, Spring): [Java, Spring]
Iteration, toArray, and Java 8 Default Methods
Collection inherits iterator() from Iterable and adds stream(), parallelStream(), forEach(), and removeIf() as Java 8 default methods.
// File: CollectionIterationDemo.java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
public class CollectionIterationDemo {
public static void main(String[] args) {
Collection<Integer> scores = new ArrayList<>(List.of(85, 92, 78, 95, 62, 88, 71));
// for-each (via Iterable — Collection inherits this)
System.out.print("for-each: ");
for (int score : scores) {
System.out.print(score + " ");
}
System.out.println();
// forEach — Java 8 default method on Collection
System.out.print("forEach: ");
scores.forEach(score -> System.out.print(score + " "));
System.out.println();
// removeIf — removes all elements matching the predicate; Java 8 default method
System.out.println("Before removeIf: " + scores);
scores.removeIf(score -> score < 80);
System.out.println("After removeIf (< 80): " + scores);
// stream — pipeline processing; Java 8 default method
System.out.println("\n=== stream operations ===");
long highScoreCount = scores.stream()
.filter(score -> score >= 90)
.count();
System.out.println("Scores >= 90: " + highScoreCount);
double average = scores.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
System.out.printf("Average score: %.2f%n", average);
// toArray — converts collection to Object array
System.out.println("\n=== toArray ===");
Object[] objectArray = scores.toArray();
System.out.println("toArray() : " + Arrays.toString(objectArray));
Integer[] typedArray = scores.toArray(new Integer[0]);
System.out.println("toArray(Integer[]) : " + Arrays.toString(typedArray));
}
}Output:
for-each: 85 92 78 95 62 88 71
forEach: 85 92 78 95 62 88 71
Before removeIf: [85, 92, 78, 95, 62, 88, 71]
After removeIf (< 80): [85, 92, 95, 88]
=== stream operations ===
Scores >= 90: 2
Average score: 90.00
=== toArray ===
toArray() : [85, 92, 95, 88]
toArray(Integer[]) : [85, 92, 95, 88]
Programming to Collection — Polymorphism in Action
The biggest practical benefit of Collection is writing methods that accept any collection type. The example below shows a utility method that works identically with ArrayList, HashSet, and TreeSet.
// File: CollectionPolymorphismDemo.java
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
public class CollectionPolymorphismDemo {
// Accepts ANY Collection — ArrayList, HashSet, TreeSet, LinkedList...
static double average(Collection<Integer> numbers) {
if (numbers.isEmpty()) return 0.0;
return numbers.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
}
static Collection<Integer> filterAbove(Collection<Integer> numbers, int threshold) {
Collection<Integer> result = new ArrayList<>();
for (int number : numbers) {
if (number > threshold) {
result.add(number);
}
}
return result;
}
public static void main(String[] args) {
List<Integer> asList = new ArrayList<>(List.of(10, 20, 30, 40, 50));
Set<Integer> asSet = new HashSet<> (List.of(10, 20, 30, 40, 50));
TreeSet<Integer> asTree = new TreeSet<> (List.of(10, 20, 30, 40, 50));
// The same method works with all three — Collection is the common contract
System.out.printf("Average from ArrayList : %.1f%n", average(asList));
System.out.printf("Average from HashSet : %.1f%n", average(asSet));
System.out.printf("Average from TreeSet : %.1f%n", average(asTree));
System.out.println();
// filterAbove works regardless of input collection type
System.out.println("Above 25 from ArrayList: " + filterAbove(asList, 25));
System.out.println("Above 25 from HashSet : " + filterAbove(asSet, 25));
System.out.println("Above 25 from TreeSet : " + filterAbove(asTree, 25));
}
}Output:
Average from ArrayList : 30.0
Average from HashSet : 30.0
Average from TreeSet : 30.0
Above 25 from ArrayList: [30, 40, 50]
Above 25 from HashSet : [30, 40, 50]
Above 25 from TreeSet : [30, 40, 50]
Basic Example
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
public class Main {
// Accepts ANY Collection — ArrayList, HashSet, or any other implementation
static void printAll(Collection<String> items) {
for (String item : items) {
System.out.println(item);
}
}
public static void main(String[] args) {
Collection<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Apple"); // List allows duplicates
System.out.println("size: " + fruits.size());
printAll(fruits);
Collection<String> uniqueFruits = new HashSet<>(fruits);
System.out.println("unique size: " + uniqueFruits.size());
}
}Output:
size: 3
Apple
Banana
Apple
unique size: 2
The printAll() method takes a parameter of type Collection<String>, so it works with any class that implements the interface — here it is called with an ArrayList. add(), size(), and the for-each loop are all Collection methods, which is why a List (which allows duplicates) and a HashSet (which does not) can both be used through the same contract even though they behave differently underneath.
Performance Considerations
Collection as an interface adds no runtime overhead — it is a compile-time contract. Every method call resolves to the concrete class's implementation via JVM dynamic dispatch (invokevirtual), which is effectively O(1) overhead per call.
What matters is which implementation is behind the Collection reference:
| Method | ArrayList | HashSet | TreeSet | LinkedList |
|---|---|---|---|---|
| add(e) | O(1) amort | O(1) avg | O(log n) | O(1) |
| remove(o) | O(n) scan | O(1) avg | O(log n) | O(n) scan |
| contains(o) | O(n) scan | O(1) avg | O(log n) | O(n) scan |
| size() | O(1) | O(1) | O(1) | O(1) |
| iterator() | O(1) | O(1) | O(1) | O(1) |
| stream() | O(1) | O(1) | O(1) | O(1) |
Key production insight: When a method accepts Collection<T> but calls contains() in a loop, the performance depends entirely on what the caller passes. If the caller passes an ArrayList, the loop is O(n²). If the caller passes a HashSet, the same loop is O(n). During code reviews, this hidden performance dependency is worth documenting — either in the method's Javadoc or by accepting Set<T> instead of Collection<T> when O(1) membership testing is required.
Best Practices
Declare parameters and return types at the widest useful level. If a method only needs to iterate, accept Iterable<T>. If it needs add and remove, accept Collection<T>. If it needs get(index), accept List<T>. Declaring everything as ArrayList<T> or HashSet<T> is the most common collection mistake in fresher code and is flagged in virtually every code review at companies that care about clean design.
Return Collection<T> when callers only need group-level access. A method that returns internal state should return the widest type that does not over-commit. Collection<T> tells callers they can iterate and query but does not guarantee ordering or index access. If the implementation changes from ArrayList to TreeSet later, callers using Collection<T> are unaffected. Callers using ArrayList<T> would need changes.
Use addAll() to combine collections of different types. list.addAll(set) and set.addAll(list) both work because both sides work with Collection. This is how Collection enables the entire collections API to compose without tight coupling between implementations.
Test add() and remove() return values when the collection may be a Set. set.add(element) returns false if the element already exists. collection.remove(element) returns false if the element was not found. Using these return values instead of calling contains() before add() is both more efficient and more readable.
Common Mistakes
Mistake 1 — Declaring Parameters as Concrete Types
// WRONG — locks callers into one implementation
public int countAbove(ArrayList<Integer> numbers, int threshold) {
return (int) numbers.stream().filter(n -> n > threshold).count();
}
// CORRECT — accepts ArrayList, LinkedList, HashSet, TreeSet, or any future type
public int countAbove(Collection<Integer> numbers, int threshold) {
return (int) numbers.stream().filter(n -> n > threshold).count();
}Mistake 2 — Calling contains() Before add() on a Set
Collection<String> tags = new HashSet<>();
// WRONG — two operations where one is sufficient
if (!tags.contains("java")) {
tags.add("java"); // two method calls — also NOT atomic in concurrent code
}
// CORRECT — add() already returns false if the element exists
boolean wasNew = tags.add("java"); // one call, one responsibilityMistake 3 — Calling size() When isEmpty() Is Sufficient
Collection<String> items = getItems();
// WRONG — calling size() just to check for zero is a minor but real code smell
if (items.size() == 0) { ... }
// CORRECT — isEmpty() is semantically precise and often optimized
if (items.isEmpty()) { ... }Mistake 4 — Ignoring UnsupportedOperationException on Unmodifiable Collections
Collection<String> readOnly = List.of("A", "B", "C");
// or: Collections.unmodifiableList(someList)
// WRONG — compiles fine, but throws UnsupportedOperationException at runtime
readOnly.add("D");
readOnly.remove("A");
// CORRECT — copy into a mutable collection before modifying
Collection<String> mutable = new ArrayList<>(readOnly);
mutable.add("D");Interview Questions
Q1. What is the Collection interface in Java and what does it define?
java.util.Collection<E> is the root interface of the Java Collections Framework for single-element containers. It extends java.lang.Iterable<E> and defines the core group-management methods: add, addAll, remove, removeAll, retainAll, clear, contains, containsAll, size, isEmpty, toArray, and iterator. Java 8 added default methods: stream, parallelStream, forEach, removeIf, and spliterator. Every List, Set, and Queue implementation inherits this contract. Map does not extend Collection.
Q2. What is the difference between Collection and Collections?
java.util.Collection (singular) is an interface — the root of the Collections hierarchy defining methods every collection must support. java.util.Collections (plural) is a final utility class with only static methods: sort, reverse, shuffle, binarySearch, min, max, frequency, unmodifiableList, synchronizedMap. You implement Collection on a class; you call static methods on Collections. This is one of the most consistent interview questions at TCS and Infosys — it distinguishes candidates who understand the architecture from those who have only used ArrayList.
Q3. Why are Collection's modification methods described as optional?
The optional modifier appears in the interface documentation to indicate that an implementing class may throw UnsupportedOperationException instead of performing the operation. This design supports read-only views — Collections.unmodifiableList() and List.of() return objects that implement Collection but throw on any structural modification. Without the optional clause, read-only views would need a completely different interface, fragmenting the hierarchy. The tradeoff is that type safety for mutability is enforced at runtime, not compile time.
Q4. When should you use Collection as a parameter type instead of List or Set?
Use Collection<T> when the method only needs the operations that Collection defines — add, remove, contains, size, isEmpty, iterator, stream — without requiring ordering guarantees, index access, or uniqueness enforcement. If the method calls get(index), use List<T>. If it relies on uniqueness, use Set<T>. If it only iterates, use Iterable<T>. The general rule is: use the widest type that covers the operations you actually call. Using Collection<T> for methods that only need iteration adds unnecessary restriction compared to Iterable<T>.
Q5. What does addAll() return and why does it matter?
addAll(Collection<? extends E> c) returns true if the collection was modified and false if nothing changed. For a List, it almost always returns true (unless c is empty). For a Set, it returns false if all elements in c were already present. This return value is valuable when building a Set from an incoming collection and needing to detect whether any new elements were actually added — for example, in deduplication pipelines where the caller wants to know if any duplicates existed in the source.
Q6. What is the difference between removeAll() and retainAll()?
removeAll(c) removes every element from the collection that also appears in c — it computes the set difference. retainAll(c) removes every element that does NOT appear in c — it computes the intersection, keeping only elements present in both. Both return true if the collection was modified. A real usage pattern: after loading a large product catalogue, call catalogue.retainAll(activeProductIds) to remove discontinued products in one step rather than iterating and removing manually.
FAQs
What methods are defined directly in the Collection interface?
Collection defines: add, addAll, remove, removeAll, retainAll, clear (modification), contains, containsAll, size, isEmpty, toArray (query and conversion), and iterator (inherited from Iterable). Java 8 added the default methods stream, parallelStream, forEach, removeIf, and spliterator. Sub-interfaces like List and Set add their own methods on top of these.
Does Collection allow null elements?
It depends on the implementation, not the interface. ArrayList and LinkedList allow null. HashSet allows one null. TreeSet and PriorityQueue throw NullPointerException on null because they need to compare elements. ArrayDeque rejects null. List.of() and Set.of() (Java 9+) reject null explicitly. The Collection interface itself makes no statement about null — always check the specific implementation's documentation.
Why does Collection not define equals and hashCode contracts?
The Collection interface intentionally leaves equals and hashCode undefined to let sub-interfaces specify appropriate contracts for their semantics. List defines element-by-element ordered equality. Set defines membership-based unordered equality. Queue typically uses identity equality. A single equals contract at the Collection level would force all three to behave the same way, which would be wrong for at least two of them.
Can you write your own class that implements Collection?
Yes. Implement Collection<E>, provide all its abstract methods, and your class gets for-each support, can be passed to any Collections utility method, and works with all Java 8+ stream operations. AbstractCollection<E> in java.util provides default implementations for most methods — extending it requires only iterator() and size() to be implemented, making custom collection creation much less work.
What is the difference between toArray() and toArray(T[] a)?
toArray() returns Object[] — the caller must cast every element. toArray(new String[0]) returns String[] directly, with no casting required. The zero-length array argument tells the JVM the target component type. Passing new String[0] is preferred over new String[collection.size()] — on modern JVMs it is equally efficient and avoids allocating a pre-sized array that may be immediately discarded if the collection size changes between the call and the array creation.
Is Collection thread-safe?
No — the Collection interface makes no thread-safety guarantees. Whether a collection is thread-safe depends entirely on the implementation. ArrayList, HashSet, and HashMap are not thread-safe. For thread-safe alternatives, use CopyOnWriteArrayList (read-heavy concurrent list), ConcurrentHashMap (concurrent map), or Collections.synchronizedList(list) (single-lock wrapper). Collection does not define or document any synchronisation policy.
Summary
java.util.Collection<E> is the root contract for every single-element data structure in Java. It defines fourteen methods — plus five Java 8 default methods — that every List, Set, and Queue implementation honours. Map is not a Collection.
The practical takeaway: declare method parameters and variables at the widest useful interface level. Collection<T> is the right type when a method needs to add, remove, query, or iterate without caring whether the caller provides a List, Set, or Queue. This single discipline makes code more flexible, easier to test, and ready for implementation changes.
For interviews: know that Collection is an interface and Collections is a utility class, understand why modification methods are optional, be able to explain the equals contract difference between List and Set, and know that Map does not extend Collection. These questions appear at every level from campus placements at TCS to senior engineering rounds at Flipkart.
What to Read Next
Learn how Map stores data as key-value pairs.
Practice Programs
Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.
Creates, modifies, and iterates over an ArrayList.
Adds elements to a HashSet and shows that duplicates are silently ignored.
Converts a List into a Set by passing it straight into a HashSet constructor.
Converts a Set into a List by passing it straight into an ArrayList constructor.