Learn Java

Lesson 34Phase 4: Collections and algorithms

The Collections Framework

List, Set, Map and Queue: what each one promises, which class to pick, and how to walk any of them safely.

By Updated 50 min

  • Collection
  • List
  • Set
  • Map
  • Queue
  • Iterator

You have kept groups of values in an ArrayList for a while now. A list suits a playlist or a row of scores, but other jobs come with other rules. A guest list should never name the same person twice. A phone book finds a number by a name. At a shop counter, whoever arrived first is served first. Java's library has a ready-made container for each of these jobs, and together they are called the Collections Framework. This lesson shows you the family: what each kind promises, which class to pick, and how to walk through any of them without tripping over your own changes.

By the end of this lesson
  • Name the four kinds of collection, List, Set, Queue and Map, and say what each one promises about order, repeats and keys.
  • Declare a collection by its interface, as in List<String> orders = new ArrayList<>();, and say which methods that lets you call.
  • Use the methods every collection shares, and walk any collection with a for-each loop, an Iterator or a map's views.
  • Explain when changing a collection inside a loop throws ConcurrentModificationException, and remove safely with Iterator.remove or removeIf.
  • Make collections that cannot change with List.of, Set.of, Map.of and copyOf, and tell a copy from a read-only view.
  • Choose a collection class for a job, and reach the ends of an ordered one with getFirst, getLast and reversed.
From before
You have used ArrayList since ArrayList: a list that can grow, including List.of, removeIf, and the crash you get when you remove from a list inside a for-each loop. From interfaces you know that an interface is a promise and a class keeps it with implements, and designing classes well showed the habit of giving a variable the interface type. Generics explained the angle brackets: List<E> is a list of some element type E. The lambdas passed to removeIf come from enums and lambdas.

A family of containers#

Picture the software behind a small café. It has to remember four different things:

  • The orders taken today, in the order they came in. Two customers can both ask for a latte, so a drink may appear twice.
  • The different drinks sold today, each one named once.
  • Each regular customer's usual drink, found by the customer's name.
  • The customers waiting at the counter, served in the order they arrived.

You could keep all four in ArrayLists, but then you would enforce every rule by hand: check for a repeat before each add, search the list for a name, remember to always take from the front. Java has a container type for each rule, with the rule built in.

A collection is an object that holds a group of other objects, called its elements. The Collections Framework is the set of interfaces and classes in java.util for working with them. Each interface is a promise about how the elements behave, and one or more classes keep each promise. It is the split you know from interfaces: List is the promise, and ArrayList is one class that keeps it. This is the family:

Class diagramthe collection family
«interface»Iterable«interface»Collection«interface»List«interface»Set«interface»Queue«interface»DequeArrayListHashSetArrayDeque«interface»MapHashMap

READ THE LINES

Choose a line, here or in the drawing, to read it in plain words.

Type parameters such as <E> are left off. Since Java 21 one more interface, SequencedCollection, also sits between Collection and both List and Deque, as the section on first and last elements explains.

Read it from the top. Iterableis the smallest promise: “you can walk through me, one element at a time”. Collection extends it with the methods every group of elements has, such as add, remove, contains and size. List, Set and Queue each extend Collection with one more rule, and Deque extends Queue. When one interface builds on another, Java writes extends and the diagram draws a solid line. The dashed lines go from a class to the interface it implements.

Map stands apart. A map holds pairs, a key and a value, so its basic operation takes two things, put(key, value), where Collection's add takes one. Map is still part of the framework, but it does not extend Collection.

InterfaceWhat it promisesRepeatsOrder when you walk itEveryday class
ListEvery element has a numbered position: 0, 1, 2 and so on.AllowedBy position, first to lastArrayList
SetNo element appears twice.Never: adding one again changes nothingDepends on the classHashSet
QueueElements wait in line and leave from the front.AllowedFront to backArrayDeque
MapEach key leads to exactly one value.Keys never, values mayDepends on the classHashMap
Key idea

Each collection interface is a promise about its elements: a List keeps positions, a Set refuses repeats, a Queue hands out from the front, and a Map finds a value by its key.

Four kinds, side by side#

Each kind is easiest to understand by watching it work. Here are the café's four jobs as four short programs. Step through them and keep an eye on the memory view under the code, which draws each kind of collection in its own way.

A List keeps order and repeats#

Orders.java
1import java.util.ArrayList;
2import java.util.List;
3
4public class Main {
5 public static void main(String[] args) {
6 List<String> orders = new ArrayList<>();
7 orders.add("latte");
8 orders.add("tea");
9 orders.add("latte");
10 orders.add("mocha");
11
12 System.out.println(orders);
13 System.out.println("Second order: " + orders.get(1));
14 System.out.println("Orders taken: " + orders.size());
15 }
16}
Prints
›[latte, tea, latte, mocha]
›Second order: tea
›Orders taken: 4

It prints [latte, tea, latte, mocha], then Second order: tea and Orders taken: 4. Both lattes stay, in the order they were added, and get(1) reaches the second order by its position. This is the ArrayList you already know. The only new thing is the type on the left, List, which the next section explains.

A Set drops repeats#

Drinks.java
1import java.util.HashSet;
2import java.util.Set;
3
4public class Main {
5 public static void main(String[] args) {
6 Set<String> drinks = new HashSet<>();
7 drinks.add("latte");
8 drinks.add("tea");
9 boolean added = drinks.add("latte");
10 drinks.add("mocha");
11
12 System.out.println(drinks);
13 System.out.println("Second latte added? " + added);
14 System.out.println("Different drinks: " + drinks.size());
15 System.out.println("Sold any tea? " + drinks.contains("tea"));
16 }
17}
Prints
›[tea, mocha, latte]
›Second latte added? false
›Different drinks: 3
›Sold any tea? true

It prints [tea, mocha, latte], Second latte added? false, Different drinks: 3 and Sold any tea? true. The second add("latte") changed nothing and returned false, because a set holds each element at most once. A set has no positions either, so there is no get(1). You ask a set whether it contains something.

Look at the order. The set printed tea, mocha, latte, which is neither the order the drinks were added nor alphabetical. A HashSet files each element into one of a row of numbered buckets, picked by a number worked out from the element itself, and it prints and walks them bucket by bucket. The narration and the memory view show the buckets as you step; HashMap and HashSet explains how a bucket is picked. For now, keep the rule: a HashSet makes no promise about order. When you need one, LinkedHashSet keeps the order in which elements were first added, and TreeSet keeps them sorted.

A Map pairs keys with values#

A map stores entries: pairs of a key and a value. The two types in the angle brackets are the key type and then the value type, so Map<String, String> maps a customer's name to a drink. put(key, value) stores an entry and get(key) finds the value again.

Regulars.java
1import java.util.HashMap;
2import java.util.Map;
3
4public class Main {
5 public static void main(String[] args) {
6 Map<String, String> usual = new HashMap<>();
7 usual.put("Asha", "latte");
8 usual.put("Ben", "tea");
9 usual.put("Chen", "tea");
10 usual.put("Asha", "mocha");
11
12 System.out.println(usual);
13 System.out.println("Ben usually has " + usual.get("Ben"));
14 System.out.println("Dev usually has " + usual.get("Dev"));
15 System.out.println("Regulars: " + usual.size());
16 }
17}
Prints
›{Asha=mocha, Ben=tea, Chen=tea}
›Ben usually has tea
›Dev usually has null
›Regulars: 3

It prints {Asha=mocha, Ben=tea, Chen=tea}, then Ben usually has tea, Dev usually has null and Regulars: 3. Ben and Chen both drink tea, so values may repeat. Keys may not: the second put for Asha replaced latte with mocha, and the map still holds three entries. Asking for a key that is not there, such as Dev, gives null.

Predict the outputStock.java
1import java.util.HashMap;
2import java.util.Map;
3
4public class Main {
5 public static void main(String[] args) {
6 Map<String, Integer> stock = new HashMap<>();
7 stock.put("latte", 5);
8 stock.put("tea", 8);
9 stock.put("latte", 2);
10 System.out.println(stock.size() + " " + stock.get("latte"));
11 }
12}

A map of drinks in stock gets three put calls. What does the last line print?

It prints 2 2. The third put found the key latte already in the map and replaced its value, 5, with 2. A map never holds two entries with the same key, so the size stays at 2.

A Queue serves the oldest first#

Line.java
1import java.util.ArrayDeque;
2import java.util.Queue;
3
4public class Main {
5 public static void main(String[] args) {
6 Queue<String> line = new ArrayDeque<>();
7 line.offer("Asha");
8 line.offer("Ben");
9 line.offer("Chen");
10 System.out.println("Waiting: " + line);
11
12 System.out.println("Next up: " + line.peek());
13 System.out.println("Serving " + line.poll());
14 System.out.println("Serving " + line.poll());
15 line.offer("Dev");
16 System.out.println("Waiting: " + line);
17 }
18}
Prints
›Waiting: [Asha, Ben, Chen]
›Next up: Asha
›Serving Asha
›Serving Ben
›Waiting: [Chen, Dev]

It prints Waiting: [Asha, Ben, Chen], Next up: Asha, Serving Asha, Serving Ben and Waiting: [Chen, Dev]. offer adds an element at the back of the queue, peek looks at the front one without taking it, and poll takes the front one out and returns it. Asha arrived first, so she was served first. That rule is called first in, first out, or FIFO. On an empty queue, peek and poll return null instead of stopping the program.

ArrayDeque is the everyday class for a queue. A Deque(said “deck”, short for double-ended queue) lets you add and take at both ends, which also makes it a good stack, where the newest element comes out first. A PriorityQueue is a queue that always hands out its smallest element first. Both get a full lesson in stacks, queues and deques.

Declare the interface, create the class#

All four programs gave the variable an interface type and created the object from a class: List<String> orders = new ArrayList<>();. You met this habit in designing classes well, where it was called programming to an interface, and it is the usual way to declare a collection in Java. Read the line in two halves. The left side is the promise the rest of the program relies on: orders is some kind of list. The right side picks the class that keeps the promise.

The split pays off in two ways. Switching to another class touches one word: change new ArrayList to new LinkedList and every other line still compiles, because the rest of the program only ever asked for a List. And a method can accept any collection that keeps the promise it needs. This method asks only for a Collection, so it takes a list, a set and a queue:

Report.java
1import java.util.ArrayDeque;
2import java.util.ArrayList;
3import java.util.Collection;
4import java.util.HashSet;
5import java.util.List;
6
7public class Main {
8 public static void main(String[] args) {
9 List<String> orders = new ArrayList<>(List.of("latte", "tea", "latte"));
10 Collection<String> drinks = new HashSet<>(orders);
11 Collection<String> line = new ArrayDeque<>(List.of("Asha", "Ben"));
12
13 report("orders", orders);
14 report("drinks", drinks);
15 report("line", line);
16 }
17
18 static void report(String name, Collection<String> items) {
19 System.out.println(name + ": " + items.size() + " items " + items);
20 }
21}
Prints
›orders: 3 items [latte, tea, latte]
›drinks: 2 items [tea, latte]
›line: 2 items [Asha, Ben]

It prints orders: 3 items [latte, tea, latte], drinks: 2 items [tea, latte] and line: 2 items [Asha, Ben]. One method served three kinds of collection. The set came from new HashSet<>(orders), which copies every element of the list and drops the repeat. The general-purpose collection classes all have a constructor like this, which copies the elements of any other collection.

The price is the rule you learned in polymorphism: the declared type decides which methods you may call. A Collection has no positions, so it has no get, even when the object behind it is an ArrayList:

Main.java
read along
1import java.util.ArrayList;
2import java.util.Collection;
3
4public class Main {
5 public static void main(String[] args) {
6 Collection<String> orders = new ArrayList<>();
7 orders.add("tea");
8 System.out.println(orders.get(0));
9 }
10}
Terminal
read along
1$ javac Main.java
2Main.java:8: error: cannot find symbol
3 System.out.println(orders.get(0));
4 ^
5 symbol: method get(int)
6 location: variable orders of type Collection<String>
71 error

So pick the most general interface that still has every method your code calls. A variable you read by position needs List. A method that only counts, checks, adds or loops can take a Collection, and then it works for every kind.

Key idea

Give a collection variable or parameter an interface type, and create the object from a class. Choose the most general interface that still has every method you call.

What every collection can do#

List, Set and Queue all extend Collection, so they share its methods. Learn these once and they work on all three.

MethodWhat it doesGives back
add(e)Adds e. A set skips an element it already holds.true if the collection changed
remove(o)Removes one element equal to o, if there is one.true if something was removed
contains(o)Checks whether an element equal to o is in there.true or false
size()Counts the elements.an int
isEmpty()The same as size() == 0, and easier to read.true or false
addAll(other)Adds every element of another collection.true if the collection changed
removeIf(test)Removes every element the lambda answers true for.true if anything was removed
clear()Removes every element.nothing
iterator()Hands out an object that walks the elements, explained below.Iterator<E>
stream()Starts a stream, a way to describe work on all the elements at once.Stream<E>
What “equal” means here
contains and remove look for an element equal to the one you pass, by calling its equals method. Strings, numbers and records compare their values, so that works as you would expect. An object of a class you wrote counts as equal only to itself until you teach the class otherwise, which is the subject of equals, hashCode and toString.
Shared.java
1import java.util.ArrayList;
2import java.util.HashSet;
3import java.util.List;
4import java.util.Set;
5
6public class Main {
7 public static void main(String[] args) {
8 List<String> orders = new ArrayList<>(List.of("latte", "tea", "latte"));
9 Set<String> drinks = new HashSet<>(orders);
10
11 System.out.println(orders.add("tea") + " " + drinks.add("tea"));
12 orders.addAll(List.of("mocha", "cocoa"));
13 drinks.addAll(orders);
14 System.out.println(orders);
15 System.out.println(drinks);
16
17 orders.removeIf(order -> order.equals("latte"));
18 drinks.remove("cocoa");
19 System.out.println(orders + " " + orders.contains("latte"));
20 System.out.println(drinks + " " + drinks.size());
21
22 long teas = orders.stream().filter(order -> order.equals("tea")).count();
23 System.out.println(teas + " teas");
24 }
25}
Prints
›true false
›[latte, tea, latte, tea, mocha, cocoa]
›[tea, mocha, cocoa, latte]
›[tea, tea, mocha, cocoa] false
›[tea, mocha, latte] 3
›2 teas
  • The first line prints true false. orders.add("tea") returns true, because a list always takes another element. drinks.add("tea") returns false, because the set already had tea.
  • addAll adds a whole collection. The list grows to six orders, [latte, tea, latte, tea, mocha, cocoa], while the set takes in only the two drinks it did not have and prints [tea, mocha, cocoa, latte].
  • removeIf drops both lattes from the list in one call, leaving [tea, tea, mocha, cocoa], and remove takes cocoa out of the set, leaving three drinks.
  • The last two lines count the teas with a stream and print 2 teas. Counting leaves the list as it was.
Streams have lessons of their own
Read orders.stream().filter(order -> order.equals("tea")).count()as “stream the orders, keep the teas, count them”. Streams are a whole way of working with collections, and they start in streams. Until then, loops do every job on this page.
ExerciseFirstRepeat.java
add returns false when a set already holds the element. Use that to find the first order that repeats an earlier one. For these orders the program should print First repeat: mocha.

Walking through any collection#

The for-each loop you used on arrays and lists works on every collection. That is the promise Iterable makes at the top of the family tree: a for-each loop can walk any object that is Iterable. Here it walks a set and a queue:

Walk.java
1import java.util.ArrayDeque;
2import java.util.HashSet;
3import java.util.List;
4import java.util.Queue;
5import java.util.Set;
6
7public class Main {
8 public static void main(String[] args) {
9 Set<String> drinks = new HashSet<>(List.of("latte", "tea", "mocha"));
10 for (String drink : drinks) {
11 System.out.println("On the menu: " + drink);
12 }
13
14 Queue<String> line = new ArrayDeque<>(List.of("Asha", "Ben"));
15 for (String name : line) {
16 System.out.println(name + " is waiting");
17 }
18 System.out.println(line.size() + " still waiting");
19 }
20}
Prints
›On the menu: tea
›On the menu: mocha
›On the menu: latte
›Asha is waiting
›Ben is waiting
›2 still waiting

The set is walked in its own order, tea, mocha, latte, the same order it prints in. The queue is walked from front to back, and walking takes nothing out: the last line prints 2 still waiting. Only poll takes elements out of a queue.

The iterator behind the loop#

A for-each loop runs on an iterator, an object whose only job is to walk a collection one element at a time. Every collection hands one out from its iterator() method, and an iterator has three methods:

  • hasNext() answers whether there is another element to visit.
  • next() hands out the next element and moves past it.
  • remove() removes the element that next() handed out last.

The Java Language Specification defines the for-each loop by turning it into iterator code. The first loop below runs exactly as if you had written the second:

What for-each means
read along
1for (String order : orders) {
2 System.out.println(order);
3}
4
5// runs as if you had written:
6Iterator<String> it = orders.iterator();
7while (it.hasNext()) {
8 String order = it.next();
9 System.out.println(order);
10}

You write the iterator out yourself when the loop needs it, usually to remove elements as it goes. The milk has run out, so every latte order must be cancelled:

SoldOut.java
1import java.util.ArrayList;
2import java.util.Iterator;
3import java.util.List;
4
5public class Main {
6 public static void main(String[] args) {
7 List<String> orders = new ArrayList<>(List.of("latte", "tea", "latte", "mocha"));
8
9 // The milk has run out: cancel every latte.
10 Iterator<String> it = orders.iterator();
11 while (it.hasNext()) {
12 String order = it.next();
13 if (order.equals("latte")) {
14 it.remove();
15 }
16 }
17 System.out.println(orders);
18 }
19}
Prints
›[tea, mocha]

It prints [tea, mocha]. Each pass asks hasNext(), takes the next order with next(), and calls it.remove() on each latte. Step through it and watch the list in the memory view: each latte disappears, the later orders shift left, and the loop still visits every order. Iterator lives in java.util, so it needs an import, and its type parameter matches the collection's: an iterator over strings is an Iterator<String>.

One remove per next
it.remove() removes the element that the last next() handed out, so you can call it once after each next(). Calling it before the first next(), or twice in a row, throws an IllegalStateException.
Put the lines in orderCups.java
Put the lines in order so the program uses an iterator to remove every cup size under 300 ml and prints [350, 450].

Tap a line, then tap where it should go. Or use the arrows.

Walking a map#

A Map is not Iterable, so for (String name : usual) does not compile: javac stops with for-each not applicable to expression type. Instead, a map offers three views of itself, and each view is a collection you can walk: keySet() holds the keys, values() the values, and entrySet() the entries. Walking the entries gives you both halves of each pair:

WalkMap.java
1import java.util.HashMap;
2import java.util.Map;
3
4public class Main {
5 public static void main(String[] args) {
6 Map<String, String> usual = new HashMap<>();
7 usual.put("Asha", "latte");
8 usual.put("Ben", "tea");
9 usual.put("Chen", "tea");
10
11 for (Map.Entry<String, String> entry : usual.entrySet()) {
12 System.out.println(entry.getKey() + " has " + entry.getValue());
13 }
14 }
15}
Prints
›Asha has latte
›Ben has tea
›Chen has tea

It prints Asha has latte, Ben has tea and Chen has tea. Each entry is a Map.Entry<String, String>, and getKey() and getValue() read its two halves. The regulars come out as Asha, Ben, Chen here, but a HashMap promises no order, the same as a HashSet. The views read the map itself, so removing something from a view removes it from the map. HashMap and HashSet walks all three views and puts that to use.

forEach with a lambda
Every Iterable also has a forEach method that takes a lambda: orders.forEach(order -> System.out.println(order)); does the same job as a for-each loop. Changing the collection inside the lambda causes the same trouble as the next section describes.

Changing a collection while you walk it#

In the ArrayList lesson you saw this crash: removing from a list inside a for-each loop stopped the program with a ConcurrentModificationException. Now that you know a for-each loop runs on an iterator, you can see exactly where the exception comes from, and why it sometimes does not come at all. Here the café cancels the tea order while it checks the orders:

Cancel.java
1import java.util.ArrayList;
2import java.util.List;
3
4public class Main {
5 public static void main(String[] args) {
6 List<String> orders = new ArrayList<>(List.of("latte", "tea", "mocha", "cocoa"));
7
8 for (String order : orders) {
9 System.out.println("Checking " + order);
10 if (order.equals("tea")) {
11 orders.remove(order);
12 }
13 }
14 System.out.println(orders);
15 }
16}
Prints
›Checking latte
›Checking tea
!Exception in thread "main" java.util.ConcurrentModificationException
! at Main.main(Main.java:8)
Stops on line 8

ConcurrentModificationException

The collection changed while a loop was walking through it.

It prints Checking latte and Checking tea, removes tea, and then stops with the exception on the for line, at the moment the loop asks for the next order.

Two numbers inside the iterator#

An ArrayList counts the changes made to it. Every add or removeadds one to a counter that the JDK's source code calls modCount, and so do a few other changes, such as clear and sort. When the list hands out an iterator, the iterator keeps two numbers of its own:

  • a cursor, the position of the next element it will hand out, starting at 0;
  • a copy of the list's change count, taken when the iterator was made.

Its two methods use them like this:

  • hasNext() compares the cursor with the size. If they differ, it answers true. It never looks at the change count.
  • next() first compares the list's change count with its copy. If they differ, the list was changed behind the iterator's back, and it throws ConcurrentModificationException. If they match, it hands out the element at the cursor and moves the cursor on by one.

Here is the café program again, number by number:

MomentcursorsizeList's countIterator's copy
The loop makes its iterator0400
next() hands out latte1400
next() hands out tea2400
orders.remove(order) takes tea out2310
hasNext(): 2 is not 3, so true2310
next(): 1 is not 0, so it throws2310

The name sounds like threads, but nothing here runs at the same time. “Concurrent modification” means the list was modified while a walk over it was in progress, and the documentation of the exception says that a single thread can cause it in exactly this way.

When the alarm stays quiet#

hasNext() never checks the count, and that gap explains the odd case from the ArrayList lesson. Predict this one, where the loop removes mocha, the second-to-last order.

Predict the outputCancel.java
1import java.util.ArrayList;
2import java.util.List;
3
4public class Main {
5 public static void main(String[] args) {
6 List<String> orders = new ArrayList<>(List.of("latte", "tea", "mocha", "cocoa"));
7
8 for (String order : orders) {
9 System.out.println("Checking " + order);
10 if (order.equals("mocha")) {
11 orders.remove(order);
12 }
13 }
14 System.out.println(orders);
15 }
16}

The loop now cancels mocha instead of tea. What does the program print?

It prints three Checking lines and [latte, tea, cocoa], with no exception. After mocha is removed, the size is 3 and the cursor is 3. hasNext() sees two equal numbers and answers false, so the loop ends before next() can notice the changed count. Cocoa is never checked. Had cocoa needed cancelling too, it would still be on the list, and nothing would tell you.

Quick check
Same program, but this time the loop cancels cocoa, the last order, when it reaches it. What happens?

Try all of these in the lab below. Each press of Next pass runs hasNext() and next() once, and the two remove buttons are the two ways the loop body could remove the current order. Start from Reset each time, remove tea, then mocha, then cocoa, and watch the cursor and the two counts.

Inside a for-each loopArrayList
Iterator<String> it = orders.iterator();
while (it.hasNext()) {
String order = it.next();
// the body: one of the remove buttons, or nothing
}

orders

cursor
latte
0
tea
1
mocha
2
cocoa
3

order is not set yet

size
4
cursor
0
list's change count
0
iterator's copy
0

The loop has made its iterator. Its cursor is 0, and it has copied the list's change count, 0. Press Next pass to run hasNext() and next().

The exception works like a smoke alarm for a bug. It goes off in most cases, but the documentation promises only a best effort, which is why the quiet case exists. So never let a program depend on the exception. Write the loop so the problem cannot happen.

Removing safely#

There are three safe ways to change a collection that you are walking:

  • Remove through the iterator, as in SoldOut.java. it.remove() moves the cursor back one place and copies the new count, so the iterator and the list stay in step. In the lab, it.remove() never leads to an exception.
  • Call removeIf. It walks and removes in one call, and every collection has it: lists, sets, queues and the views of a map.
  • Collect first, change afterwards. Walk the collection, put what you want to remove or add in a second list, and call removeAll or addAll once the loop has finished. This is the way to add, because adding inside a for-each loop trips the same check as removing.

Two customers got tired of waiting and left the line. removeIf takes them out of the queue without a loop of your own:

LeftEarly.java
1import java.util.ArrayDeque;
2import java.util.List;
3import java.util.Queue;
4import java.util.Set;
5
6public class Main {
7 public static void main(String[] args) {
8 Queue<String> line = new ArrayDeque<>(List.of("Asha", "Ben", "Chen", "Dev"));
9 Set<String> gaveUp = Set.of("Ben", "Dev");
10
11 line.removeIf(name -> gaveUp.contains(name));
12 System.out.println(line);
13 System.out.println("Next up: " + line.peek());
14 }
15}
Prints
›[Asha, Chen]
›Next up: Asha

It prints [Asha, Chen] and Next up: Asha. Ben and Dev are gone, and everyone else keeps their place in the line. The set gaveUp is only asked contains, which is why it can come from Set.of.

Key idea

Never change the size of a collection inside a for-each loop over it. Remove through the iterator, call removeIf, or collect the changes and make them after the loop.

Replacing does not count
A list's set(i, x) swaps one element for another and leaves the change count alone, so it is safe inside a loop over the list. Adding, removing, clearing and sorting all count.
ExerciseSoldOut.java
Latte and cocoa have sold out. Remove every order for a sold-out drink, without an exception and without skipping any, so the program prints the orders that are left in their original order.

Collections that cannot change#

List.of from the ArrayList lesson builds a list that nobody can change. Set and Map have the same kind of method: Set.of and Map.of arrived together with List.of in Java 9. Map.of takes keys and values in turns: key, value, key, value.

Sizes.java
1import java.util.List;
2import java.util.Map;
3import java.util.Set;
4
5public class Main {
6 public static void main(String[] args) {
7 List<String> sizes = List.of("small", "medium", "large");
8 Set<String> milks = Set.of("whole", "oat", "soy");
9 Map<String, Integer> millilitres = Map.of("small", 250, "medium", 350, "large", 450);
10
11 System.out.println(sizes);
12 System.out.println(milks.contains("oat") + " " + milks.size());
13 System.out.println(millilitres.get("medium"));
14
15 milks.add("almond");
16 }
17}
Prints
›[small, medium, large]
›true 3
›350
!Exception in thread "main" java.lang.UnsupportedOperationException
! at Main.main(Main.java:15)
Stops on line 15

UnsupportedOperationException

This collection cannot be changed. List.of and similar methods make read-only lists.

It prints [small, medium, large], true 3 and 350, then stops on the last line with an UnsupportedOperationException. All three collections are unmodifiable: every method that would change them, such as add, put, remove or clear, throws that exception, while reading works as usual. They also refuse null as an element, a key or a value, with a NullPointerException the moment you create them.

Never depend on the order of Set.of or Map.of
The program printed the list, but it only looked things up in the set and the map. Java does not say in which order a set from Set.of or a map from Map.of walks its elements, and in practice the order changes from one run of the program to the next. Anything that depends on that order, printing included, can give a different result each time. When you need an order, copy the elements into a collection that has one: new TreeSet<>(milks) walks them in sorted order.
Quick check
What happens when this line runs? Set<String> milks = Set.of("oat", "soy", "oat");

A copy or a view?#

Sometimes you want to let other code read a list that you are still changing. Java has two ways to hand it over, and they behave differently. Think of the café's specials. The chalkboard by the door shows the specials as they are right now. A printed flyer shows them as they were when it was printed.

Predict the outputSpecials.java
1import java.util.ArrayList;
2import java.util.Collections;
3import java.util.List;
4
5public class Main {
6 public static void main(String[] args) {
7 List<String> specials = new ArrayList<>(List.of("soup", "pie"));
8 List<String> board = Collections.unmodifiableList(specials);
9 List<String> flyer = List.copyOf(specials);
10
11 specials.add("cake");
12 System.out.println("board: " + board);
13 System.out.println("flyer: " + flyer);
14 }
15}

The kitchen adds cake after the board and the flyer have been made. What do they show?

It prints board: [soup, pie, cake] and flyer: [soup, pie]. Notice the s in Collections: it is a class full of static helper methods, not the Collection interface.

Collections.unmodifiableList(specials)
List.copyOf(specials)
A view: it keeps no elements of its own and reads them from specials each time.
A copy: it gets its own elements, taken at the moment you call it.
Later changes to specials show through, like the chalkboard.
Later changes to specials do not show, like the flyer.
Nobody can change the list through the view: add and remove throw UnsupportedOperationException.
Nobody can change the copy at all: add and remove throw UnsupportedOperationException.
Use it to let other code read a list that you keep changing.
Use it for a snapshot that never changes, like the defensive copy in designing classes well.

Set.copyOf and Map.copyOf copy sets and maps in the same way, and Collections.unmodifiableSet and Collections.unmodifiableMap make views of them.

Key idea

List.of, Set.of, Map.of and copyOf build collections that nobody can change. Collections.unmodifiableList builds a read-only window onto a list that its owner can still change.

First, last and reversed#

Some collections keep their elements in a definite order: a list, a deque, a LinkedHashSet (the order of adding) and a TreeSet (sorted order). Until Java 21, each of them reached its ends in its own way. The last element of a list was orders.get(orders.size() - 1), a deque had getLast(), a TreeSet had last(), and a LinkedHashSet had no direct way at all: you walked the whole set to reach its last element.

Java 21 gave all of them one set of names through a new interface, SequencedCollection, which sits between Collection and List or Deque. Its methods are getFirst, getLast, addFirst, addLast, removeFirst, removeLast and reversed.

Ends.java
1import java.util.ArrayDeque;
2import java.util.ArrayList;
3import java.util.Deque;
4import java.util.List;
5
6public class Main {
7 public static void main(String[] args) {
8 List<String> orders = new ArrayList<>(List.of("latte", "tea", "mocha"));
9 System.out.println("First: " + orders.getFirst());
10 System.out.println("Last: " + orders.getLast());
11
12 orders.addFirst("espresso");
13 orders.removeLast();
14 System.out.println(orders);
15 System.out.println(orders.reversed());
16
17 Deque<String> cups = new ArrayDeque<>(List.of("blue", "red", "green"));
18 System.out.println(cups.getFirst() + " " + cups.getLast());
19 System.out.println(cups.reversed());
20 }
21}
Prints
›First: latte
›Last: mocha
›[espresso, latte, tea]
›[tea, latte, espresso]
›blue green
›[green, red, blue]

It prints First: latte and Last: mocha. addFirst puts espresso at the front and removeLast takes mocha off the end, leaving [espresso, latte, tea], and reversed() gives the list back to front: [tea, latte, espresso]. The deque answers to the same names with blue green and [green, red, blue]. On an empty list, getFirst() throws a NoSuchElementException, where get(0) would throw an IndexOutOfBoundsException.

A LinkedHashSet implements SequencedSet, the set version. On a set, addFirst and addLast never create a repeat: they move an element that is already there to that end. The variable has to be declared as a SequencedSet (or a LinkedHashSet), since a plain Set has no getFirst.

Tried.java
1import java.util.LinkedHashSet;
2import java.util.List;
3import java.util.SequencedSet;
4
5public class Main {
6 public static void main(String[] args) {
7 SequencedSet<String> tried = new LinkedHashSet<>(List.of("latte", "tea", "mocha"));
8 System.out.println(tried.getFirst() + " then " + tried.getLast());
9
10 tried.addLast("latte");
11 System.out.println(tried);
12 System.out.println(tried.reversed());
13 }
14}
Prints
›latte then mocha
›[tea, mocha, latte]
›[latte, mocha, tea]

addLast("latte") moved latte from the front to the end instead of adding a second one. reversed() does not copy anything, either. It returns a view of the same collection in the opposite order, so a change to the original shows up in it:

NewestFirst.java
1import java.util.ArrayList;
2import java.util.List;
3
4public class Main {
5 public static void main(String[] args) {
6 List<String> orders = new ArrayList<>(List.of("latte", "tea"));
7 List<String> newestFirst = orders.reversed();
8
9 orders.add("mocha");
10 System.out.println(newestFirst);
11 }
12}
Prints
›[mocha, tea, latte]

Step through it. In the step where mocha joins orders, the memory view shows it at the front of newestFirst as well, because there is only one list underneath.

A HashSet has no order to speak of, so it is not a sequenced collection, and getFirst() on one stops javac with cannot find symbol. Maps with an order, LinkedHashMap and TreeMap, got the same treatment through SequencedMap, with methods such as firstEntry(), lastEntry() and reversed().

Choosing a collection#

Choosing comes down to a few questions about the job. Do you find things by a key? May an item appear twice? Which item do you want back next, and does the order matter when you walk them? Answer those, pick the interface, then pick the class.

The jobDeclare it asStart withAlso worth knowing
Items in order, reached by position, repeats allowed: a playlist, the day's ordersListArrayListLinkedList, which is rarely faster; linked lists shows when
Each item once, and a quick answer to “is it in there?”: usernames taken, tagsSetHashSetLinkedHashSet keeps the order of adding; TreeSet keeps items sorted, as sorting shows
A value found by its key: a word and its count, a customer and an orderMapHashMapLinkedHashMap keeps the order of adding; TreeMap keeps keys sorted. Both are in HashMap and HashSet
Items served in the order they arrived: a waiting line, print jobsQueueArrayDequePriorityQueue serves the smallest or most urgent first, in stacks and queues
The newest item back first: an undo history, a stack of platesDequeArrayDequeIt replaces the old Stack class
A fixed group that never changes: cup sizes, the days of the weekList, Set or MapList.of, Set.of, Map.ofcopyOf for a frozen copy of another collection

When you are not sure, start with ArrayList, HashSet or HashMap, declared as List, Set or Map. Because the rest of your code uses only the interface, switching to a LinkedHashSet or a TreeMap later changes a single word.

The picker asks the same questions one at a time. Try it with the jobs in the table, or with something from a program you use every day.

Collection picker

Will you look things up by a key?

A key is what you search by: a customer's name to find their usual drink, or a word to find how often it appears.

Quick check
A game remembers which levels the player has finished. Finishing a level twice must not list it twice, and the menu shows the finished levels in the order the player finished them. Which class fits best?

Try it

Answer each one in your head or on paper first, then open the answer.

1
For each job, name the interface you would declare: the songs in a playlist, where a song may repeat; the words a spell checker knows; each word in a book with the number of times it appears; documents waiting for a printer.
2
Collection<String> orders = new ArrayList<>(); compiles, but orders.get(0) on the next line does not. Why, when the object behind it is an ArrayList? What would you change?
3
This loop cancels the latte, the first order. What does it print before it stops, and why does it stop?
4
Inside a for-each loop over an ArrayList, why does removing the second-to-last element end the loop without an exception, while removing the last one throws?
5
specials is an ArrayList. What is the difference between Collections.unmodifiableList(specials) and List.copyOf(specials)?
6
A Map is not Iterable. How do you print every key together with its value?

Practice

Work through these on your own. Each one builds on this page.

1
Write a method static int countLong(Collection<String> words, int min) that counts the words with at least min letters. Call it with a List, a HashSet and an ArrayDeque built from the same words, with one long word written twice, and explain why the set gives a smaller count.
2
Change Cancel.java so that it adds a "biscuit" order whenever it sees a tea. Run it and read the exception. Then fix it: collect the new orders in a second list during the loop, and add them all with addAll once the loop has finished.
3
Simulate the café counter with a Queue<String>. Three customers arrive, one is served, two more arrive, and then everyone left is served. Before each serving, print who is next with peek, then print who was served.
4
In the iterator lab, start from Reset and remove each of the four orders with orders.remove(order), one per run. For each, write down whether the loop throws, ends early or finishes, and explain why using the cursor, the size and the two counts. Then do the same with it.remove().
5
Pick an app you use every day, such as a music player, a chat app or an online shop. Name three collections it probably keeps, and for each give the interface, the class you would start with, and one sentence on why.

Further reading#

Spotted a mistake, or a part that did not make sense? Report it by email