10.1
The Collections Framework
Each way of storing data makes some jobs quick and others costly. Java gives you several choices behind shared interfaces, so you can switch when your program needs something different.
Previously on
You already know the parts that make this framework work.
In Section 4.2 you learned why arrays provide fast access but have a fixed length. In its last exercise you built a growing array by hand and found that doubling its size beats adding one slot at a time.
Section 7.2 gave you interfaces and polymorphism. Section 9.3 gave you generics.
This phase puts those ideas together.
The problem
Imagine that your program needs to keep some numbers and answer questions about them.
int[] numbers = {3, 7, 10, 1, 4, 12};Adding to the end is quick. Put the value in the next free slot. The work stays the same even if the array is large.
Finding the largest takes time. The largest value could be anywhere, so you must inspect every element. A million elements can mean a million checks.
So sort it and keep it sorted:
int[] sorted = {1, 3, 4, 7, 10, 12};Now the largest value is easy to find. It is the last element: sorted[sorted.length - 1].
Adding has become more expensive. To insert 6, you first find its place, then shift every following value one slot to the right.
Sorting made one job easier and another job harder.
The idea
Java handles this by providing several implementations: array-based, linked, hash-based, and tree-based. Each one has different strengths. They share interfaces, which means you can often change the implementation without changing the rest of your code.
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.get(0);
numbers = new LinkedList<>(); // the implementation changed
numbers.add(1); // the same List methods still compile
numbers.get(0);The variable has the type List. At first it refers to an ArrayList; later it refers to a
LinkedList. The methods here come from List, so both objects can handle them.
This is polymorphism from Section 7.2.
The compiler checks that you are using List methods. When the program runs, Java uses the
methods from the object that is actually there.
The shape of the framework
Iterable
|
Collection
___________|___________
| | |
List Set Queue
ArrayList HashSet ArrayDeque
LinkedList TreeSet PriorityQueue
Vector LinkedHashSet
Stack
Map <- a separate family, not a Collection
___________|___________
| | |
HashMap TreeMap LinkedHashMapRead the diagram from top to bottom. Each interface adds a new promise:
| Interface | What it promises | |
|---|---|---|
| Iterable | the starting point | you can visit one item at a time |
| Collection | adds common operations | add, remove, size, contains |
| List | adds order and positions | get(3), duplicates allowed |
| Set | requires unique items | no duplicates, usually no order |
| Queue | defines an order for removal | which item comes out next |
| Map | is not a Collection | stores key-value pairs |
Under the hood
CoreWhy Map sits outside
This often surprises people at first.
Collection is designed for one item at a time. Its central method is:
boolean add(E element);A Map needs a pair for each entry: a key and a value. Its central method is:
V put(K key, V value);put needs two arguments, so it does not fit the one-element add(E element) method. That is
why Map has its own branch of the framework.
You can check it yourself:
System.out.println(Collection.class.isAssignableFrom(Map.class)); // falseHashMap implements Map, Cloneable, and Serializable. It does not implement Iterable,
which is why this does not compile:
for (String s : myMap) { } // ERROR. A Map is not Iterable.To loop over a map, first ask it for a collection view:
for (String key : myMap.keySet()) { }
for (Integer v : myMap.values()) { }
for (Map.Entry<String, Integer> e : myMap.entrySet()) { }Those three results are collections. They let you work through a map one key, value, or entry at a time.
What each implementation is really made of
The framework is built from structures you already know, then presented through shared interfaces.
| Class | What is underneath | |
|---|---|---|
| ArrayList | a growable array | the same idea you built in the 4.2 exercises |
| LinkedList | nodes joined by references | each node points to the next one |
| HashSet | a HashMap that keeps the keys | buckets chosen with hashCode |
| TreeSet | a balanced tree | items stay sorted as the tree is searched |
| ArrayDeque | a circular array | efficient at both ends |
| PriorityQueue | a heap stored in an array | the smallest item stays at the front |
None of these classes hides a brand-new idea. They use arrays, references, and trees in ways that favour different operations.
A marker interface that changes real code
Both classes are lists, but ArrayList also has RandomAccess:
ArrayList -> List, RandomAccess, Cloneable, Serializable, Collection, Iterable
LinkedList -> List, Deque, Cloneable, Serializable, Queue, Collection, IterableRandomAccess appears only on the first line. It has no methods. It is a marker interface
from Section 8.5. It tells the library that
reaching element i is cheap, as you saw in Section 4.2.
Collections.binarySearch checks for it:
if (list instanceof RandomAccess) {
// use index based search
} else {
// walk with an iterator instead
}Repeated indexing of a linked list would be costly, so the library chooses another strategy. That one empty interface gives the library the information it needs.
Reading a signature
Methods in the framework use generics, and many use wildcards. Here is a real example:
boolean addAll(Collection<? extends E> c);You can read each part now. E is this collection’s element type
(9.3). ? extends E accepts a collection of E or a
subtype of E (9.4). We use extends because this
parameter produces elements for us to read. That is the PECS rule.
This phase applies the array, interface, and generics ideas you have already learned.
What it costs
There is a fair amount to learn: several classes, several interfaces, and the tradeoffs behind each choice. That is why this phase has eight sections.
The wrong choice may not cause an error. Use a LinkedList where an ArrayList would suit the
work better, and the program still runs. A loop that should take a second might take a minute,
with no warning from Java.
Generic collections hold objects, not primitives. List<int> does not compile because generic
type arguments must be reference types, a restriction connected to type erasure from
Section 9.3. Java therefore boxes each number, with the
memory and time cost you measured in Section 7.3.
Interfaces promise behaviour, not speed. List says nothing about how long get(i) takes, so a
method that receives a List cannot assume indexing is cheap. RandomAccess communicates that
extra fact when it matters.
Start with ArrayList, measure your program, and change the implementation only if the profile
points elsewhere. Shared interfaces keep that change local instead of turning it into a rewrite.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
Why does Java have several collection types instead of one that does every job?
Show the answer
Because the way data is arranged always creates a tradeoff.
An unsorted array lets you add at the end quickly, but finding its largest value means checking every element.
A sorted array keeps the largest value at the end, but inserting a new value means shifting everything that comes after it.
Each data structure makes some operations cheap and makes others cost more. Java provides several choices so you can choose the tradeoff that suits the job.
`Map` is not a `Collection`. Why not, and what would go wrong if it were?
Show the answer
Because
Collectionis built around one thing per slot. Its central method isadd(E e), taking a single element.A
Mapstores pairs. Its central method isput(K key, V value), taking two. Anadd(E e)method takes one element, so it cannot represent that operation.You can check this yourself:
HashMapimplementsMap,CloneableandSerializable, and nothing else. It is notIterableeither, which is why you cannot writefor (x : myMap).To loop over a map you ask it for a collection first:
keySet(),values()orentrySet(). Those are collections.`List<Integer> l = new ArrayList<>();` Why declare the variable as `List` rather than `ArrayList`?
Show the answer
It leaves room to change the implementation later.
The variable's type decides what you are allowed to call. Declare it as
Listand you can call onlyListmethods, which every list implementation provides. If you switch to aLinkedList, code that uses only those methods still compiles.Declare it as
ArrayListand you might use an ArrayList-only method without noticing, and then the swap breaks compilation in places you did not expect.This is polymorphism from Section 7.2 in practice. A useful habit is to declare the interface and create the class.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises85 pointsabout 100 minutes
Prove the Trade Is Unavoidable
ex-10-1-aTwo arrangements of the same numbers. Measure both operations on both.
The table you produce is the point. One column wins the first row, the other wins the second, and neither wins both.
Then answer the last question honestly. To be fast at both you would need something that is neither a plain array nor a sorted array, and whatever you invent will be slow at a third thing.
That is not a limitation of Java. It is the shape of the problem, and it is why the next seven sections exist.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class TheTrade {
static int findMaxUnsorted(int[] a, int size)
static int findMaxSorted(int[] a, int size)
static int[] insertUnsorted(int[] a, int size, int v)
static int[] insertSorted(int[] a, int size, int v)
}size is how many slots are in use, which is not the same as a.length. Both insert methods return the array to use next, because a full array has to be replaced by a bigger one.
What your program must do
- Implement all four methods so the tests pass
- findMaxSorted must read one slot. If it loops, you have not used the fact that it is sorted
- Both insert methods must grow the array when every slot is used
- In main, time both operations on both arrangements and print a table
- Say which arrangement wins each row, and what you would give up to win both
import java.util.*;
public class TheTrade {
// Walk every used slot and return the largest.
static int findMaxUnsorted(int[] a, int size) {
return 0; // TODO
}
// The array is already sorted. One read, no loop.
static int findMaxSorted(int[] a, int size) {
return 0; // TODO
}
// Put v at the end. Grow the array first if every slot is used.
static int[] insertUnsorted(int[] a, int size, int v) {
return a; // TODO
}
// Put v where it belongs, shifting the rest right. Grow first if full.
static int[] insertSorted(int[] a, int size, int v) {
return a; // TODO
}
public static void main(String[] args) {
// TODO: build both arrangements with 100_000 numbers
// TODO: time 10_000 max-lookups on each
// TODO: time 10_000 insertions into each
// TODO: print the table
}
}
Hint 1
a[size - 1], one array read.Hint 2
a = Arrays.copyOf(a, a.length * 2). Do it first, then insert, then return the array you ended up with.Hint 3almost the answer
a[at] <= v to find the spot, then copy backwards from size down to at + 1 so nothing is overwritten before it is moved.Swap the Implementation
ex-10-1-bWrite one method that accepts every container in Java.
Then do the thing that locks you in. Declare a variable as ArrayList, call a method only ArrayList has, and then try to swap in a LinkedList.
Note where the error appears. It is not on the line you changed. It is on the line that used the extra method, which might be hundreds of lines away or in another file.
That distance is the argument for the rule. Write it down in your own words when you are done.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Swap {
static String report(Collection<String> items)
static Collection<String> largest(Collection<Collection<String>> groups)
}report returns the size and the word items, like "2 items". largest returns whichever group holds the most. The parameter types are the exercise: pick anything narrower and the tests will not compile.
What your program must do
- Make report work for all four containers without changing it once
- Implement largest so it compares groups of different classes
- Declare a variable as ArrayList, call an ArrayList-only method, then try to swap in a LinkedList
- Write down which line the error appeared on, and which line you actually changed
- State the rule in one line
import java.util.*;
public class Swap {
// One method that accepts EVERY kind of container: List, Set and Deque alike.
static String report(Collection<String> items) {
return ""; // TODO
}
// Whichever group holds the most items.
static Collection<String> largest(Collection<Collection<String>> groups) {
return null; // TODO
}
public static void main(String[] args) {
ArrayList<String> a = new ArrayList<>(List.of("Atlas", "Beacon"));
LinkedList<String> l = new LinkedList<>(List.of("Cipher"));
HashSet<String> h = new HashSet<>(List.of("Drift"));
ArrayDeque<String> d = new ArrayDeque<>(List.of("Ember"));
// TODO: pass all four to report()
// TODO: declare a variable as ArrayList, call ensureCapacity() on it,
// then change the declared type to LinkedList. Record where the
// error appears, and how far it is from the line you changed.
}
}
Hint 1
Collection. Take that as the parameter type and every one of them fits.Hint 2
ensureCapacity exists on ArrayList and not on List. Once you call it, changing the variable's type to LinkedList stops compiling.Hint 3almost the answer
List<String> x = new ArrayList<>(); The same applies to parameters and to return types, which is why largest returns a Collection and not an ArrayList.Map Is Not a Collection
ex-10-1-cThe hierarchy diagram says Map sits outside Collection. Prove it rather than trust it.
Print the interfaces each class implements and compare them. HashMap’s list is much shorter than ArrayList’s, and Iterable is missing from it.
Then try a for-each over a map and read the error. Then loop over it three ways that work.
Finish with the design reason, stated in terms of method signatures. One method takes one argument and the other takes two. That is the whole answer, and it is more satisfying than “because Java says so”.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class NotACollection {
static boolean mapIsCollection()
static boolean listIsCollection()
static boolean mapIsIterable()
static List<String> viaKeySet(Map<String, Integer> m)
static List<Integer> viaValues(Map<String, Integer> m)
static List<String> viaEntrySet(Map<String, Integer> m)
}The three via methods each return a sorted list, so the answer does not depend on the order HashMap happens to use. viaEntrySet returns strings shaped like "Atlas=88".
What your program must do
- Answer the three hierarchy questions by asking the type system, not by typing true or false
- Print the interfaces of HashMap and ArrayList and compare the two lists
- Write a for-each over a Map, read the error, then comment it out
- Implement the three loops that do work
- Explain the design reason in terms of method signatures
import java.util.*;
public class NotACollection {
// Ask the type system rather than trusting the diagram.
static boolean mapIsCollection() { return true; /* TODO */ }
static boolean listIsCollection() { return false; /* TODO */ }
static boolean mapIsIterable() { return true; /* TODO */ }
// Three loops that DO work over a Map. Each returns a sorted list.
static List<String> viaKeySet(Map<String, Integer> m) {
return List.of(); // TODO
}
static List<Integer> viaValues(Map<String, Integer> m) {
return List.of(); // TODO
}
// Each entry becomes "key=value", for example "Atlas=88".
static List<String> viaEntrySet(Map<String, Integer> m) {
return List.of(); // TODO
}
public static void main(String[] args) {
System.out.println("Is Map a Collection? " + mapIsCollection());
System.out.println("Is List a Collection? " + listIsCollection());
System.out.println("Is Map Iterable? " + mapIsIterable());
// TODO: print every interface HashMap and ArrayList implement, and compare
// TODO: try to for-each over a Map directly. Read the error, then comment it out
}
}
Hint 1
Collection.class.isAssignableFrom(Map.class) asks the question directly. HashMap.class.getInterfaces() gives you the list to compare.Hint 2
keySet(), values() and entrySet(). All three return real collections, which is why for-each accepts them.Hint 3almost the answer
Collection.add(E e) takes one thing. Map.put(K k, V v) takes two. There is no honest way to fit a pair into a method built for one element.Registry, Rebuilt on Collections
ex-10-1-dGo back to the Registry you built with arrays in Phase IV and rebuild it on the framework.
The point is the list of what disappears. In Phase IV you tracked a size separately from the array length, checked for capacity before adding, wrote a grow-and-copy method, and shifted elements when removing.
All of that is now inside ArrayList, and you wrote it yourself in the Section 4.2 exercises, so you know exactly what is happening.
Finish by swapping to a LinkedList and confirming nothing else in your file changes. If something breaks, you declared a variable as the class instead of the interface.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class RegistryCollections {
static List<Unit> newRoster()
static boolean removeById(List<Unit> roster, int id)
static double averageReadiness(List<Unit> roster)
static Unit mostReady(List<Unit> roster)
static List<Unit> activeOnly(List<Unit> roster)
}Unit is the record already in the file. An empty roster averages 0.0 and has no most ready unit, so mostReady returns null. activeOnly returns a new list and leaves the roster untouched.
What your program must do
- Implement all five methods with no capacity handling anywhere
- Swap ArrayList for LinkedList inside newRoster and confirm the tests still pass
- Say why that swap changed nothing else in the file
- List what you deleted compared with the Phase IV array version
import java.util.*;
public class RegistryCollections {
record Unit(String name, int id, double readiness, boolean active) { }
// In Phase IV this was a Unit[] with a separate size counter, a capacity
// check before every add, a grow-and-copy method, and a shift loop on remove.
// None of that appears below. That absence is the exercise.
static List<Unit> newRoster() {
return null; // TODO
}
// true when a unit was actually removed.
static boolean removeById(List<Unit> roster, int id) {
return false; // TODO
}
// 0.0 for an empty roster.
static double averageReadiness(List<Unit> roster) {
return 0.0; // TODO
}
// null for an empty roster.
static Unit mostReady(List<Unit> roster) {
return null; // TODO
}
// A new list. The roster passed in must not change.
static List<Unit> activeOnly(List<Unit> roster) {
return null; // TODO
}
public static void main(String[] args) {
// TODO: build a roster, add units, report count, average and best
// TODO: remove one by id
// TODO: change newRoster to return a LinkedList and confirm nothing else moves
// TODO: list what you deleted compared with the Phase IV version
}
}
Hint 1
newRoster returns new ArrayList<>(), but its declared return type is List. That difference is what makes the later swap free.Hint 2
roster.removeIf(u -> u.id() == id) does it in one line and already returns the boolean you need. That is a lambda, and Phase XI explains it properly.Hint 3almost the answer
After the credits
Iterable sits at the top of the diagram, and it has exactly one method.
That method is why this line works for an ArrayList, a HashSet, and a TreeSet without any
changes:
for (String name : anything) { }Arrays also work with a for-each loop, but that is a separate rule in the Java language. You have used the loop since Section 4.1, where you learned that Java rewrites it behind the scenes. Section 10.2 looks at that rewrite closely.
You will also see why removing an element inside a for-each loop throws an exception, and how to remove it safely.
It is the smallest interface in the framework, and the one the other collection interfaces build on.
Threads you opened in this section
- The Collections Frameworkstream() was bolted onto Collection as a default method.11.3 - Streams, and When the Work Happens
- The Collections FrameworkNone of these are thread safe. Concurrent versions exist for a reason.Phase XIV. Concurrency
The Collections Framework will return in 10.2 - Iterable and the Iterator