Skip to content

Arrays and Collections

Step 9 of 20

Arrays and Collections

Introduction

This lesson covers the fifth objective of the Oracle Certified Professional Java SE 21 Developer exam (1Z0-830): Working with Arrays and Collections. Oracle does not publish a weighting per objective, but collections appear in a large share of questions, directly (what does this Map operation return?) and indirectly (streams and concurrency questions use lists, maps and sets as their raw material). Java 21 adds one new topic you must know: sequenced collections (JEP 431).

You will learn how arrays behave, which collection type to pick, the exact contracts of List, Set, Map and Deque, the immutable factory methods, sorting with Comparable and Comparator, and the rules of generics and wildcards. As always, we focus on what the exam asks: predicting output and spotting compile errors.

Learning objectives

After this lesson you will be able to:

  • Declare, initialize, copy, compare and sort arrays, including multidimensional and ragged arrays.
  • Choose among ArrayList, LinkedList, HashSet, LinkedHashSet, TreeSet, HashMap, LinkedHashMap, TreeMap and ArrayDeque.
  • Predict the effect of add, remove, replace and bulk operations, and the exceptions they throw.
  • Use the Java 21 sequenced collection methods (getFirst, getLast, reversed, addFirst).
  • Implement Comparable and Comparator correctly, including chained comparators.
  • Create immutable collections and explain the difference between a copy and an unmodifiable view.
  • Apply generic methods, bounded types and wildcards (? extends, ? super).

1. Arrays

An array is a fixed-size, zero-indexed object holding elements of one type. The length is fixed at creation and read with the length field (not a method).

int[] scores = new int[3];                 // {0, 0, 0}
String[] names = {"Ana", "Bo", "Cy"};      // array initializer
int[][] grid = new int[2][];               // ragged: second dimension not allocated
grid[0] = new int[]{1, 2, 3};
grid[1] = new int[]{4};

Facts and traps:

  • Valid declarations: int[] a, int a[], and int[] a, b[] (here b is int[][]). Mixed forms are a favorite trick.
  • Default values: 0, false, '\u0000', null. new String[2] holds two null references.
  • Indexing outside 0..length-1 throws ArrayIndexOutOfBoundsException at run time; a negative size in new int[-1] throws NegativeArraySizeException.
  • Arrays are covariant: Object[] o = new String[1]; o[0] = 1; compiles but throws ArrayStoreException.
  • == compares references, equals on arrays is also reference equality. Use Arrays.equals(a, b) for content and Arrays.deepEquals for nested arrays. Arrays.toString(a) and Arrays.deepToString(a) print content.
  • Useful helpers in java.util.Arrays: sort, binarySearch, fill, copyOf, copyOfRange, asList, stream, mismatch, compare.

Binary search requires a sorted array. If the key is found it returns its index; otherwise it returns -(insertionPoint) - 1. For {10, 20, 30} searching 25 returns -3 (insertion point 2). On an unsorted array the result is undefined.

Arrays.asList returns a fixed-size list backed by the array: set writes through to the array, but add and remove throw UnsupportedOperationException.

String[] arr = {"b", "a"};
List<String> view = Arrays.asList(arr);
view.set(0, "z");                       // arr is now {"z", "a"}
view.add("q");                          // UnsupportedOperationException

2. The Collections Framework at a glance

Interface Duplicates Order Typical implementations
List Yes Insertion (indexed) ArrayList, LinkedList
Set No Depends HashSet (none), LinkedHashSet (insertion), TreeSet (sorted)
Map Unique keys Depends HashMap, LinkedHashMap, TreeMap
Deque / Queue Yes FIFO / both ends ArrayDeque, LinkedList, PriorityQueue

Map is not a Collection. Collections (plural) is the utility class; Collection is the root interface.

3. List

ArrayList is backed by a resizable array: fast random access, slow insertion in the middle. LinkedList implements both List and Deque: fast insertion at the ends, slow indexed access.

List<String> list = new ArrayList<>(List.of("a", "b", "c"));
list.add("d");              // [a, b, c, d]
list.add(1, "x");           // [a, x, b, c, d]
list.remove("b");           // removes the object: [a, x, c, d]
list.remove(0);             // removes the index: [x, c, d]
list.set(1, "z");           // [x, z, d]
list.indexOf("d");          // 2
list.removeIf(s -> s.equals("x"));   // [z, d]
list.replaceAll(String::toUpperCase); // [Z, D]

The overload trap: with List<Integer> nums, nums.remove(1) removes index 1 (the int overload wins), while nums.remove(Integer.valueOf(1)) removes the value 1.

Iteration while modifying: adding or removing inside an enhanced for loop throws ConcurrentModificationException at the next iteration step (even removing the second-to-last element can, by chance, avoid it, but do not rely on it). Use Iterator.remove() or removeIf.

subList(from, to) returns a view (to exclusive); structural changes of the parent afterward make the view invalid.

4. Set

A Set rejects duplicates according to equals (and hashCode for hash-based sets, compareTo for TreeSet). add returns false if the element was already present.

Set<String> hs = new HashSet<>(List.of("b", "a", "c"));   // order not guaranteed
Set<String> ls = new LinkedHashSet<>(List.of("b", "a"));  // [b, a]
Set<String> ts = new TreeSet<>(List.of("b", "a", "c"));   // [a, b, c]

TreeSet offers first(), last(), floor, ceiling, lower, higher, headSet, tailSet, descendingSet. Elements must be mutually comparable or the set needs a Comparator, otherwise a ClassCastException occurs on insertion. TreeSet does not allow null (natural ordering); HashSet allows one null.

Remember the contract: objects equal by equals must have equal hashCode. If you override equals but not hashCode, two "equal" objects land in different buckets and a HashSet may contain both.

5. Map

A Map<K,V> associates unique keys with values. put returns the previous value (or null).

Map<String, Integer> stock = new HashMap<>();
stock.put("java", 3);
stock.put("java", 5);                       // returns 3, replaces
stock.putIfAbsent("sql", 1);
stock.merge("java", 1, Integer::sum);       // 6
stock.computeIfAbsent("go", k -> 0);        // inserts 0
stock.computeIfPresent("sql", (k, v) -> v + 10);  // 11
stock.getOrDefault("rust", -1);             // -1, nothing inserted
for (Map.Entry<String, Integer> e : stock.entrySet()) { ... }

Key points:

  • HashMap allows one null key and null values; TreeMap has sorted keys (no null key with natural order); LinkedHashMap keeps insertion order (or access order if configured).
  • keySet(), values() and entrySet() are views: removing from them removes from the map.
  • Map.of(...) and Map.copyOf produce immutable maps that reject null and duplicate keys (IllegalArgumentException) and have unspecified iteration order. Map.ofEntries and Map.entry help with more than 10 pairs.
  • merge(key, value, fn): if the key is absent (or its value is null) it stores value; otherwise it stores fn(old, value); if fn returns null the key is removed.

6. Deque, Queue and stack operations

Deque is a double-ended queue. ArrayDeque is the standard implementation and also the recommended stack (the legacy Stack class extends Vector and is synchronized).

Operation Throws on failure Returns special value
Insert at tail add, addLast offer, offerLast
Insert at head addFirst, push offerFirst
Remove from head remove, removeFirst, pop poll, pollFirst
Remove from tail removeLast pollLast
Examine head element, getFirst peek, peekFirst

push puts the element at the head, pop removes from the head, so push(1); push(2); pop() returns 2 (LIFO). ArrayDeque rejects null. PriorityQueue orders by natural order or a comparator, but its iteration and toString order is not sorted; only poll/peek give the smallest element.

7. Sequenced collections (Java 21)

JEP 431 adds interfaces SequencedCollection, SequencedSet and SequencedMap for collections with a defined encounter order. List, Deque, LinkedHashSet, SortedSet (so TreeSet), LinkedHashMap and SortedMap implement them; HashSet and HashMap do not.

List<String> steps = new ArrayList<>(List.of("plan", "code", "test"));
steps.getFirst();            // plan
steps.getLast();             // test
steps.addFirst("idea");      // [idea, plan, code, test]
steps.removeLast();          // [idea, plan, code]
List<String> back = steps.reversed();   // a reverse-ordered VIEW: [code, plan, idea]

LinkedHashMap<String, Integer> m = new LinkedHashMap<>();
m.put("a", 1); m.put("b", 2);
m.firstEntry();              // a=1
m.lastEntry();               // b=2
m.putFirst("z", 0);          // z moves to the front

reversed() is a view: changes in the original show in the reversed one and the reverse. getFirst() on an empty list throws NoSuchElementException. Immutable lists (List.of) support getFirst/reversed but throw UnsupportedOperationException for addFirst. For TreeSet, addFirst/addLast throw UnsupportedOperationException because order is defined by the comparator.

8. Sorting: Comparable and Comparator

Comparable<T> defines the natural order with int compareTo(T other): negative, zero or positive. Comparator<T> defines an external order with int compare(T a, T b).

record Course(String title, int hours, double rating) { }

List<Course> courses = new ArrayList<>(List.of(
    new Course("Java", 40, 4.8), new Course("SQL", 20, 4.8), new Course("Go", 20, 4.5)));

courses.sort(Comparator.comparingDouble(Course::rating).reversed()
                       .thenComparing(Course::hours)
                       .thenComparing(Course::title));
// SQL (4.8, 20h), Java (4.8, 40h), Go (4.5, 20h)

Rules: Collections.sort(list) needs elements implementing Comparable, otherwise a compile error (generic bound) or ClassCastException. Comparator.reversed() applies to the comparator built before it; chain .reversed() carefully because comparing(a).thenComparing(b).reversed() reverses both. Comparator.naturalOrder(), reverseOrder(), nullsFirst, and Comparator.comparing(keyExtractor, keyComparator) are common. An inconsistent compareTo (not matching equals) makes TreeSet treat "different" objects as duplicates.

9. Immutable collections and views

Creation Mutable? Nulls Notes
List.of(...), Set.of(...), Map.of(...) No Rejected Set.of/Map.of reject duplicates at creation
List.copyOf(c) No Rejected Independent copy (returns same instance if already immutable)
Collections.unmodifiableList(l) Not through the view Allowed A view: changes to l show through
Arrays.asList(arr) set only Allowed Fixed-size, backed by array
stream.toList() No Allowed Unmodifiable list

Attempting add, remove or set on an immutable collection throws UnsupportedOperationException; even contains(null) on List.of throws NullPointerException.

10. Generics and wildcards

Generics give compile-time type safety; the type arguments are erased at run time, so List<String> and List<Integer> are the same class and you cannot do new T(), instanceof List<String> or create generic arrays.

static <T extends Comparable<T>> T max(List<T> items) {
    T best = items.get(0);
    for (T t : items) if (t.compareTo(best) > 0) best = t;
    return best;
}
  • List<Object> is not a supertype of List<String> (generics are invariant).
  • List<? extends Number> (upper bound): you can read Number, but you cannot add anything except null. Use it for producers.
  • List<? super Integer> (lower bound): you can add Integer, but you read only Object. Use it for consumers. The mnemonic is PECS: Producer Extends, Consumer Super.
  • List<?> is the unknown type: read as Object, add only null.
  • The diamond new ArrayList<>() infers the type from the context; var infers the declared type from the right side (var l = new ArrayList<>() gives ArrayList<Object>).
  • Raw types (List l = new ArrayList();) compile with warnings and let wrong types in, causing ClassCastException later.

Worked example: top-rated courses per level

Statement. Given this data, what does the program print?

record Course(String title, String level, double rating) { }

List<Course> all = List.of(
    new Course("Java", "pro", 4.8), new Course("SQL", "assoc", 4.6),
    new Course("Go", "pro", 4.5), new Course("Linux", "assoc", 4.9));

Map<String, Double> best = new TreeMap<>();
for (Course c : all) {
    best.merge(c.level(), c.rating(), Math::max);
}
System.out.println(best);

Step 1. TreeMap sorts keys alphabetically: assoc before pro.

Step 2. Process in order. Java/pro is absent so pro=4.8. SQL/assoc gives assoc=4.6. Go/pro: merge computes max(4.8, 4.5) = 4.8. Linux/assoc: max(4.6, 4.9) = 4.9.

Step 3. toString of a map prints entries in key order.

Solution. {assoc=4.9, pro=4.8}.

Why other choices fail. With a HashMap the order might differ and the question would be unanswerable; using put instead of merge would leave pro=4.5 and assoc=4.9 because the last course wins; and Map.of would not allow later modification, so merge would throw UnsupportedOperationException.

Second worked example: list views and the Java 21 methods

Statement. What does this code print?

List<Integer> base = new ArrayList<>(List.of(1, 2, 3, 4));
List<Integer> rev = base.reversed();
base.add(5);
rev.addFirst(9);
System.out.println(base + " " + rev.getLast());

Step 1. reversed() returns a view, so rev shows [4, 3, 2, 1] at first.

Step 2. base.add(5) appends to the original: [1, 2, 3, 4, 5]. The view reflects it as [5, 4, 3, 2, 1].

Step 3. rev.addFirst(9) inserts at the front of the reversed view, which is the end of the original: base becomes [1, 2, 3, 4, 5, 9].

Step 4. rev.getLast() is the last element of the reversed view, which is the first of the original: 1.

Solution. [1, 2, 3, 4, 5, 9] 1.

Why the alternatives are worse. Treating reversed() as a copy would print [1, 2, 3, 4, 5] and miss the write-through. Assuming addFirst puts 9 at index 0 of base ignores that the view is reversed. Expecting an exception confuses the view with an immutable list: this one is backed by a modifiable ArrayList.

Common pitfalls and exam tips

  • Exam tip: identify the concrete class first. Whether the output order is guaranteed depends entirely on it (HashSet no, TreeSet sorted, LinkedHashSet insertion).
  • list.remove(int) versus list.remove(Object) with List<Integer> is tested almost every session.
  • Arrays.asList is not a resizable list; List.of is not even settable.
  • Arrays.sort on an array of objects needs Comparable or a Comparator; on a primitive array it uses natural numeric order.
  • Arrays.equals(int[], int[]) compares content; a.equals(b) does not.
  • TreeSet/TreeMap use compareTo, not equals, to detect duplicates.
  • ArrayDeque and Map.of reject null; HashMap, ArrayList accept it.
  • A for-each over a list while calling list.remove(...) throws ConcurrentModificationException.
  • Collections.unmodifiableList is a view, List.copyOf is a snapshot.
  • Do not forget Java 21: getFirst(), getLast(), reversed() exist on List.

Mini case study: shopping cart ranking

An online shop keeps cart lines as record Line(String sku, int qty, long unitCents). The product owner wants (1) the lines sorted by total price descending, ties by SKU ascending, (2) the total quantity per SKU if the same SKU appears several times, and (3) a safe, read-only list to hand to the invoice module.

Solution.

Comparator<Line> byTotalDescThenSku =
    Comparator.comparingLong((Line l) -> l.qty() * l.unitCents()).reversed()
              .thenComparing(Line::sku);

List<Line> sorted = new ArrayList<>(lines);
sorted.sort(byTotalDescThenSku);

Map<String, Integer> qtyBySku = new LinkedHashMap<>();
for (Line l : lines) qtyBySku.merge(l.sku(), l.qty(), Integer::sum);

List<Line> readOnly = List.copyOf(sorted);

Commentary: reversed() is applied right after comparingLong, so only the total is reversed, and thenComparing adds the ascending SKU tie-breaker after it. Copying lines into a new ArrayList avoids mutating the caller's data (and avoids UnsupportedOperationException if lines came from List.of). merge with Integer::sum is the idiomatic accumulator, and LinkedHashMap keeps first-seen order for predictable reports. List.copyOf is an independent immutable snapshot, whereas Collections.unmodifiableList(sorted) would reflect later changes to sorted, which could surprise the invoice module.

Key terms

  • Encounter order: the order in which a collection yields its elements.
  • Sequenced collection: a collection with a defined first and last element (Java 21).
  • View: a collection backed by another, such as subList, keySet or reversed().
  • Natural ordering: the order defined by Comparable.compareTo.
  • Type erasure: removal of generic type arguments at compile time.
  • PECS: Producer Extends, Consumer Super.
  • Fail-fast iterator: throws ConcurrentModificationException on structural modification.
  • Load factor: fill ratio of a hash table before it resizes (0.75 by default for HashMap).

Summary checklist

  • I can declare and initialize one-, two-dimensional and ragged arrays and know the default values.
  • I know what Arrays.asList, List.of, List.copyOf and unmodifiableList allow and forbid.
  • I can predict List.remove(int) versus remove(Object) behavior.
  • I can pick HashSet, LinkedHashSet or TreeSet from the ordering requirement.
  • I can use put, putIfAbsent, merge, computeIfAbsent and getOrDefault on maps.
  • I can use Deque as a stack and a queue and know where push inserts.
  • I can use getFirst, getLast, reversed and addFirst from Java 21.
  • I can write chained comparators and avoid the reversed() pitfall.
  • I can apply ? extends, ? super and generic bounds.

Official sources