Java Tutorial
🔍

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:

MethodArrayListHashSetTreeSetLinkedList
add(e)O(1) amortO(1) avgO(log n)O(1)
remove(o)O(n) scanO(1) avgO(log n)O(n) scan
contains(o)O(n) scanO(1) avgO(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 responsibility

Mistake 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

Practice Programs

Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.

Browse all Java programs →