Java,
Week 6
Programming Concepts using Java, Week 6: the benefits of indirection, the Collection interface and iterators, concrete collections (List, Set, Queue, Deque, PriorityQueue) and maps (HashMap, TreeMap, LinkedHashMap). Every Java example in these notes was really compiled and run.
Chapters
The Benefits of Indirection
Weeks 4 and 5 gave us interfaces and generics. Week 6 puts them to work in the library Java ships with: the Collections framework. This first lecture explains the idea underneath it all, indirection: putting an interface between the code that uses a data structure and the code that implements it.
1.1Abstract data types
add(E element)puts an element at the rear,E remove()takes one from the front,int size()says how many there are.
Behind that interface the implementer can choose a circular array or a linked list, and the public interface does not change.
The interface hides the choice, but the two are not equivalent. They differ in things the interface does not say:
| Aspect | Circular array | Linked list |
|---|---|---|
| Efficiency | better: one-time storage allocation | a node is allocated for every element |
| Flexibility | bounded size: it can get full | better: grows as long as memory lasts |
1.2Two implementations, built for real
The slides only show the headers ({ ... }), so here are working versions. A circular array stores the head index and a count, and the position of the next free slot wraps around with %:
public class CircularArrayQueue<E> implements Queue<E> {
private Object[] items;
private int head = 0;
private int count = 0;
public CircularArrayQueue(int capacity) {
items = new Object[capacity]; // one-time storage allocation
}
public void add(E element) {
if (count == items.length) {
throw new IllegalStateException("queue is full"); // bounded size
}
items[(head + count) % items.length] = element;
count++;
}
@SuppressWarnings("unchecked")
public E remove() {
if (count == 0) throw new IllegalStateException("queue is empty");
E e = (E) items[head];
items[head] = null;
head = (head + 1) % items.length;
count--;
return e;
}
public int size() { return count; }
}public class Main {
public static void main(String[] args) {
CircularArrayQueue<Integer> q = new CircularArrayQueue<Integer>(3);
q.add(1); q.add(2); q.add(3); // array is full
System.out.println(q.remove()); // head moves on
q.add(4); // the tail wraps around to slot 0
System.out.println(q.remove() + " " + q.remove() + " " + q.remove());
}
}1
2 3 4The array was full when the 4 arrived? No: remove() had freed slot 0 first, and the new element went around the end into it. That reuse is why it is called circular. The price is a fixed capacity: with all slots taken, add has nowhere to put the element.
1.3Users choose the implementation: the problem
If the two classes are separate (CircularArrayQueue<E> and LinkedListQueue<E>), the user decides by declaring variables of the concrete class:
public class Main {
public static void main(String[] args) {
CircularArrayQueue<String> q = new CircularArrayQueue<String>(3);
q = new LinkedListQueue<String>(); // the variable was declared with the concrete class
}
}Main.java:4: error: incompatible types: LinkedListQueue<String> cannot be converted to CircularArrayQueue<String>
q = new LinkedListQueue<String>(); // the variable was declared with the concrete class
^
1 errorThe declaration is welded to the class. Later we may need a queue of dates that can grow, and then we must change the declaration of dateq, every function header that takes it, every auxiliary variable, and so on.
1.4Adding indirection: a Queue interface
Instead, create a Queue<E> interface and let the concrete classes implement it. Variables are then declared with the interface type, and only the instantiation names a concrete class:
public interface Queue<E> {
void add(E element);
E remove();
int size();
}public class Main {
public static void main(String[] args) {
Queue<String> q = new LinkedListQueue<String>(); // declared by the interface
q.add("a");
q.add("b");
q.add("c");
System.out.println(q.remove() + q.remove() + " left: " + q.size());
}
}ab left: 1Now see the benefit. The first run uses the array queue with room for only three; the fifth add fails:
public class Main {
public static void main(String[] args) {
Queue<Integer> q = new CircularArrayQueue<Integer>(3); // the ONE line to change
for (int i = 1; i <= 5; i++) {
q.add(i);
}
System.out.println(q.size());
}
}Exception in thread "main" java.lang.IllegalStateException: queue is full
at CircularArrayQueue.add(CircularArrayQueue.java:12)
at Main.main(Main.java:5)To get a queue that can grow, change one line, the instantiation. Nothing else in the program is touched:
public class Main {
public static void main(String[] args) {
Queue<Integer> q = new LinkedListQueue<Integer>(); // changed: nothing else touched
for (int i = 1; i <= 5; i++) {
q.add(i);
}
System.out.println(q.size());
}
}5The same holds for functions. A method whose parameter is a Queue<String> accepts any implementation, today's and ones written next year:
public class Main {
static void fill(Queue<String> q) { // works for any implementation
q.add("x");
q.add("y");
System.out.println(q.getClass().getSimpleName() + " has " + q.size());
}
public static void main(String[] args) {
fill(new CircularArrayQueue<String>(4));
fill(new LinkedListQueue<String>());
}
}CircularArrayQueue has 2
LinkedListQueue has 2What the compiler checks
An interface cannot be instantiated (Week 4), and a class that claims to implement it must provide every method:
public class Main {
public static void main(String[] args) {
Queue<String> q = new Queue<String>();
}
}Main.java:3: error: Queue is abstract; cannot be instantiated
Queue<String> q = new Queue<String>();
^
1 errorpublic class Broken<E> implements Queue<E> {
public void add(E element) { }
public E remove() { return null; }
}Broken.java:1: error: Broken is not abstract and does not override abstract method size() in Queue
public class Broken<E> implements Queue<E> {
^
1 errorjava.util.Queue. The Queue above is the lecture's own interface in the default package; the examples in the next lectures use the real one with an explicit import java.util.*. Do not mix the two in one file.1.5Indirection in real life
The lecture's analogy: an organisation gives senior staff an office car.
| Step | Arrangement | Indirection? |
|---|---|---|
| 1 | Each official has an assigned car. What if it breaks down? | none: concrete |
| 2 | A pool of office cars: use any that is available. | a level of indirection |
| 3 | Do not want to maintain a pool? Contract a taxi service. | another level |
| 4 | Do not want to negotiate tenders? Reimburse the taxi bills. | another level |
"All problems in computer science can be solved by another level of indirection." (Butler Lampson, Turing Award 1992.) The usual tail of the joke: ...except the problem of too many levels of indirection (extra).
1.6Lecture summary
- Use interfaces to flexibly choose between multiple concrete implementations.
- Interfaces add a level of indirection.
- Declare with the interface type; only the instantiation names the concrete class.
Collection
Most languages ship built-in collective types: arrays, lists, dictionaries. Early Java had a pile of such classes, each a separate concrete choice. This lecture shows the cure: organise them by functionality as a hierarchy of abstract interfaces and concrete implementations, with indirection in between.
2.1Why a hierarchy
Java originally had Vector, Stack, Hashtable, BitSet ... You chose the one you needed, and changing the choice meant many updates (the Lecture 1 problem again). These legacy classes still exist (extra); new code uses the framework below.
2.2The Collection interface
Collection interface abstracts the properties of grouped data: arrays, lists, sets, ... but not key-value structures like dictionaries (those are maps, Lecture 4).add(): add to the collectioniterator(): get an object that implementsIterator; use it to loop through the elements
The Iterator<E> of Week 4, now in its real form: next(), hasNext(), remove() (the names are hasNext/next, not Week 4's teaching names has_next/get_next):
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class Main {
public static void main(String[] args) {
Collection<String> cstr = new ArrayList<String>();
cstr.add("one");
cstr.add("two");
cstr.add("three");
Iterator<String> iter = cstr.iterator();
while (iter.hasNext()) {
String element = iter.next();
System.out.println(element.toUpperCase());
}
}
}ONE
TWO
THREEJava later added the for-each loop, which implicitly creates an iterator and runs through it. It is the same loop, shorter:
import java.util.ArrayList;
import java.util.Collection;
public class Main {
public static void main(String[] args) {
Collection<String> cstr = new ArrayList<String>();
cstr.add("one");
cstr.add("two");
for (String element : cstr) { // the compiler builds the iterator for us
System.out.println(element);
}
}
}one
two2.3Generic functions on collections
Because ArrayList, HashSet and the rest are all Collections, one generic function serves them all (Week 5's type parameters at work):
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
public class Main {
public static <E> boolean contains(Collection<E> c, Object obj) {
for (E element : c) {
if (element.equals(obj)) { // every E is an Object: equals() always exists
return true;
}
}
return false;
}
public static void main(String[] args) {
Collection<String> list = new ArrayList<String>(List.of("a", "b", "c"));
Collection<Integer> set = new HashSet<Integer>(List.of(10, 20, 30));
System.out.println(contains(list, "b"));
System.out.println(contains(set, 25));
System.out.println(contains(set, 20));
}
}true
false
trueThe lecture asks: how does element.equals(obj) work? E is an arbitrary type variable, so what guarantees an equals method? Every class inherits equals from Object (Week 3), and a type variable's upper bound is Object (Week 5). So the call is always legal. Whether it compares the way you want depends on the class overriding equals.
2.4Removing elements: Iterator.remove()
Iterator.remove() removes the element that was last returned by next(). Picture the iterator as standing between elements, never on one:
- When the iterator starts, it stands before the first element: "basically over nothing".
- The first
next()passes the first element and stands right before the second. - Now
remove()deletes the first element (the last onenext()accessed). - Calling
remove()again immediately is an error: no new element was passed.
element.remove(), calling remove on the element (a String), which has no such method. It must be the iterator's iter.remove():import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class Main {
public static void main(String[] args) {
Collection<String> cstr = new ArrayList<String>();
Iterator<String> iter = cstr.iterator();
while (iter.hasNext()) {
String element = iter.next();
if (element.length() == 4) {
element.remove(); // the slide's line
}
}
}
}Main.java:12: error: cannot find symbol
element.remove(); // the slide's line
^
symbol: method remove()
location: variable element of type String
1 errorimport java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
Collection<String> cstr = new ArrayList<String>(List.of("tea", "coffee", "milk", "cola"));
Iterator<String> iter = cstr.iterator();
while (iter.hasNext()) {
String element = iter.next();
if (element.length() == 4) { // delete the element if it has some property
iter.remove(); // removes the element next() just returned
}
}
System.out.println(cstr);
}
}[tea, coffee]The two failure cases, run for real. Removing twice in a row, and removing before any next(), both throw IllegalStateException:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<String>(List.of("a", "b", "c"));
Iterator<String> iter = names.iterator();
iter.next();
iter.remove();
iter.remove(); // no next() in between
}
}Exception in thread "main" java.lang.IllegalStateException
at java.base/java.util.ArrayList$Itr.remove(ArrayList.java:1062)
at Main.main(Main.java:11)import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<String>(List.of("a", "b", "c"));
Iterator<String> iter = names.iterator();
iter.remove(); // standing before the first element
}
}Exception in thread "main" java.lang.IllegalStateException
at java.base/java.util.ArrayList$Itr.remove(ArrayList.java:1062)
at Main.main(Main.java:9)To remove consecutive elements, interleave a next(). To remove the very first element, access it first:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<String>(List.of("a", "b", "c", "d"));
Iterator<String> iter = names.iterator();
iter.next(); // a
iter.remove();
iter.next(); // b: interleave a next() before the second remove()
iter.remove();
System.out.println(names);
}
}[c, d]ConcurrentModificationException:import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<String>(List.of("a", "b", "c"));
for (String s : names) {
if (s.equals("a")) names.remove(s); // changes the list behind the iterator's back
}
}
}Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1095)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1049)
at Main.main(Main.java:7)2.5The rest of Collection
Collection defines a much larger set of abstract methods than add and iterator:
| Method | What it does |
|---|---|
int size() | number of elements |
boolean isEmpty() | true when there are none |
boolean contains(Object obj) | is an equal element present? |
boolean containsAll(Collection<?> c) | is every element of c present? |
boolean addAll(Collection<? extends E> from) | add elements from a compatible collection |
boolean remove(Object obj) | remove one matching element |
boolean removeAll(Collection<?> c) | remove every element that is present in c |
boolean equals(Object other) | compare with another collection |
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class Main {
public static void main(String[] args) {
Collection<String> c = new ArrayList<String>();
System.out.println(c.isEmpty());
c.add("x"); c.add("y"); c.add("z");
System.out.println(c.size() + " " + c.contains("y"));
System.out.println(c.containsAll(List.of("x", "z")));
c.addAll(List.of("p", "q")); // copy in another collection
System.out.println(c);
c.remove("y"); // Collection.remove(Object): one value
c.removeAll(List.of("p", "q")); // remove every element that is in the argument
System.out.println(c);
}
}true
3 true
true
[x, y, z, p, q]
[x, z]int size() boolean isEmpty(); is missing a semicolon after size(). As written it does not compile:public interface Main {
int size() boolean isEmpty();
}Main.java:2: error: ';' expected
int size() boolean isEmpty();
^
1 errorTwo points the lecture makes. addAll(from) takes a Collection<? extends E>: the wildcard from Week 5 lets you copy a list of Integer into a list of Number, but not the reverse. And Collection.remove(Object) is quite different from Iterator.remove(): one looks for a value, the other deletes the last-returned element.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> ints = List.of(1, 2);
List<Number> numbers = new ArrayList<Number>();
numbers.addAll(ints); // addAll(Collection<? extends E>): Integers are Numbers
System.out.println(numbers);
List<Integer> target = new ArrayList<Integer>();
target.addAll(numbers); // the wrong way round
}
}Main.java:12: error: incompatible types: List<Number> cannot be converted to Collection<? extends Integer>
target.addAll(numbers); // the wrong way round
^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error2.6The AbstractCollection class
To implement Collection a class must write all those methods, which is tedious. The lecture's cure is default implementations. They live not in the interface but in an abstract class, AbstractCollection<E> implements Collection<E>, that builds everything it can on top of iterator() and size(), for example:
public abstract Iterator<E> iterator(); // abstract: depends on the representation
public boolean contains(Object obj) { // default: built on iterator()
for (E element : this) {
if (element.equals(obj)) return true;
}
return false;
}A concrete class now extends AbstractCollection, defines iterator() for its own representation (and size()), and may override contains() if it can do better. Here a tiny array-backed Bag writes just those two and gets contains, containsAll, isEmpty, toString and for-each free:
import java.util.AbstractCollection;
import java.util.Iterator;
public class Bag<E> extends AbstractCollection<E> {
private Object[] items = new Object[10];
private int n = 0;
public void put(E e) { items[n++] = e; }
public int size() { return n; }
public Iterator<E> iterator() { // the only representation-specific method besides size()
return new Iterator<E>() {
private int pos = 0;
public boolean hasNext() { return pos < n; }
@SuppressWarnings("unchecked")
public E next() { return (E) items[pos++]; }
};
}
}import java.util.List;
public class Main {
public static void main(String[] args) {
Bag<String> bag = new Bag<String>();
bag.put("red");
bag.put("green");
bag.put("blue");
System.out.println(bag.contains("green")); // inherited: built on iterator()
System.out.println(bag.containsAll(List.of("red", "blue")));
System.out.println(bag.isEmpty()); // inherited: built on size()
System.out.println(bag); // inherited toString()
for (String s : bag) { // for-each works: it is Iterable
System.out.print(s.length() + " ");
}
System.out.println();
}
}true
true
false
[red, green, blue]
3 5 4 The defaults only go as far as the missing pieces allow. This Bag never overrode add, so the inherited add refuses:
public class Main {
public static void main(String[] args) {
Bag<String> bag = new Bag<String>();
bag.add("x"); // add() was never overridden
}
}Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.AbstractCollection.add(AbstractCollection.java:253)
at Main.main(Main.java:4)add and Iterator.remove are optional; a collection that does not support one throws UnsupportedOperationException. The same happens for a plain iterator (its default remove says "unsupported") and for immutable collections like List.of:import java.util.Iterator;
public class Main {
public static void main(String[] args) {
Iterator<String> it = new Iterator<String>() {
int i = 0;
public boolean hasNext() { return i < 2; }
public String next() { return "v" + i++; }
};
it.next();
it.remove(); // remove() is optional: the default says unsupported
}
}Exception in thread "main" java.lang.UnsupportedOperationException: remove
at java.base/java.util.Iterator.remove(Iterator.java:102)
at Main.main(Main.java:11)import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> fixed = List.of("a", "b");
fixed.add("c");
}
}Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:142)
at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(ImmutableCollections.java:147)
at Main.main(Main.java:6)default methods (Week 4), and Iterator does; the collection classes still use AbstractCollection because it can also hold state and a constructor, which an interface cannot.2.7Lecture summary
- The
Collectioninterface captures abstract properties of collections: add an element, create an iterator, ... - Use the for-each loop to avoid the explicit iterator.
- Write generic functions that operate on collections.
- Collection defines many more abstract functions; implementing each is tedious, so
AbstractCollectiongives defaults. - Concrete collections extend
AbstractCollection.
Concrete Collections
A bare Collection says nothing about order, duplicates, or how elements may be added and removed. Sub-interfaces capture exactly those extra properties: List, Set and Queue. Each has an abstract class with default code and concrete classes underneath.
| Question | Interface that answers it |
|---|---|
| Are the elements ordered? | List |
| Are duplicates allowed? | Set (no duplicates) |
| Are there constraints on adding and removing? | Queue, Deque |
3.1The List interface
An ordered collection can be accessed in two ways: through an iterator or by position (random access). List adds the positional methods:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("b"); // Collection.add: append
list.add("c");
list.add(0, "a"); // List.add(index, e): insert at a position
System.out.println(list);
System.out.println(list.get(1)); // by position
System.out.println(list.set(1, "B")); // set returns the old element
String removed = list.remove(2); // remove(int) returns the removed element
System.out.println(removed + " " + list);
}
}[a, b, c]
b
b
c [a, B]void remove(int index). The real method is E remove(int index): it returns the element it removed, which the example above uses. Likewise ListIterator.previous() is listed as void previous() but returns the previous element.ListIterator
ListIteratorextendsIterator.void add(E element): insert an element before the current position.E previous()goes to the previous element;boolean hasPrevious()checks that going back is legal.
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<String>(List.of("a", "c", "d"));
ListIterator<String> it = list.listIterator();
it.next(); // passes "a"
it.add("b"); // inserted BEFORE the current position
System.out.println(list);
it.next(); // passes "c"
System.out.println(it.hasPrevious()); // legal to go backwards?
String back = it.previous(); // previous() returns the element, not void
System.out.println(back);
it.set("C"); // replaces what previous() just returned
System.out.println(list);
}
}[a, b, c, d]
true
c
[a, b, C, d]3.2Random access: not equally cheap
In an array the location of element i can be computed. In a linked list you must start at the beginning and traverse i links.
The tagging interface RandomAccess (it has no methods) tells you whether a List supports fast random access, so you can choose the algorithm:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.RandomAccess;
public class Main {
static int sum(List<Integer> c) {
int total = 0;
if (c instanceof RandomAccess) { // a tagging interface: no methods
for (int i = 0; i < c.size(); i++) { // random access algorithm
total += c.get(i);
}
} else {
for (int x : c) { // sequential access algorithm
total += x;
}
}
return total;
}
public static void main(String[] args) {
List<Integer> a = new ArrayList<Integer>(List.of(1, 2, 3));
List<Integer> l = new LinkedList<Integer>(List.of(1, 2, 3));
System.out.println((a instanceof RandomAccess) + " " + (l instanceof RandomAccess));
System.out.println(sum(a) + " " + sum(l));
}
}true false
6 6The consequence for the loop everyone writes. For a LinkedList the methods from AbstractList are still available (get(i) works), but the loop with get(i) starts a fresh scan from the start to element i in every iteration. This run compares them (the booleans, not the raw times, since times vary):
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class Main {
static long viaGet(List<Integer> list) {
long t = System.nanoTime();
long s = 0;
for (int i = 0; i < list.size(); ++i) s += list.get(i); // a fresh scan to i each time
return System.nanoTime() - t;
}
static long viaIterator(List<Integer> list) {
long t = System.nanoTime();
long s = 0;
for (int x : list) s += x; // one pass
return System.nanoTime() - t;
}
public static void main(String[] args) {
List<Integer> linked = new LinkedList<Integer>();
List<Integer> array = new ArrayList<Integer>();
for (int i = 0; i < 30000; i++) { linked.add(i); array.add(i); }
long linkedGet = viaGet(linked), linkedIter = viaIterator(linked), arrayGet = viaGet(array);
System.out.println("LinkedList: get(i) loop slower than iterator? " + (linkedGet > 20 * linkedIter));
System.out.println("ArrayList get(i) faster than LinkedList get(i)? " + (arrayGet * 20 < linkedGet));
}
}LinkedList: get(i) loop slower than iterator? true
ArrayList get(i) faster than LinkedList get(i)? trueUse for-each (an iterator) on any list, or get(i) only when the list is a RandomAccess one. The loop for (i...) list.get(i) on a linked list is quadratic.
3.3AbstractList and the concrete lists
AbstractCollectionis a usable version ofCollection; correspondinglyAbstractListextendsAbstractCollectionand inherits its defaults.AbstractSequentialListextendsAbstractList: a further sub-class for lists without random access.LinkedList<E>extendsAbstractSequentialList: the usual flexible linked list, efficient to add and remove at arbitrary positions, no random access.ArrayList<E>extendsAbstractList: a flexible-size array that supports random access.
import java.util.AbstractCollection;
import java.util.AbstractList;
import java.util.AbstractSequentialList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
public class Main {
public static void main(String[] args) {
System.out.println(AbstractCollection.class.isAssignableFrom(AbstractList.class));
System.out.println(AbstractList.class.isAssignableFrom(AbstractSequentialList.class));
System.out.println(AbstractSequentialList.class.isAssignableFrom(LinkedList.class));
System.out.println(AbstractList.class.isAssignableFrom(ArrayList.class));
System.out.println(AbstractSequentialList.class.isAssignableFrom(ArrayList.class));
System.out.println(Collection.class.isAssignableFrom(java.util.HashMap.class));
}
}true
true
true
true
false
falseThe six lines say: AbstractList is a AbstractCollection (true), AbstractSequentialList is an AbstractList (true), a LinkedList is an AbstractSequentialList (true), an ArrayList is an AbstractList (true) but not an AbstractSequentialList (false); and a HashMap is not a Collection (false), which is Lecture 4.
AbstractList "the AbstractList interface". It is an abstract class, like AbstractCollection.Two versions of add()
add()from Collection appends to the end and returns a boolean: "did the add update the collection?"- A set may not change (a duplicate); a list always accepts.
add()from ListIterator inserts before the current position and returns void.
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
Set<String> set = new HashSet<String>();
System.out.println(list.add("x")); // Collection.add returns boolean: did the collection change?
System.out.println(list.add("x")); // a list always accepts
System.out.println(set.add("x"));
System.out.println(set.add("x")); // a set refuses a duplicate: false
ListIterator<String> it = list.listIterator();
it.add("y"); // ListIterator.add returns void
System.out.println(list);
}
}true
true
true
false
[y, x, x]import java.util.ArrayList;
import java.util.ListIterator;
public class Main {
public static void main(String[] args) {
ListIterator<String> it = new ArrayList<String>().listIterator();
boolean b = it.add("y");
}
}Main.java:7: error: incompatible types: void cannot be converted to boolean
boolean b = it.add("y");
^
1 error3.4The Set interface
A set is a collection without duplicates. The Set interface has the same methods as Collection but a more constrained behaviour:
add()has no effect and returns false if the element already exists.equals()is true if the contents match, disregarding order.- Use a
Setto enforce "no duplicates" in your data.
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
public class Main {
public static void main(String[] args) {
Set<String> hs = new HashSet<String>();
hs.add("banana"); hs.add("apple"); hs.add("cherry");
System.out.println(hs.add("apple") + " " + hs.size()); // duplicate: no effect, false
Set<String> ts = new TreeSet<String>(hs); // same contents, sorted
System.out.println(ts);
System.out.println(hs.equals(ts)); // equal contents, order ignored
System.out.println(hs); // HashSet order: unspecified
}
}false 3
[apple, banana, cherry]
true
[banana, apple, cherry]Set implementations are designed for efficient membership tests. A list must scan a sequence to find an element. A set instead maps the value to its position (a hash function) or arranges values in a 2D structure (a balanced search tree). As usual, concrete sets extend AbstractSet, which extends AbstractCollection.
- Underlying storage is an array; map value
vto positionh(v). - If
h(v)is free, storevthere. Otherwise a collision: there are different strategies to handle it. - Membership is fast: look at position
h(v). - Unordered, yet
iterator()visits every element exactly once, in unspecified order.
- Values are ordered: it maintains a sorted collection.
- The iterator visits elements in sorted order.
- Insertion is more complex than in a hash table: time O(log n) for n elements.
Hashing is only as good as equals() and hashCode()
The slides stop at "map a value to a position". The position comes from hashCode(), and the contract (Week 3) is: equal objects must have equal hash codes. A class that overrides equals but not hashCode breaks the set silently (extra):
public class Point {
private final int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
// hashCode() NOT overridden
}import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<Point> set = new HashSet<Point>();
set.add(new Point(1, 2));
set.add(new Point(1, 2)); // equal to the first one
System.out.println(set.size());
System.out.println(set.contains(new Point(1, 2)));
}
}2
falseThe two equal points hashed to different slots, so the set never compared them. Adding hashCode() fixes it:
public class Point {
private final int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
@Override
public int hashCode() { return 31 * x + y; } // equal points give equal hashes
}1
trueA TreeSet needs a different capability: an ordering. Elements must be Comparable (Week 4's idea), or you pass a Comparator:
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
public class Main {
public static void main(String[] args) {
Set<Job> plain = new TreeSet<Job>();
plain.add(new Job("print", 2)); // no way to order a Job
}
}Exception in thread "main" java.lang.ClassCastException: class Job cannot be cast to class java.lang.Comparable (Job is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')
at java.base/java.util.TreeMap.compare(TreeMap.java:1604)
at java.base/java.util.TreeMap.addEntryToEmptyMap(TreeMap.java:811)
at java.base/java.util.TreeMap.put(TreeMap.java:820)
at java.base/java.util.TreeMap.put(TreeMap.java:569)
at java.base/java.util.TreeSet.add(TreeSet.java:259)
at Main.main(Main.java:8)import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
public class Main {
public static void main(String[] args) {
Set<Job> byPriority = new TreeSet<Job>(Comparator.comparingInt(j -> j.priority));
byPriority.add(new Job("print", 2));
byPriority.add(new Job("email", 1));
byPriority.add(new Job("backup", 3));
System.out.println(byPriority); // visited in sorted order
}
}[email, print, backup]3.5The Queue interface
A queue is ordered: remove from the front, insert at the rear. Queue supports:
| Purpose | Strict (flags an error) | Gentle (returns false / null) |
|---|---|---|
| insert at the rear | boolean add(E e) | boolean offer(E e) |
| remove the front | E remove() | E poll() |
| inspect the head, no update | E element() | E peek() |
"If the queue is full, add() flags an error; if empty, remove() flags an error." The gentle versions return false or null instead. The empty-queue case, run for real:
import java.util.ArrayDeque;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
Queue<String> q = new ArrayDeque<String>();
System.out.println(q.offer("a") + " " + q.offer("b"));
System.out.println(q.peek() + " " + q.element()); // inspect the head, no update
System.out.println(q.poll() + " " + q.remove()); // remove from the front
System.out.println(q.poll() + " " + q.peek()); // empty: null, not an exception
q.remove(); // empty: this one throws
}
}true true
a a
a b
null null
Exception in thread "main" java.util.NoSuchElementException
at java.base/java.util.ArrayDeque.removeFirst(ArrayDeque.java:361)
at java.base/java.util.ArrayDeque.remove(ArrayDeque.java:522)
at Main.main(Main.java:11)The full-queue case needs a bounded queue (an ArrayDeque just grows). ArrayBlockingQueue has a fixed capacity:
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
public class Main {
public static void main(String[] args) {
Queue<String> q = new ArrayBlockingQueue<String>(2); // a bounded queue
q.add("a");
q.add("b");
System.out.println(q.offer("c")); // gentle: false
q.add("c"); // strict: error
}
}false
Exception in thread "main" java.lang.IllegalStateException: Queue full
at java.base/java.util.AbstractQueue.add(AbstractQueue.java:98)
at java.base/java.util.concurrent.ArrayBlockingQueue.add(ArrayBlockingQueue.java:329)
at Main.main(Main.java:10)Deque: a double-ended queue
addFirst, addLast, offerFirst, offerLastpollFirst, pollLast, getFirst, getLast, peekFirst, peekLast
import java.util.ArrayDeque;
import java.util.Deque;
public class Main {
public static void main(String[] args) {
Deque<Integer> dq = new ArrayDeque<Integer>();
dq.addFirst(2);
dq.addLast(3);
dq.offerFirst(1);
System.out.println(dq);
System.out.println(dq.getFirst() + " " + dq.peekLast());
System.out.println(dq.pollFirst() + " " + dq.pollLast() + " " + dq);
dq.push(7); // a Deque is also a stack
dq.push(8);
System.out.println(dq.pop() + " " + dq.pop());
}
}[1, 2, 3]
1 3
1 3 [2]
8 7boolean addFirst(E) and boolean addLast(E). They return void; the offer... versions are the ones that return boolean:import java.util.ArrayDeque;
import java.util.Deque;
public class Main {
public static void main(String[] args) {
Deque<Integer> dq = new ArrayDeque<Integer>();
boolean b = dq.addFirst(1); // the slide says boolean
}
}Main.java:7: error: incompatible types: void cannot be converted to boolean
boolean b = dq.addFirst(1); // the slide says boolean
^
1 errorPriorityQueue and the concrete queues
PriorityQueue:remove()returns the highest priority item.LinkedListimplements Queue (it is a list and a queue).ArrayDeque: a circular array deque (Lecture 1's idea).
import java.util.Collections;
import java.util.PriorityQueue;
public class Main {
public static void main(String[] args) {
PriorityQueue<Integer> pq = new PriorityQueue<Integer>();
pq.add(5); pq.add(1); pq.add(4); pq.add(2); pq.add(3);
System.out.println(pq); // internal heap order, NOT sorted
System.out.print("remove(): ");
while (!pq.isEmpty()) System.out.print(pq.remove() + " "); // smallest first
System.out.println();
PriorityQueue<Integer> max = new PriorityQueue<Integer>(Collections.reverseOrder());
max.add(5); max.add(1); max.add(4);
System.out.println(max.remove()); // now the largest is "highest priority"
}
}[1, 2, 4, 5, 3]
remove(): 1 2 3 4 5
5With natural ordering "highest priority" means the smallest value. Printing a PriorityQueue shows the internal heap order, which is not sorted; only remove() gives sorted output. A Comparator reverses it.
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
LinkedList<String> ll = new LinkedList<String>();
Queue<String> asQueue = ll; // a LinkedList is a Queue ...
List<String> asList = ll; // ... and a List
asQueue.offer("first");
asQueue.offer("second");
System.out.println(asList.get(1));
System.out.println(asQueue.poll());
}
}second
first3.6Lecture summary
- Different kinds of collection are specified by sub-interfaces:
List,Set,Queue. Listallows random access and a more functionalListIterator.Setforbids duplicates;Queuerestricts add and remove.- Each interface has a version under
AbstractCollection; concrete classes extendAbstractList,AbstractSet,AbstractQueue.
Maps
Collections group values. A map stores key-value pairs, like a dictionary: look up a value by its key. Java keeps maps in their own interface, Map<K,V>, outside the Collection hierarchy.
import java.util.Collection;
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
Collection<String> c = new HashMap<String, Integer>();
}
}Main.java:6: error: incompatible types: HashMap<String,Integer> cannot be converted to Collection<String>
Collection<String> c = new HashMap<String, Integer>();
^
1 error4.1The Map interface
- Two type parameters:
Kfor keys,Vfor values. V get(Object key)fetches the value for a key;V put(K key, V value)updates the value for a key.- The keys form a set: only one entry per key.
- Putting a fresh value under an existing key overwrites the old value, and
putreturns the previous value (ornull).
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> ages = new HashMap<String, Integer>();
System.out.println(ages.put("Asha", 21)); // no earlier value: null
System.out.println(ages.put("Asha", 22)); // returns the PREVIOUS value; overwrites
ages.put("Ravi", 30);
System.out.println(ages.get("Asha"));
System.out.println(ages.get("Meena")); // absent key: null
System.out.println(ages.size()); // one entry per key
System.out.println(ages.containsKey("Ravi") + " " + ages.containsValue(99));
}
}null
21
22
null
2
true falsecontainsKey: containsKey(Object key) and containsKey(Object value). The second must be containsValue. As written the interface cannot even compile (same signature twice):public interface MyMap<K, V> {
V get(Object key);
boolean containsKey(Object key);
boolean containsKey(Object value);
}MyMap.java:4: error: method containsKey(Object) is already defined in interface MyMap
boolean containsKey(Object value);
^
1 error4.2Updating a map: the initialisation problem
Key-value stores accumulate quantities: word frequencies in a text, total runs in a tournament. The problem: update the value if the key exists; otherwise create a new entry. A naive put(k, get(k) + 1) fails for a missing key because get returns null, and unboxing it (Week 5) throws:
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> scores = new HashMap<String, Integer>();
scores.put("bat", scores.get("bat") + 10); // get() is null: unboxing a null
}
}Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null
at Main.main(Main.java:7)Three tools solve it. The lecture's examples use an undeclared name bat; read it as a key, here the string "Asha".
| Tool | What it does |
|---|---|
getOrDefault(key, 0) | returns the value, or the default if the key is absent; then put(k, getOrDefault(k, 0) + n). |
putIfAbsent(key, 0) | initialises a missing key, then put(k, get(k) + n) is safe. |
merge(key, n, Integer::sum) | if the key is absent, store n; otherwise combine the current value with n using Integer::sum. |
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> scores = new HashMap<String, Integer>();
String bat = "Asha";
// 1. getOrDefault
scores.put(bat, scores.getOrDefault(bat, 0) + 10);
// 2. putIfAbsent, then update
scores.putIfAbsent(bat, 0);
scores.put(bat, scores.get(bat) + 5);
// 3. merge
scores.merge(bat, 20, Integer::sum);
scores.merge("Ravi", 20, Integer::sum); // missing key: initialised to 20
System.out.println(scores);
}
}{Ravi=20, Asha=35}The word-frequency problem from the lecture, with the first tool (the order printed is HashMap's, see 4.5):
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
String text = "to be or not to be";
Map<String, Integer> freq = new HashMap<String, Integer>();
for (String w : text.split(" ")) {
freq.put(w, freq.getOrDefault(w, 0) + 1); // initialise-or-update in one line
}
System.out.println(freq);
}
}{not=1, be=2, or=1, to=2}Integer::sum is a method reference (Java 8): shorthand for the function (a, b) -> Integer.sum(a, b), i.e. a + b. merge calls it only when the key already has a value.4.3Extracting keys and values: views
Set<K> keySet()Collection<V> values()Set<Map.Entry<K,V>> entrySet()
Keys form a Set (no repeats) while values form an arbitrary Collection (values may repeat). The key-value pairs form a set over a special type, Map.Entry. Java calls these views. Iterating a map means iterating a view; use entrySet() to get each key and its value without looking the map up again:
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
public class Main {
public static void main(String[] args) {
Map<String, Integer> staff = new TreeMap<String, Integer>();
staff.put("Asha", 50000);
staff.put("Ravi", 60000);
staff.put("Meena", 55000);
Set<String> keys = staff.keySet();
for (String key : keys) {
System.out.println("key " + key);
}
for (Map.Entry<String, Integer> entry : staff.entrySet()) { // key and value, no second lookup
String k = entry.getKey();
Integer v = entry.getValue();
System.out.println(k + " earns " + v);
}
System.out.println(staff.values());
}
}key Asha
key Meena
key Ravi
Asha earns 50000
Meena earns 55000
Ravi earns 60000
[50000, 55000, 60000]A view is live, not a copy: removing through keySet() or values() removes from the map, and Entry.setValue writes through (extra). A view that has no way to represent the change refuses it: a key set cannot add, because there is no value to pair with.
import java.util.Map;
import java.util.TreeMap;
public class Main {
public static void main(String[] args) {
Map<String, Integer> m = new TreeMap<String, Integer>();
m.put("a", 1); m.put("b", 2); m.put("c", 3);
m.keySet().remove("a"); // a view: removing here removes from the map
for (Map.Entry<String, Integer> e : m.entrySet()) {
e.setValue(e.getValue() * 10); // writes through to the map
}
System.out.println(m);
m.values().remove(30); // by value
System.out.println(m);
}
}{b=20, c=30}
{b=20}import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> m = new HashMap<String, Integer>();
m.keySet().add("k"); // a key set cannot add on its own
}
}Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.AbstractCollection.add(AbstractCollection.java:253)
at Main.main(Main.java:7)import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> m = new HashMap<String, Integer>();
m.put("a", 1); m.put("b", 2);
for (String k : m.keySet()) {
m.put(k + "x", 0); // adding a key while iterating
}
}
}Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1605)
at java.base/java.util.HashMap$KeyIterator.next(HashMap.java:1628)
at Main.main(Main.java:8)4.4Concrete maps
- Works like HashSet: a hash table stores the keys and values.
- No fixed order for the keys returned by
keySet().
- Like TreeSet: a balanced search tree stores the entries.
- An iterator over
keySet()processes the keys in sorted order.
- Remembers the order in which keys were inserted: the hash table entries are also connected as a (doubly) linked list.
- Iterators over
keySet()andvalues()go in insertion order. There is a similarLinkedHashSet.
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
public class Main {
static void fill(Map<String, Integer> m) {
m.put("pear", 1); m.put("apple", 2); m.put("fig", 3); m.put("banana", 4);
}
public static void main(String[] args) {
Map<String, Integer> hash = new HashMap<String, Integer>();
Map<String, Integer> tree = new TreeMap<String, Integer>();
Map<String, Integer> linked = new LinkedHashMap<String, Integer>();
fill(hash); fill(tree); fill(linked);
System.out.println("HashMap " + hash.keySet()); // no promise
System.out.println("TreeMap " + tree.keySet()); // sorted
System.out.println("LinkedHashMap " + linked.keySet()); // insertion order
}
}HashMap [banana, apple, pear, fig]
TreeMap [apple, banana, fig, pear]
LinkedHashMap [pear, apple, fig, banana]The HashMap line is what one real run printed; the language promises no order, and a different JDK may print another. The other two orders are guaranteed.
import java.util.TreeMap;
public class Main {
public static void main(String[] args) {
TreeMap<Integer, String> t = new TreeMap<Integer, String>();
t.put(30, "c"); t.put(10, "a"); t.put(20, "b");
System.out.println(t); // keys in sorted order
System.out.println(t.firstKey() + " " + t.lastKey());
System.out.println(t.headMap(30)); // keys strictly below 30
}
}{10=a, 20=b, 30=c}
10 30
{10=a, 20=b}Access order: a least-recently-used cache
A LinkedHashMap can use access order instead: every get() and put() moves that pair to the end of the list, so processing the entries goes in least recently used order. That is the basis of scheduling and caching.
import java.util.LinkedHashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> m = new LinkedHashMap<String, Integer>(16, 0.75f, true); // access order
m.put("a", 1); m.put("b", 2); m.put("c", 3);
System.out.println(m.keySet());
m.get("a"); // a get moves the pair to the end
System.out.println(m.keySet());
m.put("b", 20); // so does a put on an existing key
System.out.println(m.keySet()); // least recently used first
}
}[a, b, c]
[b, c, a]
[c, a, b]Extra: a complete cache needs one more step, evicting the oldest entry when full. Overriding removeEldestEntry does it:
import java.util.LinkedHashMap;
import java.util.Map;
public class LruCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LruCache(int capacity) {
super(16, 0.75f, true); // access order
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity; // drop the least recently used
}
}public class Main {
public static void main(String[] args) {
LruCache<String, Integer> cache = new LruCache<String, Integer>(3);
cache.put("a", 1); cache.put("b", 2); cache.put("c", 3);
cache.get("a"); // a is now the most recently used
cache.put("d", 4); // evicts b, the least recently used
System.out.println(cache.keySet());
}
}[c, a, d]Null keys, mutable keys (extra)
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
public class Main {
public static void main(String[] args) {
Map<String, Integer> h = new HashMap<String, Integer>();
h.put(null, 1); // HashMap allows one null key
System.out.println(h.get(null));
Map<String, Integer> t = new TreeMap<String, Integer>();
t.put(null, 1); // TreeMap must compare keys
}
}1
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because "k1" is null
at java.base/java.util.TreeMap.compare(TreeMap.java:1604)
at java.base/java.util.TreeMap.addEntryToEmptyMap(TreeMap.java:811)
at java.base/java.util.TreeMap.put(TreeMap.java:820)
at java.base/java.util.TreeMap.put(TreeMap.java:569)
at Main.main(Main.java:12)A map is only as reliable as its keys. If a key is changed after insertion so that its hash changes, the entry becomes unreachable:
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<Key, String> m = new HashMap<Key, String>();
Key k = new Key(1);
m.put(k, "value");
k.id = 2; // the key's hash changes after insertion
System.out.println(m.get(k));
System.out.println(m.get(new Key(1)));
System.out.println(m.size());
}
}null
null
1Use immutable keys (String, Integer ...). If you write your own key class, override both equals and hashCode and do not mutate the fields they use.
4.5Which collection when?
| Need | Use | Why |
|---|---|---|
| ordered, by position, mostly reading | ArrayList | random access |
| many inserts and removals in the middle | LinkedList | no shifting; but no random access |
| no duplicates, fast membership | HashSet | hash table |
| no duplicates, sorted | TreeSet | search tree, O(log n) |
| first in, first out | ArrayDeque / LinkedList | Queue |
| always take the best next item | PriorityQueue | highest priority first |
| key to value, no order | HashMap | hash table |
| key to value, sorted keys | TreeMap | search tree |
| key to value, insertion order or LRU | LinkedHashMap | linked list threaded through |
4.6Lecture summary
- The
Mapinterface captures key-value stores:get(),put(),containsKey(),containsValue()... - Parameterised by two type variables,
Kfor keys andVfor values. Keys form a set. - Different ways to update an entry, depending on whether the key exists:
getOrDefault(),putIfAbsent(),merge(). - Extract keys as a
Set, values as aCollection, pairs as aSet:keySet(),values(),entrySet(). Use these views to iterate. - Concrete implementations:
HashMap,TreeMap,LinkedHashMap.
Interview Question Bank
What is likely to be asked from Week 6, with 30-second answers, deeper explanations, a lecture-or-extra tag on each question, and code you can run.
Week 6 is the Collections week, and collections questions are among the most common in Java interviews: ArrayList versus LinkedList, the three maps, hashing and equals/hashCode, removing while iterating, and counting with a map. Every answer starts with a 30-second version. Each question is tagged From the lecture or Extra.
One clear sentence: what it is.
Three or four lines of code.
What goes wrong without it.
5.1Interview heat-map: what is most likely
5.2Interfaces, lists and sets
List<String> x = new ArrayList<>() instead of declaring the variable as ArrayList<String>?Think of changing the implementation later.
Main.java:4: error: incompatible types: LinkedListQueue<String> cannot be converted to CircularArrayQueue<String>5The interface type is a level of indirection. Code that uses List depends only on what a list does, so the implementation can be swapped by changing one line (the new), with no change to variables, parameters or return types. Declared with the concrete class, the choice spreads through the whole program.
Go deeper
- Same for parameters and return types: accept
Collection<E>orList<E>, notArrayList<E>. - Exception: use the concrete type when you need a method only it has (e.g.
ensureCapacityonArrayList).
LinkedList over an ArrayList?Compare random access and insertion in the middle.
| ArrayList | LinkedList | |
|---|---|---|
| get(i) | fast (computed) | walks i links |
| insert / remove in the middle | shifts the elements after it | cheap once positioned |
| memory | one array (grows by copying) | one node per element |
| also a Queue/Deque? | no | yes |
LinkedList: get(i) loop slower than iterator? true ArrayList get(i) faster than LinkedList get(i)? truetrue falseArrayList by default: a flexible array with fast random access. Choose LinkedList when you insert and remove at arbitrary positions often (through an iterator) and do not index. A linked list has no random access: get(i) walks i links, so a for (i...) get(i) loop is quadratic.
Go deeper
- In practice
ArrayListis faster for most workloads, even many inserts, because of memory locality (extra). - The marker interface
RandomAccesslets generic code pick an algorithm.
hashCode() when it overrides equals()?A HashSet maps a value to a slot.
2 false1 trueA hash collection first uses hashCode() to pick a slot, and only then equals() within that slot. If two equal objects have different hash codes they land in different slots and are never compared: a set stores both, and contains misses. The contract: equal objects must have equal hash codes.
Go deeper
- The reverse is allowed: unequal objects may share a hash code (a collision).
- Do not use fields that change in
hashCodeif the object is a key (see the mutable-key question).
HashSet and when a TreeSet? What must a TreeSet's elements provide?Both forbid duplicates; they differ in order and cost.
Exception in thread "main" java.lang.ClassCastException: class Job cannot be cast to class java.lang.Comparable (Job is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')[email, print, backup]Both are sets: no duplicates, quick membership. HashSet: hash table, fastest, no order, needs equals/hashCode. TreeSet: balanced tree, O(log n), iterates in sorted order, needs an ordering: elements that are Comparable or a Comparator passed to the constructor; otherwise ClassCastException.
Go deeper
- Two elements are "the same" for a
TreeSetwhen the comparison returns 0, not whenequalsis true (extra).
RandomAccess interface, and why does it have no methods?A tag, not a capability you call.
true false 6 6It is a tagging (marker) interface: it only declares that a list supports fast random access. Code tests c instanceof RandomAccess and picks an index loop or an iterator loop. ArrayList carries the tag; LinkedList does not.
Go deeper
- Other marker interfaces:
Serializable,Cloneable(extra).
ListIterator add to Iterator?Only lists have it, and it moves both ways.
[a, b, c, d]
true
c
[a, b, C, d]Only lists give one. It extends Iterator with add (insert before the current position), set (replace the last returned), and backwards movement: hasPrevious(), previous() (which returns the element, though the slide lists void).
Go deeper
- Do not confuse
ListIterator.add(void, position) withCollection.add(boolean, append).
AbstractCollection for?Collection has many methods; implementing all is tedious.
true
true
false
[red, green, blue]
3 5 4 Exception in thread "main" java.lang.UnsupportedOperationExceptionIt is an abstract class that implements Collection and writes default versions of most methods on top of iterator() and size() (for example contains, containsAll, toString). A concrete collection extends it, supplies the representation-specific parts, and may override defaults for speed. This is the template method idea.
Go deeper
- Optional methods such as
addthrowUnsupportedOperationExceptionuntil overridden. - An interface cannot do this job alone for state and constructors, even with default methods.
Collections (with an s)?Collection and Collections?One letter, two different kinds of thing.
List<Integer> nums = new ArrayList<>(List.of(3, 1, 2));
Collections.sort(nums); // a static helper from the class
System.out.println(Collections.max(nums));Collection is the interface at the top of the framework (this lecture). Collections is a utility class of static helper methods that operate on collections: sort, max, reverse, unmodifiableList ... The same split exists for Arrays (helpers) versus array types.
Go deeper
- Static methods can live in an interface since Java 8 (Week 4), but the library kept this older design.
Collections.sort lists have to satisfy?5.3Iterators, queues and stacks
Which remove, and on what?
[tea, coffee]Exception in thread "main" java.util.ConcurrentModificationExceptionException in thread "main" java.lang.IllegalStateExceptionUse the iterator's remove(): it deletes the element next() last returned. Calling list.remove() inside a for-each changes the list behind the iterator's back, and the iterator fails fast with ConcurrentModificationException. Calling the iterator's remove() twice without a next() in between, or before the first next(), throws IllegalStateException.
Go deeper
- Since Java 8:
collection.removeIf(x -> ...)does it in one line (extra). - "Fail-fast" is a best-effort bug detector, not a thread-safety guarantee.
add and offer, and between remove and poll?Strict and gentle versions of the same operations.
false
Exception in thread "main" java.lang.IllegalStateException: Queue fullException in thread "main" java.util.NoSuchElementExceptionEach has a strict form that flags an error and a gentle form that returns a value. add on a full bounded queue throws IllegalStateException; offer returns false. remove() and element() on an empty queue throw NoSuchElementException; poll() and peek() return null. element/peek only look at the head.
Go deeper
- Because
pollreturnsnullfor "empty", do not storenullin a queue (extra).
PriorityQueue order its elements, and what does printing one show?remove() returns the highest priority item.
[1, 2, 4, 5, 3]
remove(): 1 2 3 4 5
5remove() / poll() always return the highest-priority element: by natural ordering the smallest, or whatever a Comparator says. It is not kept fully sorted: printing or iterating shows the internal heap order. Only repeated remove() gives sorted output.
Go deeper
- Insert and remove cost O(log n); peeking the head is O(1) (extra).
There is an old Stack class. Is it the right one?
[1, 2, 3] 1 3 1 3 [2] 8 7Use a Deque, typically ArrayDeque: push adds at the front, pop removes from the front, peek looks. The old Stack class is a legacy Vector subclass (Lecture 2's list of early classes) and is not recommended.
Go deeper
- Slide slip:
addFirst/addLastreturnvoid, notboolean.
LinkedList as a stack?List.of(...) return, and can you add to it?Optional operations again.
Exception in thread "main" java.lang.UnsupportedOperationExceptionAn immutable list: reading works, any change (add, remove, set) throws UnsupportedOperationException. It is a List by interface, but it chose not to support the optional write methods. Copy it into a new ArrayList if you need to change it.
Go deeper
- Same family:
Arrays.asList(fixed size) andCollections.unmodifiableList(a read-only view).
5.4Maps
HashMap, TreeMap and LinkedHashMap?Think about what order keySet() gives.
| HashMap | TreeMap | LinkedHashMap | |
|---|---|---|---|
| storage | hash table | balanced search tree | hash table + linked list |
| iteration order | none promised | sorted by key | insertion (or access) |
| get / put | fast | O(log n) | fast |
| null key | one allowed | NullPointerException | one allowed |
HashMap [banana, apple, pear, fig]
TreeMap [apple, banana, fig, pear]
LinkedHashMap [pear, apple, fig, banana]All three are Maps. HashMap uses a hash table: fast, no order. TreeMap uses a balanced search tree: keys come out sorted, operations cost O(log n). LinkedHashMap is a hash table also threaded by a linked list: keys come out in insertion order (or, with a flag, access order for an LRU cache).
Go deeper
- The sets mirror this:
HashSet,TreeSet,LinkedHashSet. - Keys of a
TreeMapneed an ordering:Comparablekeys or aComparator.
Map a Collection? How do you iterate over one?A map holds pairs.
Main.java:6: error: incompatible types: HashMap<String,Integer> cannot be converted to Collection<String>Asha earns 50000
Meena earns 55000No. Collection groups single elements; a map holds key-value pairs, so Map<K,V> is a separate interface with two type parameters. You iterate a map through one of its views: keySet() (a Set<K>), values() (a Collection<V>), or best entrySet() (a Set<Map.Entry<K,V>>) to get key and value together without a second lookup.
Go deeper
- Views are live: removing through them removes from the map.
- Iterating
keySet()and callingget(key)inside works but does a lookup per key.
Map.Entry.setValue do?Map.Update if the key exists, otherwise create it.
Map<String, Integer> freq = new HashMap<>();
for (String w : text.split(" ")) {
freq.merge(w, 1, Integer::sum); // or put(w, getOrDefault(w, 0) + 1)
}{not=1, be=2, or=1, to=2}Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is nullKeep a Map<String,Integer> of counts and update it with merge(word, 1, Integer::sum) (store 1 if absent, otherwise add 1), or with put(w, getOrDefault(w, 0) + 1). The trap is put(w, get(w) + 1): for a new word get returns null and unboxing it throws NullPointerException.
Go deeper
- Use a
TreeMapfor alphabetical output, aLinkedHashMapfor first-appearance order.
LinkedHashMap can remember access order.
import java.util.LinkedHashMap;
import java.util.Map;
public class LruCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LruCache(int capacity) {
super(16, 0.75f, true); // access order
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity; // drop the least recently used
}
}[c, a, d]Extend LinkedHashMap with the access-order flag (super(16, 0.75f, true)), so each get or put moves a pair to the end, and override removeEldestEntry to return size() > capacity: the front, the least recently used, is evicted automatically.
Go deeper
- Not thread-safe; wrap with
Collections.synchronizedMapif shared (extra).
HashMap?The entry was filed under the old hash.
null null 1The entry stays in the slot for the old hash. Looking it up with the changed key goes to the new slot and finds nothing; looking it up with an equal-to-the-old key reaches the right slot but equals now fails. The entry is lost yet still counted in size(). Use immutable keys.
Go deeper
- Same danger for elements of a
HashSetand for objects changed inside aTreeSet(extra).
HashMap have a null key? A TreeMap?A tree has to compare keys.
1
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because "k1" is nullHashMap allows one null key (and null values). TreeMap (natural ordering) throws NullPointerException, because it has to call compareTo on keys. Many other collections (ArrayDeque, List.of, PriorityQueue) reject null too.
Go deeper
- A
nullreturn fromgetis ambiguous: absent key, or a key mapped tonull? UsecontainsKey.
Week 6 Summary Sheet
Week 6 on one page: a lecture-by-lecture glance, key terms, the pairs to tell apart, the errors you can now read, a complexity table, a checklist and corrections to the source notes.
6.1Week 6 at a glance
An abstract data type separates interface from implementation. Declare with the interface (Queue<E>); only the instantiation names the class. Circular array: efficient, bounded. Linked list: flexible.
add, iterator, size, contains, addAll(Collection<? extends E>). For-each builds an iterator. Iterator.remove() deletes the last next(). AbstractCollection gives defaults.
List ordered + positional (ArrayList random access, LinkedList not). Set no duplicates (HashSet hash, TreeSet sorted). Queue / Deque / PriorityQueue.
Map<K,V> is not a Collection: keys form a set. put returns the old value. Update with getOrDefault, putIfAbsent, merge. Iterate the views keySet, values, entrySet. HashMap, TreeMap, LinkedHashMap.
6.2Key terms, lecture by lecture
- abstract data type
- Public interface separated from private implementation.
- circular array
- Array whose head and tail wrap around; bounded.
- indirection
- An interface between user code and implementation.
- instantiation
- The one place a concrete class is named.
- Collection
- Interface for grouped data (not key-value).
- Iterator
hasNext,next,remove- for-each
- Implicitly creates and runs an iterator.
- AbstractCollection
- Abstract class with default implementations.
- List
- Ordered; access by iterator or position.
- RandomAccess
- Tagging interface: fast positional access.
- Set
- No duplicates;
addreturns false for one. - hash table
- Maps value to a slot
h(v); collisions possible. - Deque
- Double-ended queue.
- PriorityQueue
removegives the highest priority.
- Map.Entry
- One key-value pair.
- view
- A live Set / Collection over the map.
- merge
- Insert, or combine with the old value.
- access order
- LinkedHashMap mode: get/put moves to the end (LRU).
6.3The pairs you must tell apart
List<E> xdeclare by interfaceArrayList<E> xwelded to a classCollectioninterfaceCollectionsstatic helper classIterator.remove()last returned elementCollection.remove(Object)a given valueArrayListrandom accessLinkedListcheap middle editsHashSethash, no orderTreeSetsorted, O(log n)addstrict: throwsoffergentle: falseremove() / element()throw on emptypoll() / peek()null on emptyHashMapno orderLinkedHashMapinsertion / access orderkeySet()Set of keysentrySet()key and value togethergetOrDefaultread with a defaultmergeinsert or combine6.4Messages and errors you can now read
| What went wrong | What you will see | When |
|---|---|---|
| assigning a different implementation to a concrete variable | error: incompatible types: LinkedListQueue<String> cannot be converted to CircularArrayQueue<String> | compile time |
new on an interface | error: Queue is abstract; cannot be instantiated | compile time |
| a class that skips an interface method | error: Broken is not abstract and does not override abstract method size() in Queue | compile time |
calling remove() on the element | error: cannot find symbol symbol: method remove() | compile time |
| addAll from a wider collection | error: incompatible types: List<Number> cannot be converted to Collection<? extends Integer> | compile time |
| two methods with the same signature | error: method containsKey(Object) is already defined in interface MyMap | compile time |
| using the result of a void method | error: incompatible types: void cannot be converted to boolean | compile time |
| a Map where a Collection is expected | error: incompatible types: HashMap<String,Integer> cannot be converted to Collection<String> | compile time |
| removing before or twice after next() | java.lang.IllegalStateException | run time |
| changing a list or map during a for-each | java.util.ConcurrentModificationException | run time |
| a bounded queue is full | java.lang.IllegalStateException: Queue full | run time |
| remove() or element() on an empty queue | java.util.NoSuchElementException | run time |
| TreeSet of elements with no ordering | java.lang.ClassCastException: class Job cannot be cast to class java.lang.Comparable | run time |
| TreeMap with a null key | java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" | run time |
| map.get(k) + 1 on a missing key | java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" | run time |
| add on a collection that never overrode it | java.lang.UnsupportedOperationException | run time |
| add to List.of(...) | java.lang.UnsupportedOperationException | run time |
| adding to a key set | java.lang.UnsupportedOperationException | run time |
6.5Cost at a glance (extra)
| Collection | get / contains | add | remove | order |
|---|---|---|---|---|
| ArrayList | get O(1), contains O(n) | end O(1) amortised | O(n) (shifts) | insertion |
| LinkedList | get O(n), contains O(n) | ends O(1) | O(1) at an iterator | insertion |
| HashSet / HashMap | O(1) expected | O(1) expected | O(1) expected | none |
| TreeSet / TreeMap | O(log n) | O(log n) | O(log n) | sorted |
| LinkedHashMap | O(1) expected | O(1) expected | O(1) expected | insertion / access |
| ArrayDeque | ends O(1) | O(1) | O(1) | insertion |
| PriorityQueue | peek O(1) | O(log n) | O(log n) | by priority |
6.6Last-minute checklist
- variables typed by the interface
- only
newnames the class - circular array: fast, bounded; linked: flexible
- every abstract method implemented
- remove through the iterator:
iter.remove() - one
next()perremove() - for-each + list.remove = ConcurrentModificationException
- extend
AbstractCollectionfor defaults
- list loops: iterator, not
get(i)on a LinkedList - equals + hashCode together for hash collections
- TreeSet needs Comparable or Comparator
- offer / poll / peek are the gentle forms
- Map is not a Collection
- missing key: use getOrDefault / merge, not get + 1
- iterate
entrySet()for key and value - immutable keys
6.7Corrections made to the source notes
| Where | The source says | Correct |
|---|---|---|
| Lecture 2, removing | element.remove() | iter.remove() (checked by compiling) |
Lecture 2, Collection | int size() boolean isEmpty(); | a semicolon after size() (checked by compiling) |
| Lecture 2, defaults | "provide default implementations in the interface itself" | the library uses the abstract class AbstractCollection (interfaces also may have defaults since Java 8) |
Lecture 3, List | void remove(int index), void previous() | E remove(int index), E previous() |
Lecture 3, AbstractList | "interface" | an abstract class |
Lecture 3, Deque | boolean addFirst(E), boolean addLast(E) | void addFirst(E), void addLast(E) (checked by compiling) |
Lecture 4, Map | boolean containsKey(Object value) | boolean containsValue(Object value) (checked by compiling) |
| Lecture 4, updating | bat | an undeclared name; use a key such as "Asha" |
swap one line
circular vs linked
for-each
AbstractCollection
remove after next
fail-fast
offer, poll, peek
Deque, PriorityQueue
HashSet, TreeSet
equals + hashCode
merge, getOrDefault
Hash / Tree / Linked