Java Collections Framework: Hierarchy, Interfaces and Classes

Learn the Java Collections Framework with a Java 21 hierarchy diagram, how to choose List, Set, Map and Queue classes, and links to every tutorial.

Java Collections Framework hierarchy with Iterable, Collection, List, Set, Queue, Deque, Map and the Java 21 SequencedCollection, SequencedSet and SequencedMap interfaces

The Java Collections Framework is the set of interfaces and classes in java.util and java.util.concurrent that store, retrieve and process groups of objects, such as lists, sets, queues and maps. Every Java app uses it, whether to hold the rows of a query result, cache values by key, keep unique tags or queue work for background threads.

The framework gives us a small number of interfaces (List, Set, Queue, Deque, Map and, since Java 21, the sequenced interfaces), several implementations for each, and utility methods for sorting, searching and creating collections. The following example touches the operations we use most often in modern Java.

List<String> fruits = new ArrayList<>(List.of("apple", "banana"));
boolean added = fruits.add("cherry");                        // true
String first = fruits.getFirst();                            // "apple"
List<String> backwards = fruits.reversed();                  // [cherry, banana, apple]
Set<String> unique = new TreeSet<>(List.of("kiwi", "apple", "kiwi"));  // [apple, kiwi]
SequencedMap<String, Integer> stock = new LinkedHashMap<>();
Integer previous = stock.put("apple", 5);                    // null
Integer bananas = stock.merge("banana", 3, Integer::sum);    // 3
Map.Entry<String, Integer> lastEntry = stock.lastEntry();    // banana=3
Deque<String> orders = new ArrayDeque<>(List.of("pizza", "pasta"));
String nextOrder = orders.pollFirst();                       // "pizza"
List<String> longNames = fruits.stream().filter(f -> f.length() > 5).toList();  // [banana, cherry]

Notice that we declare variables by interface type and pick the class only on the right side. We go through the interface hierarchy, the sequenced collections added in Java 21, how to pick an implementation, and the thread-safe and legacy classes, with links to a detailed tutorial for every class.

1. What Is the Java Collections Framework?

A collection is an object that groups other objects, called its elements. Before Java 1.2, Java had only arrays, Vector and Hashtable, and every library invented its own container classes. The Collections Framework added in Java 1.2 replaced them with one set of interfaces, so a method that accepts a List works with any list implementation.

The framework has three parts.

  • Interfaces such as Collection, List and Map define what we can do with a group of elements.
  • Implementations such as ArrayList, HashSet and TreeMap store the elements with a specific data structure, so each one has its own speed and ordering.
  • Algorithms and helpers, mostly static methods in Collections and factory methods such as List.of(), sort, search, copy and wrap collections.

Since Java 5, all collection types are generic. A List<String> accepts only strings, so the compiler rejects a wrong element type instead of a ClassCastException appearing at runtime. We never use raw types such as List without a type argument in new code.

// does not compile: incompatible types, int cannot be converted to String
List<String> names = new ArrayList<>();
names.add(42);

2. Collection Interfaces and Their Hierarchy

Every collection type except maps extends Collection, which in turn extends Iterable, so any collection works in a for-each loop. The Map interface has its own branch because it stores key-value pairs, not single elements. Java 21 inserted three sequenced interfaces into the tree, which gives every ordered type a common parent.

Java Collections Framework hierarchy with Iterable, Collection, List, Set, Queue, Deque, Map and the Java 21 SequencedCollection, SequencedSet and SequencedMap interfaces
Ordered collection types share SequencedCollection or SequencedMap since Java 21, and Map stays outside the Collection branch

Each interface adds a guarantee on top of its parent. Knowing the guarantee is more useful than knowing the class names, because it decides which methods we can call.

InterfaceGuarantee it addsCommon implementations
CollectionA group of elements with add(), remove(), contains(), size() and stream()All except maps
SequencedCollection (Java 21)A defined encounter order with first and last elements and a reversed() viewList, Deque, LinkedHashSet, TreeSet
ListIndex-based access, duplicates allowedArrayList, LinkedList, CopyOnWriteArrayList
SetNo duplicate elements, based on equals()HashSet, LinkedHashSet, TreeSet, EnumSet
SortedSet / NavigableSetElements kept sorted, with range and nearest-match queriesTreeSet, ConcurrentSkipListSet
QueueElements wait for processing, with offer(), poll() and peek()PriorityQueue, ArrayBlockingQueue
DequeInsertion and removal at both ends, so it works as a queue and as a stackArrayDeque, LinkedList
MapUnique keys mapped to valuesHashMap, LinkedHashMap, TreeMap, EnumMap
SequencedMap (Java 21)A defined key order with firstEntry(), lastEntry() and reversed()LinkedHashMap, TreeMap

Code that only reads or loops over elements accepts the most general interface it needs. A method that takes a Collection<String> works with a list, a set or a deque, whereas a method that takes an ArrayList<String> forces callers to copy their data.

Collection<String> tags = new ArrayList<>(List.of("java", "sql", "jpa"));
int count = tags.size();                                     // 3
boolean hasSql = tags.contains("sql");                       // true
boolean removed = tags.removeIf(t -> t.startsWith("j"));     // true
Collection<String> left = tags;                              // 

A Map is part of the framework even though it is not a Collection. It gives us collection views of its content with keySet(), values() and entrySet(), and those views are regular collections.

Map<String, Integer> ages = new TreeMap<>(Map.of("Lokesh", 37, "Alex", 29));
Set<String> names = ages.keySet();                           // [Alex, Lokesh]
Collection<Integer> years = ages.values();                   // [29, 37]
boolean adult = ages.values().stream().allMatch(a -> a >= 18);  // true

3. Sequenced Collections in Java 21

Before Java 21, getting the last element looked different for every type, such as list.get(list.size() – 1), deque.getLast() or sortedSet.last(), and a LinkedHashSet had no way to get it at all. JEP 431 added SequencedCollection, SequencedSet and SequencedMap with the same methods for every ordered type. The sequenced collections tutorial covers each method in detail.

SequencedCollection<String> playlist = new ArrayList<>(List.of("intro", "verse"));
playlist.addFirst("count-in");
playlist.addLast("outro");
String opener = playlist.getFirst();                         // "count-in"
String closer = playlist.getLast();                          // "outro"
SequencedCollection<String> backwards = playlist.reversed(); // [outro, verse, intro, count-in]
SequencedMap<String, Integer> scores = new LinkedHashMap<>();
scores.put("ana", 90);
scores.put("ben", 75);
Map.Entry<String, Integer> firstScore = scores.firstEntry(); // ana=90
Map.Entry<String, Integer> removedScore = scores.pollLastEntry();  // ben=75

The reversed() method returns a view, not a copy, so it takes constant time to create and changes to the view write through to the original collection. On an unmodifiable list such as List.of(), the read methods work and addFirst() throws UnsupportedOperationException. On an empty collection, getFirst() throws NoSuchElementException.

List<String> fixed = List.of("a", "b");
String a = fixed.getFirst();                                 // "a"
fixed.addFirst("z");                                         // UnsupportedOperationException
String none = new ArrayList<String>().getFirst();            // NoSuchElementException

4. Choosing the Right Implementation

Pick the implementation from two questions, namely which order we need and how we look elements up. Most code ends up with ArrayList, HashMap, HashSet or ArrayDeque, and we move to a specialized class only when a requirement asks for it.

Say a food ordering app keeps the lines of a cart, the coupon codes a user already applied, the menu items by id and the orders waiting for the kitchen. The cart is an ArrayList because lines have a position and duplicates are fine. Applied coupons go into a HashSet, since the app only asks whether a code was used. The menu is a HashMap keyed by id, and the kitchen queue is an ArrayDeque, or a PriorityQueue when express orders go first.

We needUseTypical cost of the main operation
Ordered elements with index accessArrayListget(i) O(1), add() at end amortized O(1)
Fast membership checks, no orderHashSetcontains() O(1) on average
Unique elements in insertion orderLinkedHashSetcontains() O(1) on average
Unique elements kept sortedTreeSetadd(), contains() O(log n)
Lookup by key, no orderHashMapget(), put() O(1) on average
Keys in insertion or access orderLinkedHashMapget(), put() O(1) on average
Keys sorted, range queriesTreeMapget(), put() O(log n)
Queue or stackArrayDequeofferLast(), pollFirst(), push() O(1)
Smallest or highest-priority element firstPriorityQueueoffer(), poll() O(log n), peek() O(1)
Enum keys or enum elementsEnumMap, EnumSetArray or bit-vector access

Hash-based classes depend on correct equals() and hashCode() methods in the element or key class, and sorted classes depend on compareTo() or a Comparator. Records generate equals() and hashCode() from their components, so a record with immutable components works as a key without extra code.

5. List Implementations

A List keeps elements in the order we add them and allows duplicates. The Java List guide covers the interface methods, and the ArrayList guide covers the class we use in nearly every project. The ArrayList class stores elements in an array, so reading by index is fast, whereas LinkedList is faster only when we insert or remove elements at the front of the list or through a ListIterator.

TutorialWhat it covers
Java LinkedListA doubly linked list that implements List and Deque, and when it beats ArrayList
Java CopyOnWriteArrayListA thread-safe list for read-heavy data, such as a list of event listeners
Create a List with a single elementList.of(e), Collections.singletonList() and a mutable single-element list
UnsupportedOperationExceptionWhy add() fails on Arrays.asList() and List.of() lists, and how to fix it

6. Set Implementations

A Set rejects duplicates, so add() returns false when the element is already present. The three general-purpose sets differ only in the order in which they return elements.

Set<String> visited = new HashSet<>();
boolean firstVisit = visited.add("home");                    // true
boolean secondVisit = visited.add("home");                   // false
Set<String> ordered = new LinkedHashSet<>(List.of("cart", "home", "cart"));  // [cart, home]
NavigableSet<Integer> prices = new TreeSet<>(List.of(30, 10, 20));
Integer cheapestOver15 = prices.ceiling(15);                 // 20
TutorialWhat it covers
Java HashSetThe default set, backed by a HashMap, with no iteration order
Java LinkedHashSetA set that keeps insertion order and implements SequencedSet
Java TreeSetA sorted set with first(), ceiling(), headSet() and custom ordering
Java CopyOnWriteArraySetA thread-safe set for small, read-mostly data

7. Map Implementations

A Map stores one value per key. Calling put() with an existing key replaces the value, and methods such as getOrDefault(), merge() and computeIfAbsent() cover the counting and grouping code we used to write with if statements. The HashMap guide is the starting point.

Map<String, Integer> wordCount = new HashMap<>();
for (String word : List.of("to", "be", "or", "not", "to", "be")) {
    wordCount.merge(word, 1, Integer::sum);
}
Integer toCount = wordCount.get("to");                       // 2
Integer missing = wordCount.getOrDefault("is", 0);           // 0
Map<Character, List<String>> byLetter = new TreeMap<>();
byLetter.computeIfAbsent('a', k -> new ArrayList<>()).add("apple");
List<String> aWords = byLetter.get('a');                     // [apple]

The map tutorials are grouped by what we want to do. Choosing a class and comparing classes come first, followed by special-purpose maps and common tasks.

TutorialWhat it covers
Java LinkedHashMapInsertion order, access order and a simple LRU cache
Java TreeMapSorted keys, navigation methods and range views such as subMap()
TreeMap vs HashMapOrdering, performance and null handling side by side
Java HashtableThe legacy synchronized map, how it differs from HashMap and why new code avoids it
Java EnumMapA compact map for enum keys
Java WeakHashMapEntries that disappear when the key is no longer referenced
Java IdentityHashMapKey comparison with == instead of equals()
Immutable vs unmodifiable mapsMap.of(), Map.copyOf() and Collections.unmodifiableMap()
Case-insensitive map keysTreeMap with String.CASE_INSENSITIVE_ORDER and library options
Nested mapsMaps of maps, with safe reads and updates
Inverting a mapSwapping keys and values, including duplicate values
Convert a List to a MapCollectors.toMap() with merge functions and ordered maps

8. Queue and Deque Implementations

A Queue holds elements until something processes them, such as print jobs or tasks for a worker thread. The Java Queue guide explains the two method families, where add(), remove() and element() throw exceptions and offer(), poll() and peek() return false or null instead.

Deque<String> undo = new ArrayDeque<>();
undo.push("type A");
undo.push("type B");
String lastAction = undo.pop();                              // "type B"
Queue<Integer> tickets = new PriorityQueue<>(List.of(5, 1, 3));
Integer mostUrgent = tickets.poll();                         // 1
TutorialWhat it covers
Java ArrayDequeThe default queue and stack, faster than Stack and LinkedList
Java PriorityQueueNatural and Comparator ordering for the next element to process
Java ArrayBlockingQueueA bounded blocking queue for producer-consumer code
Java PriorityBlockingQueueA thread-safe priority queue that blocks on empty
Java SynchronousQueueA queue with no capacity that hands each element to a waiting thread
TransferQueue and LinkedTransferQueueProducers that wait until a consumer takes the element

9. Creating and Converting Collections

Since Java 9, the factory methods List.of(), Set.of() and Map.of() create unmodifiable collections in one line. They reject null elements, and Set.of() and Map.of() reject duplicates with an IllegalArgumentException. When we need a mutable collection, we pass the factory result to a constructor such as new ArrayList<>(…).

List<String> days = List.of("mon", "tue");
List<String> editable = new ArrayList<>(days);
boolean addedWed = editable.add("wed");                      // true
List<String> snapshot = List.copyOf(editable);               // [mon, tue, wed]
List<String> upper = editable.stream().map(String::toUpperCase).toList();  // [MON, TUE, WED]
Set<String> broken = Set.of("a", "a");                       // IllegalArgumentException: duplicate element: a
List<String> withNull = List.of("a", null);                  // NullPointerException

Streams are the usual way to build one collection from another. The stream to immutable collection tutorial compares Stream.toList(), Collectors.toUnmodifiableList() and Collectors.collectingAndThen(), and the groupingBy() tutorial shows how to build a Map of lists in one call.

10. Iterating and Sorting Collections

For most loops, the for-each loop or forEach() is enough, and the ways to iterate a collection article compares every option, including how to remove elements safely. The cursor interfaces behind those loops have their own tutorials.

  • Java Iterator explains hasNext(), next(), remove() and why changing a list inside a for-each loop throws ConcurrentModificationException.
  • Java ListIterator moves forward and backward through a list and adds, replaces or removes elements on the way.
  • Java Spliterator splits a source into parts, which is how parallel streams divide their work.
  • Iterator vs ListIterator vs Spliterator puts all cursors, including the legacy Enumeration, in one comparison table.

Sorting uses one of two contracts. A class that has one natural order implements Comparable, and any other order is a Comparator passed to List.sort(), TreeSet or TreeMap.

record Book(String title, int year) {}
List<Book> books = new ArrayList<>(List.of(new Book("Dune", 1965), new Book("Emma", 1815)));
books.sort(Comparator.comparingInt(Book::year));
String oldest = books.getFirst().title();                    // "Emma"

11. Thread-Safe Collections

The general-purpose classes such as ArrayList and HashMap are not thread-safe. When several threads change one collection, we use a class from java.util.concurrent, which handles locking internally and gives iterators that never throw ConcurrentModificationException.

Use caseClassTutorial
Shared cache or counters by keyConcurrentHashMapJava ConcurrentMap
Sorted keys shared between threadsConcurrentSkipListMapJava ConcurrentSkipListMap
Locking a whole map vs fine-grained locksCollections.synchronizedMap()synchronizedMap() vs ConcurrentHashMap
Producer-consumer hand-offArrayBlockingQueue, LinkedBlockingQueueSee the queue tutorials in section 8
Listeners read often, changed rarelyCopyOnWriteArrayListSee the list tutorials in section 5

A ConcurrentHashMap makes single operations such as merge() atomic, but two separate calls such as containsKey() followed by put() can still interleave with another thread. We use the atomic compound methods putIfAbsent(), computeIfAbsent() and merge() whenever we read a value and update it based on what we read.

ConcurrentMap<String, Integer> hits = new ConcurrentHashMap<>();
try (ExecutorService pool = Executors.newFixedThreadPool(4)) {
    for (int i = 0; i < 1000; i++) {
        pool.submit(() -> hits.merge("home", 1, Integer::sum));
    }
}
Integer homeHits = hits.get("home");                         // 1000

12. Legacy Collection Classes

The classes from Java 1.0 still compile and run, but new code has a better replacement for each. Apart from the Enumeration interface, these classes synchronize every method, which costs time in single-threaded code and still does not make compound operations safe.

Legacy classProblemUse instead
VectorSynchronized on every callArrayList, or CopyOnWriteArrayList for shared read-mostly lists
StackExtends Vector, so it exposes index access to a stackArrayDeque with push() and pop()
HashtableSynchronized, no null keys or valuesHashMap, or ConcurrentHashMap across threads
EnumerationNo remove(), long method namesIterator, or Enumeration.asIterator() for old APIs

We still meet these types in old APIs, for example Properties extends Hashtable, and ClassLoader.getResources() returns an Enumeration. In those places, we convert to a modern type at the boundary and keep the rest of the code on the current interfaces.

13. Java Collections FAQs

Developers who start with the Collections Framework ask these questions most often.

13.1. What is the difference between Collection and Collections?

The Collection interface is the root type for lists, sets and queues. The Collections class is a utility class with static methods such as sort(), shuffle(), unmodifiableList() and synchronizedMap().

13.2. Is Map part of the Java Collections Framework?

Yes, but Map does not extend Collection. A map stores key-value pairs, so methods such as add(E) do not fit. Its keySet(), values() and entrySet() methods return collection views.

13.3. Which collection gives the fastest lookup?

For lookup by key or membership checks, HashMap and HashSet run in constant time on average. For lookup by position, ArrayList.get(i) runs in constant time. A TreeMap is slower, O(log n), but keeps keys sorted.

13.4. Are Java collections thread-safe?

No, the general-purpose classes are not thread-safe. Use the classes in java.util.concurrent, such as ConcurrentHashMap and CopyOnWriteArrayList, when several threads change the same collection, and share unmodifiable collections freely.

13.5. Can a collection hold primitive values?

No. Collections hold objects, so an int is boxed into an Integer when we add it. For large amounts of numbers, an int[] array or an IntStream avoids the boxing cost.

13.6. Should we still use Vector and Hashtable?

No. Both are legacy classes from Java 1.0. Use ArrayList and HashMap in single-threaded code, and CopyOnWriteArrayList or ConcurrentHashMap when threads share the data.

14. Conclusion

The Java Collections Framework organizes containers into a few interfaces, and since Java 21 every ordered type also has getFirst(), getLast() and reversed(). We declare variables by interface, pick ArrayList, HashSet, HashMap or ArrayDeque by default, and move to sorted, ordered or concurrent classes when a requirement calls for them.

Factory methods and streams create collections in one line, and the legacy classes stay in old APIs only. Each class and task above links to its own tutorial with complete examples.

15. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.