SEN2211 · Week 9 · Data Structure Classes in Java

Java's ready-made data structures: Collections

ArrayList, LinkedList, ArrayDeque, Stack, the Queue/Deque interfaces, Iterator and HashMap. The Java counterparts of the structures you wrote by hand, and which one to choose for which job. The exam has “what is printed” and structure selection questions.

Java Collections FrameworkListArrayListLinkedListArrayDequeStackQueue interfaceDeque interfaceIteratorConcurrentModificationExceptionHashMapgenericsautoboxing

For eight weeks we wrote linked lists, stacks and queues by hand. In a real project you don’t write these from scratch: Java’s java.util package has tested, fast versions of all of them. The goal of this week is twofold: to recognize the Java counterpart of every structure you wrote by hand, and to pick the right class for the right job. Exam questions are usually of the “what is printed?” type and contain small traps: is remove(1) an index or a value, the toString difference between Stack and ArrayDeque, the difference between poll and remove on an empty queue.

Golden rules:

  1. Declare the variable with the interface type. List<Integer> a = new ArrayList<>();
  2. Use ArrayDeque for stacks and queues. The Stack class is old.
  3. ArrayList if there is a lot of access by index, ArrayDeque/LinkedList if there is a lot of insertion at the ends.
  4. Removing while traversing: Iterator.remove() or removeIf. Don’t call list.remove inside a for-each.
  5. HashMap for lookup by key.

A map of the Collections Framework

InterfaceWhat it providesClasses
List<E>ordered, indexed, duplicates allowedArrayList, LinkedList
Queue<E>FIFO: offer, poll, peekArrayDeque, LinkedList, PriorityQueue
Deque<E>double-ended: addFirst, pollLast, push, popArrayDeque, LinkedList
Map<K,V>key → valueHashMap, TreeMap, LinkedHashMap
Set<E>no duplicatesHashSet, TreeSet

All of them are generic: they take a type parameter such as <Integer>. You can’t put in a primitive type (int, double); a wrapper class is used. Java converts automatically between int and Integer (autoboxing).

ArrayList: the growing array

It keeps an array inside, and when full it copies to about 1.5 times the size. The same idea as the “growing array stack” in week 5.

List<String> s = new ArrayList<>();
s.add("A"); s.add("B"); s.add("C");
s.add(1, "X");          // insert in the middle, shift the right side
s.set(0, "Y");          // replace
System.out.println(s + " size=" + s.size() + " indexOf(B)=" + s.indexOf("B") + " contains(A)=" + s.contains("A"));
[Y, X, B, C] size=4 indexOf(B)=2 contains(A)=false

PITFALL · remove(int) and remove(Object). On a List<Integer>, list.remove(1) removes index 1. To remove the element whose value is 1, write list.remove(Integer.valueOf(1)). When Java sees an int argument, it chooses the index version.

List<Integer> list = new ArrayList<>(List.of(10, 20, 30, 40));
list.remove(1);                     // index 1 → 20 goes
System.out.println(list);
list.remove(Integer.valueOf(30));   // value 30
System.out.println(list);
[10, 30, 40]
[10, 40]

LinkedList: a doubly linked list

The ready-made version of the DoublyLinkedList from week 3. It implements both the List and Deque interfaces, so it can be used both by index and from the ends.

LinkedList<Integer> ll = new LinkedList<>();
ll.add(5); ll.addFirst(2); ll.addLast(8); ll.add(1, 4);
System.out.println(ll + " getFirst=" + ll.getFirst() + " get(2)=" + ll.get(2));
ll.removeLast(); ll.removeFirst();
System.out.println(ll);
[2, 4, 5, 8] getFirst=2 get(2)=5
[4, 5]

get(2) works but internally it walks from the front (or from the back, whichever is closer): O(n). A loop for (int i = 0; i < ll.size(); i++) ll.get(i) over a LinkedList is O(n²) in total. Use for-each to traverse.

Measurement: inserting n elements at the front.

n=10000 ArrayList add(0) 3.1 ms, LinkedList addFirst 0.6 ms
n=20000 ArrayList add(0) 11.3 ms, LinkedList addFirst 0.8 ms
n=40000 ArrayList add(0) 46.2 ms, LinkedList addFirst 0.8 ms

When n quadrupled, ArrayList slowed by about 15 times (each insertion O(n), O(n²) in total). LinkedList stayed almost constant (each insertion O(1)). The times vary by machine, the ratio doesn’t.

Stack and queue: ArrayDeque

ArrayDeque is the ready-made version of the circular-array deque from week 8. It is the fastest choice both as a stack and as a queue.

Deque<Integer> stack = new ArrayDeque<>();   // stack
stack.push(1); stack.push(2); stack.push(3);
System.out.println(stack + " peek=" + stack.peek());

Queue<Integer> q = new LinkedList<>();       // queue
q.offer(7); q.offer(3); q.offer(5);
System.out.println(q.poll() + " " + q.peek() + " " + q);
[3, 2, 1] peek=3
7 3 [3, 5]

The old Stack class

java.util.Stack still exists and some instructors use it. It derives from Vector, every method is synchronized (slow), and it also allows access by index (which breaks the stack rule).

Stack<Integer> st = new Stack<>();
st.push(1); st.push(2); st.push(3);
System.out.println(st.pop() + " " + st.peek() + " " + st);
3 2 [1, 2]

PITFALL · Two different toStrings. With the same push order, Stack writes from bottom to top ([1, 2, 3]) and ArrayDeque from top to bottom ([3, 2, 1]). pop gives 3 in both. In an output question, look at which class it is.

Two families: exception or special value?

OperationThrows an exceptionReturns null / false
Insertadd(e)offer(e)
Removeremove()poll()
Lookelement()peek()

On an empty queue:

null null
remove: NoSuchElementException
element: NoSuchElementException
deque peek=null
deque pop: NoSuchElementException
Stack pop: EmptyStackException
ArrayDeque null: NullPointerException

The last line: null can’t be put into an ArrayDeque, because poll returning null means “empty”.

Iterator

Every collection provides an Iterator: hasNext(), next(), remove(). The for-each loop uses this behind the scenes. It is the abstract version of the traversal you did with p = p.next on a linked list.

List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
Iterator<Integer> it = nums.iterator();
while (it.hasNext())
    if (it.next() % 2 == 0) it.remove();
System.out.println(nums);
[1, 3, 5]

If you change the list directly inside a for-each, the iterator notices and throws a ConcurrentModificationException at the next next(). A one-line modern alternative: nums.removeIf(x -> x % 2 == 0);

HashMap basics

A table that maps keys to values. Lookup, insertion and deletion are O(1) on average. How it works (hashing) is a SEN2212 topic; here we learn to use it.

Map<String, Integer> count = new HashMap<>();
for (String w : "to be or not to be".split(" "))
    count.put(w, count.getOrDefault(w, 0) + 1);
System.out.println(count.get("to") + " " + count.get("or") + " " + count.get("is") + " size=" + count.size());
System.out.println(new TreeMap<>(count));
2 1 null size=4
{be=2, not=1, or=1, to=2}
MethodWhat it does
put(k, v)inserts or updates; returns the old value (null if none)
get(k)the value or null
getOrDefault(k, d)the value or d
containsKey(k)does it exist
remove(k)removes
keySet(), values(), entrySet()for traversing

A HashMap’s traversal order is undefined; use TreeMap if you want sorted order, LinkedHashMap if you want insertion order.

Which structure for which job?

NeedChoiceWhy
Frequent access by indexArrayListget(i) is O(1)
Frequent insertion/removal at the endsArrayDequeboth ends O(1), at array speed
LIFO (undo, parentheses, DFS)ArrayDeque (push/pop)stack
FIFO (job queue, BFS)ArrayDeque (offer/poll)queue
Order by priorityPriorityQueuesmallest first
Lookup by keyHashMapO(1) on average
Keys sortedTreeMapO(log n)
Many removals from the middle while traversingLinkedList + Iteratorremoval is O(1)

Common mistakes

WrongCorrect
List<int>List<Integer>
list.remove(5) removes the valueIt removes the index; for the value, Integer.valueOf(5)
A get(i) loop on a LinkedListfor-each, otherwise O(n²)
Stack and ArrayDeque print in the same orderStack bottom→top, ArrayDeque top→bottom
remove() on an empty queue returns nullIt throws an exception; for null use poll()
list.remove(x) inside a for-eachIterator.remove() or removeIf
map.put always adds a new elementWith the same key it updates the value
HashMap writes keys in sorted orderThe order is undefined; use TreeMap

Test yourself

Exam-style questions. Solve them on paper first, then reveal the answer.

Question 1medium

What is printed?

List<Integer> list = new ArrayList<>(List.of(10, 20, 30, 40));
list.remove(1);
System.out.println(list);
list.remove(Integer.valueOf(30));
System.out.println(list);
Show answer
[10, 30, 40] / [10, 40]

remove(1) takes an int and removes the element at index 1 (20). remove(Integer.valueOf(30)) takes an object and removes the element whose value is 30. The most commonly misjudged point in a List<Integer>.

Question 2easy

What is printed?

List<String> s = new ArrayList<>();
s.add("A"); s.add("B"); s.add("C");
s.add(1, "X");
s.set(0, "Y");
System.out.println(s + " " + s.indexOf("B") + " " + s.contains("A"));
Show answer
[Y, X, B, C] 2 false

add(1, X) inserts in the middle and shifts the ones to the right: [A, X, B, C]. set(0, Y) replaces, and the number of elements stays the same: [Y, X, B, C]. B is at index 2. A no longer exists.

Question 3medium

What is printed?

Stack<Integer> st = new Stack<>();
st.push(1); st.push(2); st.push(3);
System.out.println(st.pop() + " " + st.peek() + " " + st);
Show answer
3 2 [1, 2]

pop removes 3, peek shows 2. Because the Stack class derives from Vector, toString writes in array order, that is from bottom to top: [1, 2].

Question 4hard

What is printed? Compare with the previous question.

Deque<Integer> ad = new ArrayDeque<>();
ad.push(1); ad.push(2); ad.push(3);
System.out.println(ad + " peek=" + ad.peek());
Show answer
[3, 2, 1] peek=3

In ArrayDeque, push = addFirst. Elements are added at the front, and toString writes from front to back: from top to bottom [3, 2, 1]. The same push order, the opposite appearance from Stack. In both, pop gives 3.

Question 5easy

What is printed?

Queue<Integer> q = new LinkedList<>();
q.offer(7); q.offer(3); q.offer(5);
System.out.println(q.poll() + " " + q.peek() + " " + q);
q.offer(q.poll());
System.out.println(q);
Show answer
7 3 [3, 5] / [5, 3]

FIFO: poll removes 7, peek shows the front: 3. Remaining [3, 5]. q.offer(q.poll()) moves the front to the back: [5, 3].

Question 6medium

q is an empty Queue<Integer> (an ArrayDeque). What happens on each line?

System.out.println(q.poll());
System.out.println(q.peek());
q.remove();
Show answer
null, null, NoSuchElementException

The Queue interface has two versions of each operation: offer/poll/peek return a special value (false/null) when empty or full, while add/remove/element throw an exception. poll and peek print null, remove throws a NoSuchElementException.

Question 7medium

What is printed?

List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
Iterator<Integer> it = nums.iterator();
while (it.hasNext())
    if (it.next() % 2 == 0) it.remove();
System.out.println(nums);
Show answer
[1, 3, 5]

The Iterator's own remove() method safely removes the element most recently returned by next(). The evens go: [1, 3, 5]. This is the right way to remove while traversing (or removeIf).

Question 8hard

What happens in each case?

List<Integer> a = new ArrayList<>(List.of(1, 2, 3, 4));
for (Integer x : a) if (x == 2) a.remove(x);   // (i)

List<Integer> b = new ArrayList<>(List.of(1, 2, 3, 4));
for (Integer x : b) if (x == 3) b.remove(x);   // (ii)
System.out.println(b);
Show answer
(i) ConcurrentModificationException; (ii) no exception, prints [1, 2, 4]

for-each uses a hidden Iterator. When the list is changed from outside, the next next() throws a ConcurrentModificationException: (i). In (ii), when 3 is removed the size becomes 3 and the iterator's position is also 3; hasNext() returns false, next() is never called and there is no exception. That it works by coincidence doesn't mean the code can be trusted; use Iterator.remove or removeIf.

Question 9medium

What is printed?

Map<String, Integer> count = new HashMap<>();
for (String w : "to be or not to be".split(" "))
    count.put(w, count.getOrDefault(w, 0) + 1);
System.out.println(count.get("to") + " " + count.get("or") + " " + count.get("is") + " size=" + count.size());
Show answer
2 1 null size=4

For each word it takes the current count (0 if absent) and adds 1. to = 2, be = 2, or = 1, not = 1: 4 distinct keys. get returns null for a key that doesn't exist.

Question 10medium

What is printed?

Map<String, Integer> m = new HashMap<>();
System.out.println(m.put("ali", 70));
System.out.println(m.put("ali", 85));
m.put("ece", 90);
System.out.println(m.get("ali") + " " + m.size() + " " + m.getOrDefault("can", -1));
Show answer
null / 70 / 85 2 -1

put returns the key's old value: the first time there is none (null), the second time 70. When the same key is put a second time, the value changes and no new element is added: size 2. can doesn't exist, the default is -1.

Question 11easy

Choose the most suitable Java class for each: (a) an undo feature in a text editor, (b) print jobs served in arrival order, (c) looking up a student's grade by student ID, (d) frequent access to the i-th element of a growing list.

Show answer
(a) ArrayDeque as a stack, (b) ArrayDeque/LinkedList as a Queue, (c) HashMap, (d) ArrayList

(a) Undo = the last thing done is undone first: LIFO, a stack. (b) The first to arrive is printed first: FIFO, a queue. (c) Lookup by key: HashMap, O(1) on average. (d) Access by index: ArrayList get(i) is O(1); in LinkedList it is O(n).

Question 12easy

Give the time complexity: (a) ArrayList.get(i), (b) ArrayList.add(0, x), (c) LinkedList.get(i), (d) LinkedList.addFirst(x), (e) HashMap.get(key) on average.

Show answer
(a) O(1) (b) O(n) (c) O(n) (d) O(1) (e) O(1)

ArrayList is an array: access by index is O(1), inserting at the front shifts everything O(n). LinkedList is a doubly linked list: walking to the i-th element is O(n), inserting at the front is O(1). Measurement: when n went from 10 000 → 40 000, the ArrayList.add(0) time went 3.1 ms → 46.2 ms (about 16 times, O(n²) in total), while LinkedList.addFirst stayed almost constant.

Verification: All code fragments were run with JDK 26 in verify/SEN2211/W09.java (java W09.java); the outputs (toString formats, null returns, thrown exception types) were taken verbatim from the program. The times of ArrayList.add(0, x) and LinkedList.addFirst were measured for n = 10 000, 20 000, 40 000 and the growth ratio compared.

Review card

INTERFACE ← CLASS: List ← ArrayList, LinkedList | Queue/Deque ← ArrayDeque, LinkedList | Map ← HashMap, TreeMap ArrayList: get/set O(1), add at the end O(1)*, add(0,x)/remove(0) O(n) LinkedList: addFirst/addLast/removeFirst/removeLast O(1), get(i) O(n) list.remove(1) → INDEX 1; list.remove(Integer.valueOf(1)) → VALUE 1 STACK: Deque<Integer> s = new ArrayDeque<>(); push/pop/peek (Stack is old; toString bottom→top) ArrayDeque toString front to back: push 1,2,3 → [3, 2, 1] QUEUE: Queue<Integer> q = new ArrayDeque<>(); offer/poll/peek When empty: poll/peek → null | remove/element/pop → NoSuchElementException | Stack.pop → EmptyStackException ArrayDeque does not accept null (NullPointerException) list.remove inside for-each → ConcurrentModificationException; use it.remove() or removeIf HashMap: put (returns the old value), get (null if absent), getOrDefault, containsKey, remove; O(1) on average; NO order TreeMap: sorted by key; counter pattern: m.put(k, m.getOrDefault(k, 0) + 1) Generics: Integer, not int (autoboxing)