Skip to content
Java collections 6 min read

Collection Interface

The Collection interface sits at the very top of the Java collections hierarchy. Almost every data structure you use — ArrayList, HashSet, LinkedList, PriorityQueue — traces its roots directly back to this single interface. Understanding it gives you a universal vocabulary for working with any group of objects in Java.

Collection interface hierarchy

What Is the Collection Interface?

Collection<E> lives in java.util and extends Iterable<E>, which means anything that implements it can be used in a for-each loop. It defines the minimum contract every collection must honor: you can add elements, remove them, check membership, ask for the size, and iterate.

import java.util.Collection;
import java.util.ArrayList;

public class CollectionDemo {
    public static void main(String[] args) {
        Collection<String> fruits = new ArrayList<>();

        fruits.add("Apple");
        fruits.add("Banana");
        fruits.add("Cherry");

        System.out.println("Size: " + fruits.size());
        System.out.println("Contains Banana: " + fruits.contains("Banana"));
        System.out.println(fruits);
    }
}

Output:

Size: 3
Contains Banana: true
[Apple, Banana, Cherry]

Notice that the variable is declared as Collection<String> even though the object is an ArrayList. This is a best practice — program to the interface, not the implementation. You can swap ArrayList for HashSet later without touching any other code.

The Hierarchy at a Glance

Collection sits inside a broader family tree. Here is how the key pieces relate:

Iterable<E>
  └── Collection<E>
        ├── List<E>      → ArrayList, LinkedList, Vector
        ├── Set<E>       → HashSet, LinkedHashSet, TreeSet
        └── Queue<E>     → PriorityQueue, ArrayDeque

Map is not a Collection — it maps keys to values and extends nothing in this hierarchy. You will find it documented separately on the Map Interface page.

Core Methods

Every class that implements Collection must provide these methods:

MethodDescription
add(E e)Adds one element; returns true if the collection changed
addAll(Collection<? extends E> c)Adds all elements from another collection
remove(Object o)Removes a single occurrence of the element
removeAll(Collection<?> c)Removes every element that appears in c
retainAll(Collection<?> c)Keeps only elements that also appear in c
contains(Object o)Returns true if the element is present
containsAll(Collection<?> c)Returns true if all elements of c are present
size()Returns the number of elements
isEmpty()Returns true if size is 0
clear()Removes all elements
toArray()Returns an Object[] snapshot
toArray(T[] a)Returns a typed array
iterator()Returns an Iterator<E> over the elements
stream()Returns a sequential Stream<E> (Java 8+)
parallelStream()Returns a parallel Stream<E> (Java 8+)

Working With the Methods

Adding and Removing Elements

import java.util.*;

public class AddRemoveDemo {
    public static void main(String[] args) {
        Collection<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5));

        // Add a single element
        numbers.add(6);

        // Add all from another collection
        numbers.addAll(List.of(7, 8));

        // Remove a specific element
        numbers.remove(Integer.valueOf(3)); // pass boxed type, not index!

        System.out.println(numbers);
    }
}

Output:

[1, 2, 4, 5, 6, 7, 8]

Warning: When you call remove() on a Collection<Integer>, always pass a boxed Integer, not a raw int. If you write numbers.remove(3), Java compiles it as remove(Object o) when the collection is typed — but on a List, it could match the remove(int index) overload instead, removing the element at index 3. Be explicit with Integer.valueOf(3).

Bulk Operations

import java.util.*;

public class BulkDemo {
    public static void main(String[] args) {
        Collection<String> a = new ArrayList<>(Arrays.asList("red", "green", "blue"));
        Collection<String> b = Arrays.asList("green", "blue", "yellow");

        // Intersection — keep only elements in both
        Collection<String> intersection = new ArrayList<>(a);
        intersection.retainAll(b);
        System.out.println("Intersection: " + intersection);

        // Difference — remove elements that appear in b
        Collection<String> difference = new ArrayList<>(a);
        difference.removeAll(b);
        System.out.println("Difference: " + difference);
    }
}

Output:

Intersection: [green, blue]
Difference: [red]

Iterating Over a Collection

Because Collection extends Iterable, you get three clean ways to iterate:

import java.util.*;

public class IterationDemo {
    public static void main(String[] args) {
        Collection<String> names = List.of("Alice", "Bob", "Carol");

        // 1. for-each (most common)
        for (String name : names) {
            System.out.println(name);
        }

        // 2. Iterator (use when you need to remove during iteration)
        Collection<String> mutable = new ArrayList<>(names);
        Iterator<String> it = mutable.iterator();
        while (it.hasNext()) {
            if (it.next().startsWith("B")) {
                it.remove(); // safe removal during iteration
            }
        }
        System.out.println("After removal: " + mutable);

        // 3. Stream (Java 8+)
        names.stream()
             .filter(n -> n.length() > 3)
             .forEach(System.out::println);
    }
}

Output:

Alice
Bob
Carol
After removal: [Alice, Carol]
Alice
Carol

Tip: Never call collection.remove() directly inside a for-each loop — it throws ConcurrentModificationException. Always use the Iterator’s own remove() method, or use removeIf() (Java 8+).

removeIf — the Cleaner Alternative (Java 8+)

import java.util.*;

public class RemoveIfDemo {
    public static void main(String[] args) {
        Collection<Integer> nums = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6));
        nums.removeIf(n -> n % 2 == 0); // remove all even numbers
        System.out.println(nums);
    }
}

Output:

[1, 3, 5]

Converting to an Array

import java.util.*;

public class ToArrayDemo {
    public static void main(String[] args) {
        Collection<String> colors = List.of("red", "green", "blue");

        // Typed array (preferred)
        String[] arr = colors.toArray(new String[0]);
        System.out.println(Arrays.toString(arr));

        // Java 11+: cleaner syntax
        String[] arr2 = colors.toArray(String[]::new);
        System.out.println(Arrays.toString(arr2));
    }
}

Output:

[red, green, blue]
[red, green, blue]

Tip: Passing new String[0] is the idiomatic way since Java 6. The JVM allocates the right-sized array internally — you do not need to pre-size it.

Writing a Method That Accepts Any Collection

Using Collection as a parameter type instead of ArrayList or HashSet makes your methods far more flexible:

import java.util.*;

public class SumDemo {
    // Works with ArrayList, HashSet, LinkedList — any Collection of Numbers
    public static double sum(Collection<? extends Number> numbers) {
        double total = 0;
        for (Number n : numbers) {
            total += n.doubleValue();
        }
        return total;
    }

    public static void main(String[] args) {
        System.out.println(sum(List.of(1, 2, 3)));           // 6.0
        System.out.println(sum(Set.of(1.5, 2.5, 3.0)));      // 7.0
    }
}

Under the Hood

How Collection Relates to Iterable

Collection inherits iterator() from Iterable. The for-each loop is pure compiler sugar — for (T x : col) desugars to:

Iterator<T> it = col.iterator();
while (it.hasNext()) {
    T x = it.next();
    // loop body
}

Every concrete class supplies its own Iterator that knows how to traverse its internal storage — an array index for ArrayList, a node pointer for LinkedList, a bucket walk for HashSet.

Fail-Fast Iterators

Most java.util collections use a fail-fast iterator. They track an internal modCount (a modification counter). Every structural change (add, remove, clear) increments modCount. The iterator saves a snapshot of modCount when created; if it detects a mismatch on the next hasNext() or next() call, it throws ConcurrentModificationException immediately rather than risking silent data corruption.

This is a diagnostic tool, not a thread-safety guarantee. For true concurrent access, use the classes in Concurrent Collections.

AbstractCollection — the Template

Java ships AbstractCollection<E> (an abstract class) that provides default implementations for most Collection methods built on top of just iterator() and size(). Concrete classes like ArrayList override these defaults with far more efficient implementations (e.g., contains in ArrayList is an O(n) array scan, while HashSet overrides it to be O(1)).

Streams Are Lazy

collection.stream() does not immediately process data. It creates a pipeline of operations that only execute when you call a terminal operation (forEach, collect, count, etc.). This means chaining several .filter() and .map() calls costs almost nothing until the terminal step triggers the traversal.

Quick Comparison: Sub-Interfaces of Collection

InterfaceDuplicatesOrder GuaranteedTypical Use
ListYesYes (insertion order)Ordered sequences, index access
SetNoDepends on implUnique values, membership testing
QueueYesFIFO / priorityTask queues, scheduling
  • Collections Framework — a bird’s-eye view of the entire framework and when to pick each data structure
  • List Interface — ordered, index-accessible collections that allow duplicates
  • Set Interface — collections that enforce uniqueness
  • Queue & PriorityQueue — FIFO and priority-based element ordering
  • Iterator — how to safely traverse and mutate any collection
  • Stream API — functional-style bulk operations on any Collection
Last updated June 13, 2026
Was this helpful?