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 BasithUpdated 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.
- Name the four kinds of collection,
List,Set,QueueandMap, 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
Iteratoror a map's views. - Explain when changing a collection inside a loop throws
ConcurrentModificationException, and remove safely withIterator.removeorremoveIf. - Make collections that cannot change with
List.of,Set.of,Map.ofandcopyOf, 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,getLastandreversed.
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:
READ THE LINES
Choose a line, here or in the drawing, to read it in plain words.
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.
| Interface | What it promises | Repeats | Order when you walk it | Everyday class |
|---|---|---|---|---|
List | Every element has a numbered position: 0, 1, 2 and so on. | Allowed | By position, first to last | ArrayList |
Set | No element appears twice. | Never: adding one again changes nothing | Depends on the class | HashSet |
Queue | Elements wait in line and leave from the front. | Allowed | Front to back | ArrayDeque |
Map | Each key leads to exactly one value. | Keys never, values may | Depends on the class | HashMap |
Key ideaEach 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#
import java.util.ArrayList;import java.util.List; public class Main { public static void main(String[] args) { List<String> orders = new ArrayList<>(); orders.add("latte"); orders.add("tea"); orders.add("latte"); orders.add("mocha"); System.out.println(orders); System.out.println("Second order: " + orders.get(1)); System.out.println("Orders taken: " + orders.size()); }}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#
import java.util.HashSet;import java.util.Set; public class Main { public static void main(String[] args) { Set<String> drinks = new HashSet<>(); drinks.add("latte"); drinks.add("tea"); boolean added = drinks.add("latte"); drinks.add("mocha"); System.out.println(drinks); System.out.println("Second latte added? " + added); System.out.println("Different drinks: " + drinks.size()); System.out.println("Sold any tea? " + drinks.contains("tea")); }}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.
import java.util.HashMap;import java.util.Map; public class Main { public static void main(String[] args) { Map<String, String> usual = new HashMap<>(); usual.put("Asha", "latte"); usual.put("Ben", "tea"); usual.put("Chen", "tea"); usual.put("Asha", "mocha"); System.out.println(usual); System.out.println("Ben usually has " + usual.get("Ben")); System.out.println("Dev usually has " + usual.get("Dev")); System.out.println("Regulars: " + usual.size()); }}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.
import java.util.HashMap;import java.util.Map; public class Main { public static void main(String[] args) { Map<String, Integer> stock = new HashMap<>(); stock.put("latte", 5); stock.put("tea", 8); stock.put("latte", 2); System.out.println(stock.size() + " " + stock.get("latte")); }}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#
import java.util.ArrayDeque;import java.util.Queue; public class Main { public static void main(String[] args) { Queue<String> line = new ArrayDeque<>(); line.offer("Asha"); line.offer("Ben"); line.offer("Chen"); System.out.println("Waiting: " + line); System.out.println("Next up: " + line.peek()); System.out.println("Serving " + line.poll()); System.out.println("Serving " + line.poll()); line.offer("Dev"); System.out.println("Waiting: " + line); }}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:
import java.util.ArrayDeque;import java.util.ArrayList;import java.util.Collection;import java.util.HashSet;import java.util.List; public class Main { public static void main(String[] args) { List<String> orders = new ArrayList<>(List.of("latte", "tea", "latte")); Collection<String> drinks = new HashSet<>(orders); Collection<String> line = new ArrayDeque<>(List.of("Asha", "Ben")); report("orders", orders); report("drinks", drinks); report("line", line); } static void report(String name, Collection<String> items) { System.out.println(name + ": " + items.size() + " items " + items); }}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:
import java.util.ArrayList;import java.util.Collection; public class Main { public static void main(String[] args) { Collection<String> orders = new ArrayList<>(); orders.add("tea"); System.out.println(orders.get(0)); }}$ javac Main.javaMain.java:8: error: cannot find symbol System.out.println(orders.get(0)); ^ symbol: method get(int) location: variable orders of type Collection<String>1 errorSo 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 ideaGive 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.
| Method | What it does | Gives 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> |
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.import java.util.ArrayList;import java.util.HashSet;import java.util.List;import java.util.Set; public class Main { public static void main(String[] args) { List<String> orders = new ArrayList<>(List.of("latte", "tea", "latte")); Set<String> drinks = new HashSet<>(orders); System.out.println(orders.add("tea") + " " + drinks.add("tea")); orders.addAll(List.of("mocha", "cocoa")); drinks.addAll(orders); System.out.println(orders); System.out.println(drinks); orders.removeIf(order -> order.equals("latte")); drinks.remove("cocoa"); System.out.println(orders + " " + orders.contains("latte")); System.out.println(drinks + " " + drinks.size()); long teas = orders.stream().filter(order -> order.equals("tea")).count(); System.out.println(teas + " teas"); }}- The first line prints
true false.orders.add("tea")returnstrue, because a list always takes another element.drinks.add("tea")returnsfalse, because the set already had tea. addAlladds 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].removeIfdrops both lattes from the list in one call, leaving[tea, tea, mocha, cocoa], andremovetakes 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.
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.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:
import java.util.ArrayDeque;import java.util.HashSet;import java.util.List;import java.util.Queue;import java.util.Set; public class Main { public static void main(String[] args) { Set<String> drinks = new HashSet<>(List.of("latte", "tea", "mocha")); for (String drink : drinks) { System.out.println("On the menu: " + drink); } Queue<String> line = new ArrayDeque<>(List.of("Asha", "Ben")); for (String name : line) { System.out.println(name + " is waiting"); } System.out.println(line.size() + " 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 thatnext()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:
for (String order : orders) { System.out.println(order);} // runs as if you had written:Iterator<String> it = orders.iterator();while (it.hasNext()) { String order = it.next(); System.out.println(order);}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:
import java.util.ArrayList;import java.util.Iterator;import java.util.List; public class Main { public static void main(String[] args) { List<String> orders = new ArrayList<>(List.of("latte", "tea", "latte", "mocha")); // The milk has run out: cancel every latte. Iterator<String> it = orders.iterator(); while (it.hasNext()) { String order = it.next(); if (order.equals("latte")) { it.remove(); } } System.out.println(orders); }}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>.
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.[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:
import java.util.HashMap;import java.util.Map; public class Main { public static void main(String[] args) { Map<String, String> usual = new HashMap<>(); usual.put("Asha", "latte"); usual.put("Ben", "tea"); usual.put("Chen", "tea"); for (Map.Entry<String, String> entry : usual.entrySet()) { System.out.println(entry.getKey() + " has " + entry.getValue()); } }}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.
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:
import java.util.ArrayList;import java.util.List; public class Main { public static void main(String[] args) { List<String> orders = new ArrayList<>(List.of("latte", "tea", "mocha", "cocoa")); for (String order : orders) { System.out.println("Checking " + order); if (order.equals("tea")) { orders.remove(order); } } System.out.println(orders); }}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 throwsConcurrentModificationException. 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:
| Moment | cursor | size | List's count | Iterator's copy |
|---|---|---|---|---|
| The loop makes its iterator | 0 | 4 | 0 | 0 |
next() hands out latte | 1 | 4 | 0 | 0 |
next() hands out tea | 2 | 4 | 0 | 0 |
orders.remove(order) takes tea out | 2 | 3 | 1 | 0 |
hasNext(): 2 is not 3, so true | 2 | 3 | 1 | 0 |
next(): 1 is not 0, so it throws | 2 | 3 | 1 | 0 |
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.
import java.util.ArrayList;import java.util.List; public class Main { public static void main(String[] args) { List<String> orders = new ArrayList<>(List.of("latte", "tea", "mocha", "cocoa")); for (String order : orders) { System.out.println("Checking " + order); if (order.equals("mocha")) { orders.remove(order); } } System.out.println(orders); }}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.
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.
Iterator<String> it = orders.iterator();while (it.hasNext()) { String order = it.next(); // the body: one of the remove buttons, or nothing}orders
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
removeAlloraddAllonce 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:
import java.util.ArrayDeque;import java.util.List;import java.util.Queue;import java.util.Set; public class Main { public static void main(String[] args) { Queue<String> line = new ArrayDeque<>(List.of("Asha", "Ben", "Chen", "Dev")); Set<String> gaveUp = Set.of("Ben", "Dev"); line.removeIf(name -> gaveUp.contains(name)); System.out.println(line); System.out.println("Next up: " + line.peek()); }}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 ideaNever 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.
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.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.
import java.util.List;import java.util.Map;import java.util.Set; public class Main { public static void main(String[] args) { List<String> sizes = List.of("small", "medium", "large"); Set<String> milks = Set.of("whole", "oat", "soy"); Map<String, Integer> millilitres = Map.of("small", 250, "medium", 350, "large", 450); System.out.println(sizes); System.out.println(milks.contains("oat") + " " + milks.size()); System.out.println(millilitres.get("medium")); milks.add("almond"); }}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.
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.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.
import java.util.ArrayList;import java.util.Collections;import java.util.List; public class Main { public static void main(String[] args) { List<String> specials = new ArrayList<>(List.of("soup", "pie")); List<String> board = Collections.unmodifiableList(specials); List<String> flyer = List.copyOf(specials); specials.add("cake"); System.out.println("board: " + board); System.out.println("flyer: " + flyer); }}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.
add and remove throw UnsupportedOperationException.add and remove throw UnsupportedOperationException.Set.copyOf and Map.copyOf copy sets and maps in the same way, and Collections.unmodifiableSet and Collections.unmodifiableMap make views of them.
Key ideaList.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.
import java.util.ArrayDeque;import java.util.ArrayList;import java.util.Deque;import java.util.List; public class Main { public static void main(String[] args) { List<String> orders = new ArrayList<>(List.of("latte", "tea", "mocha")); System.out.println("First: " + orders.getFirst()); System.out.println("Last: " + orders.getLast()); orders.addFirst("espresso"); orders.removeLast(); System.out.println(orders); System.out.println(orders.reversed()); Deque<String> cups = new ArrayDeque<>(List.of("blue", "red", "green")); System.out.println(cups.getFirst() + " " + cups.getLast()); System.out.println(cups.reversed()); }}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.
import java.util.LinkedHashSet;import java.util.List;import java.util.SequencedSet; public class Main { public static void main(String[] args) { SequencedSet<String> tried = new LinkedHashSet<>(List.of("latte", "tea", "mocha")); System.out.println(tried.getFirst() + " then " + tried.getLast()); tried.addLast("latte"); System.out.println(tried); System.out.println(tried.reversed()); }}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:
import java.util.ArrayList;import java.util.List; public class Main { public static void main(String[] args) { List<String> orders = new ArrayList<>(List.of("latte", "tea")); List<String> newestFirst = orders.reversed(); orders.add("mocha"); System.out.println(newestFirst); }}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 job | Declare it as | Start with | Also worth knowing |
|---|---|---|---|
| Items in order, reached by position, repeats allowed: a playlist, the day's orders | List | ArrayList | LinkedList, which is rarely faster; linked lists shows when |
| Each item once, and a quick answer to “is it in there?”: usernames taken, tags | Set | HashSet | LinkedHashSet 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 order | Map | HashMap | LinkedHashMap 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 jobs | Queue | ArrayDeque | PriorityQueue serves the smallest or most urgent first, in stacks and queues |
| The newest item back first: an undo history, a stack of plates | Deque | ArrayDeque | It replaces the old Stack class |
| A fixed group that never changes: cup sizes, the days of the week | List, Set or Map | List.of, Set.of, Map.of | copyOf 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.
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.
Try it
Answer each one in your head or on paper first, then open the answer.
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?specials is an ArrayList. What is the difference between Collections.unmodifiableList(specials) and List.copyOf(specials)?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.
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."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.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.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().Further reading#
Spotted a mistake, or a part that did not make sense? Report it by email