Java Collections Framework: Overview and Hierarchy

Introduction

Almost every Java program has to keep a group of objects: the items of an order, the users who are logged in, the words of a text and how often each occurs. This tutorial gives an overview of the Java Collections Framework, the part of the JDK that is used for this. It covers, in this order, what the framework consists of, its interface hierarchy - including the sequenced collections added in Java 21 - and the maps, which form a hierarchy of their own. It then lists the main implementations and what sets them apart, shows the operations common to all collections in a program run against four different implementations, explains why variables are declared with an interface type, and ends with a guide to choosing the right collection. The examples run on JDK 25.

(Note - Collections are generic types such as List<String>. If generics are new to you, read Introduction to Java GenericsClick to read tutorial: Introduction to Java Generics: Why and How first.)

What the Collections Framework is

An array holds a fixed number of elements of one type, and it can do little more than store and return them by index. The Java Collections Framework, in the package java.util, provides the data structures that programs need beyond that. It has three parts -
  • Interfaces - List, Set, Queue, Deque, Map and others. Each describes a kind of collection and what can be done with it, independent of how it is stored.
  • Implementations - classes such as ArrayList, HashSet and HashMap, which store the elements in a particular way: in an array, a hash table, a linked list or a tree.
  • Algorithms - ready-made operations such as sorting, searching, shuffling and finding the largest element, in the Collections class and as methods of the interfaces.
Compared with an array, a collection grows and shrinks as elements are added and removed, and each kind offers what its purpose needs: a set refuses duplicates, a map finds a value by its key, a queue hands out elements in order. All collections hold objects, so primitive values such as int are stored as their wrapper objectsClick to read: wrapper objects (Integer), and Java converts between the two automatically.

The Collection interface hierarchy

All collections except maps belong to one family of interfaces, with Collection at its centre. The Java Collection interface hierarchy - Iterable at the top, then Collection; below Collection the interfaces List, Set and Queue; SequencedCollection, added in Java 21, sits between Collection and List, Deque and SequencedSet; SortedSet and NavigableSet extend SequencedSet; Deque extends Queue; the main classes ArrayList, LinkedList, HashSet, LinkedHashSet, TreeSet, PriorityQueue and ArrayDeque are shown under the interfaces they implement
  • Iterable is the root. Anything Iterable can be used in a for-each loop, which is why every collection can be looped over in the same way. How to make your own classes iterable is shown in Implementing the Iterable interfaceClick to read: Implementing the Iterable interface.
  • Collection declares the operations every collection has: add, remove, contains, size, isEmpty, clear, the bulk operations addAll, removeAll and retainAll, and stream().
  • List - an ordered sequence that allows duplicates, with access by index: get(i), set(i, e), add(i, e).
  • Set - a collection without duplicates. Whether two elements are duplicates is decided by their equals() and hashCode() methods, see the equals and hashCode contractClick to read: the equals and hashCode contract. SortedSet and NavigableSet keep the elements sorted and add range operations such as first(), headSet() and ceiling().
  • Queue - elements waiting to be processed, normally first in, first out, with offer, poll and peek. Deque (double-ended queue) can add and remove at both ends, so it serves as a queue and as a stack.
  • SequencedCollection, added in Java 21, is the common type of the collections that have a defined order - lists, deques and ordered sets. It gives them uniform methods for both ends: getFirst(), getLast(), addFirst(), addLast(), removeFirst(), removeLast() and reversed(). Java 21 Sequenced Collections ExplainedClick to read: Java 21 Sequenced Collections Explained covers them in detail.

The Map interface hierarchy

A map stores key-value pairs: each key appears once and is associated with one value, so a value is found by its key - a word and its count, a user id and the user. Because a map holds pairs rather than single elements, Map does not extend Collection; it is the root of a hierarchy of its own. It is still part of the framework, and it offers its contents as collections through three views: keySet(), values() and entrySet(). The Java Map interface hierarchy - Map at the top; SequencedMap, added in Java 21, below it; SortedMap and NavigableMap extend SequencedMap; HashMap implements Map, LinkedHashMap implements SequencedMap, TreeMap implements NavigableMap; a map offers its contents as three collection views - keySet is a Set, values is a Collection and entrySet is a Set of entries
  • Map declares put, get, getOrDefault, containsKey, remove, size and the three views.
  • SequencedMap (Java 21) is the type of maps with a defined order of their entries, with firstEntry(), lastEntry(), pollFirstEntry() and reversed().
  • SortedMap and NavigableMap keep the keys sorted and add range operations such as headMap(), floorKey() and ceilingKey().

The main implementations

Each interface has several implementations. They behave the same as far as the interface goes, and differ in order, duplicates, nulls and speed, because they store the elements differently. These ten cover almost all everyday needs -
ClassInterfaceStored inOrder of iterationDuplicatesnull
ArrayListLista resizable arrayinsertion (index) orderyesyes
LinkedListList, Dequea doubly linked listinsertion (index) orderyesyes
HashSetSeta hash tableno defined ordernoone
LinkedHashSetSequencedSeta hash table + a linked listinsertion ordernoone
TreeSetNavigableSeta red-black treesortednono
ArrayDequeDequea resizable circular arrayfront to backyesno
PriorityQueueQueuea binary heappoll() returns the smallest firstyesno
HashMapMapa hash tableno defined orderkeys noone key
LinkedHashMapSequencedMapa hash table + a linked listinsertion orderkeys noone key
TreeMapNavigableMapa red-black treesorted by keykeys nono keys
None of these classes is thread-safe. When several threads change a collection, use a concurrent collection from java.util.concurrent, such as ConcurrentHashMap - see Java 8 ConcurrentHashMap, LongAdder and StampedLockClick to read: Java 8 ConcurrentHashMap, LongAdder and StampedLock.

The common Collection operations

Because all collections share the Collection interface, one method can work with any of them. The following program fills four different collections through the same method, with the same six words - one of them twice - and then uses only Collection methods on them.
CommonOperations.java
//CommonOperations.java
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.TreeSet;

public class CommonOperations {

  static void fill(Collection<String> words) {
    for (String w : List.of("pear", "apple", "fig", "apple", "kiwi", "banana")) {
      words.add(w);
    }
  }

  static void describe(String name, Collection<String> words) {
    System.out.printf("%-10s size %d  contains fig: %-5s  %s%n", name, words.size(), words.contains("fig"), words);
  }

  public static void main(String[] args) {
    Collection<String> list = new ArrayList<>();
    Collection<String> hashSet = new HashSet<>();
    Collection<String> treeSet = new TreeSet<>();
    Collection<String> deque = new ArrayDeque<>();
    fill(list);
    fill(hashSet);
    fill(treeSet);
    fill(deque);
    describe("ArrayList", list);
    describe("HashSet", hashSet);
    describe("TreeSet", treeSet);
    describe("ArrayDeque", deque);

    System.out.println();
    list.remove("apple");
    System.out.println("ArrayList after remove(\"apple\") : " + list);
    list.removeIf(w -> w.length() < 5);
    System.out.println("after removeIf(length < 5)      : " + list);
    treeSet.retainAll(List.of("apple", "kiwi", "plum"));
    System.out.println("TreeSet after retainAll         : " + treeSet);
    for (String w : hashSet) {
      System.out.print(w.toUpperCase() + " ");
    }
    System.out.println("<- for-each over the HashSet");
  }
}
 OUTPUT of the above code
ArrayList  size 6  contains fig: true   [pear, apple, fig, apple, kiwi, banana]
HashSet    size 5  contains fig: true   [banana, apple, pear, kiwi, fig]
TreeSet    size 5  contains fig: true   [apple, banana, fig, kiwi, pear]
ArrayDeque size 6  contains fig: true   [pear, apple, fig, apple, kiwi, banana]

ArrayList after remove("apple") : [pear, fig, apple, kiwi, banana]
after removeIf(length < 5)      : [apple, banana]
TreeSet after retainAll         : [apple, kiwi]
BANANA APPLE PEAR KIWI FIG <- for-each over the HashSet
Explanation of the output
  • fill() and describe() accept any Collection<String>, so the same code worked with a list, two sets and a deque.
  • The list and the deque kept both "apple"s and the insertion order. The two sets dropped the duplicate, so their size is 5: add() on a set returns false when the element is already there.
  • The TreeSet printed its words in alphabetical order. The HashSet printed them in an order that comes from the words' hash codes; it is not insertion order, and a program must not depend on it.
  • remove("apple") removed only the first of the two, removeIf() removed every word shorter than five letters (see Collection.removeIfClick to read: Collection.removeIf), and retainAll() kept only the words that are also in the given list.
  • The for-each loop works on every collection, because every collection is Iterable.

Maps and their views

A map is used through its keys. The next program counts how often each word occurs - the most common use of a map - and then looks at the result through the three views.
WordCount.java
//WordCount.java
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

public class WordCount {
  public static void main(String[] args) {
    String text = "the cat and the dog and the bird";
    Map<String, Integer> counts = new HashMap<>();
    for (String word : text.split(" ")) {
      counts.put(word, counts.getOrDefault(word, 0) + 1);
    }
    System.out.println("counts           : " + counts);
    System.out.println("count of the     : " + counts.get("the"));
    System.out.println("count of fish    : " + counts.get("fish") + " / getOrDefault: " + counts.getOrDefault("fish", 0));

    Map<String, Integer> sorted = new TreeMap<>(counts);
    System.out.println("keySet (sorted)  : " + sorted.keySet());
    System.out.println("values           : " + sorted.values());
    for (Map.Entry<String, Integer> e : sorted.entrySet()) {
      System.out.print(e.getKey() + "=" + e.getValue() + "  ");
    }
    System.out.println("<- entrySet");
  }
}
 OUTPUT of the above code
counts           : {the=3, and=2, cat=1, bird=1, dog=1}
count of the     : 3
count of fish    : null / getOrDefault: 0
keySet (sorted)  : [and, bird, cat, dog, the]
values           : [2, 1, 1, 1, 3]
and=2  bird=1  cat=1  dog=1  the=3  <- entrySet
Explanation of the output
  • put() with a key that is already there replaces its value, which is how each count went up: getOrDefault(word, 0) returns the current count, or 0 for a new word.
  • get() returns null for a key that is not in the map, which is easy to miss; getOrDefault() avoids it.
  • Copying the HashMap into a TreeMap sorted the entries by key. The views then showed the keys, the values in key order, and the entries as key-value pairs.

Programming to the interface

In the examples, the variables are declared with an interface type - List<String> words = new ArrayList<>(), not ArrayList<String> words = .... This is the usual style, for two reasons -
  • The rest of the code depends only on what a list can do, not on how this one is stored. Changing the implementation is then a change of one line - from ArrayList to LinkedList, or from HashSet to TreeSet when the elements should come out sorted.
  • Methods that take and return interface types, such as fill(Collection<String>) above, can be used with any implementation.
The class name is needed only where the object is created. Declare a variable with the class type only when the code needs a method that the interfaces do not have, which is rare.

Sequenced collections in Java 21

Before Java 21, getting the last element meant list.get(list.size() - 1) for a list, deque.getLast() for a deque, and a loop for a LinkedHashSet. The sequenced interfaces give all ordered collections the same methods.
SequencedDemo.java
//SequencedDemo.java
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.SequencedMap;
import java.util.SequencedSet;

public class SequencedDemo {
  public static void main(String[] args) {
    List<String> steps = new ArrayList<>(List.of("plan", "build", "test"));
    steps.addFirst("idea");
    steps.addLast("ship");
    System.out.println("list     : " + steps + "  first " + steps.getFirst() + ", last " + steps.getLast());
    System.out.println("reversed : " + steps.reversed());

    SequencedSet<String> visited = new LinkedHashSet<>(List.of("home", "search", "product"));
    System.out.println("set      : " + visited + "  last visited " + visited.getLast());

    SequencedMap<String, Integer> scores = new LinkedHashMap<>();
    scores.put("Tom", 72);
    scores.put("Ann", 95);
    scores.put("Raj", 88);
    System.out.println("map      : " + scores + "  first entry " + scores.firstEntry() + ", reversed " + scores.reversed());
  }
}
 OUTPUT of the above code
list     : [idea, plan, build, test, ship]  first idea, last ship
reversed : [ship, test, build, plan, idea]
set      : [home, search, product]  last visited product
map      : {Tom=72, Ann=95, Raj=88}  first entry Tom=72, reversed {Raj=88, Ann=95, Tom=72}
reversed() returns a reversed view, not a copy: it reflects the original, and changes made through it change the original.

Choosing a collection

Two questions lead to the right interface, and a third to the implementation - Choosing a Java collection - if values are looked up by a key, use a Map: HashMap by default, LinkedHashMap to keep insertion order, TreeMap for sorted keys; if duplicates must be rejected, use a Set: HashSet, LinkedHashSet or TreeSet in the same way; if elements are taken from the ends, first in first out or last in first out, use ArrayDeque, or PriorityQueue to take the smallest first; otherwise use a List, which is ArrayList in almost all cases
  • Looked up by a key? - a Map.
  • Must duplicates be rejected? - a Set.
  • Taken from the front or the back, as in a queue or a stack? - a Deque, or a PriorityQueue when the smallest (or highest-priority) element must come out first.
  • Otherwise - a List.
  • Then the order: the hash-based class by default, the linked one to keep insertion order, the tree-based one to keep the elements sorted.
For the list itself, ArrayList is almost always the right choice; why, and when LinkedList is not, is the subject of ArrayList vs LinkedListClick to read tutorial: Java ArrayList vs LinkedList: Performance and When to Use. For collections that never change, Java 9's List.of(), Set.of() and Map.of() create compact unmodifiable ones - see Java 9 immutable collectionsClick to read: Java 9 immutable collections.

Summary

In this tutorial we saw that the Java Collections Framework consists of interfaces, implementations and algorithms. The interfaces form two hierarchies: Iterable and Collection with List, Set, Queue and Deque, joined in Java 21 by SequencedCollection; and Map, which stores key-value pairs and offers them through its views. The main implementations differ in how they store elements, and so in order, duplicates and nulls. A program ran the same Collection operations against a list, two sets and a deque and showed those differences, and a word count showed a map and its views. We declared variables with interface types, used the sequenced methods of Java 21, and finished with three questions for choosing a collection. The next tutorial compares the two lists: ArrayList vs LinkedListClick to read tutorial: Java ArrayList vs LinkedList: Performance and When to Use.