Module 7 · Collections & Generics ⏱ 18 min

Collections

By the end of this lesson you will be able to:
  • Use List and ArrayList to store an ordered sequence of values
  • Use Map and HashMap to look up a value by key
  • Explain what generics like List<String> buy you at compile time

Arrays have a fixed length. Once you create new int[10], it holds exactly ten slots — forever. Most real programs do not know in advance how many things they will hold: a shopping cart grows as the shopper browses, a leaderboard shrinks as scores are checked, a log fills until the day ends. For all of that, Java ships the Collections framework — a set of ready-made, resizable containers. The two you will reach for constantly are List, an ordered sequence you can grow and shrink, and Map, a key-to-value lookup. Together they replace almost every hand-rolled data structure.

List, ArrayList, and generics

A List keeps items in the order you added them and lets you fetch any one by position. ArrayList is the everyday implementation — it grows automatically as you add. The angle brackets declare what it holds: List<String> reads as a list of strings. That bracketed type is a generic, and it is not decoration. Try fruits.add(42) on a List<String> and the compiler refuses before the program ever runs, sparing you a broken cast later. Generics move a whole family of bug from runtime into the build.

flowchart LR
  COL["Collections"] --> L["List (ordered, by index)"]
  COL --> M["Map (key to value)"]
  L --> LA["ArrayList"]
  M --> HM["HashMap"]
  style COL fill:#c2410c,color:#fff
  style L fill:#c2410c,color:#fff
List keeps an ordered, indexable sequence; Map stores value lookups by key. ArrayList and HashMap are the common implementations.

Why generics exist: life without them

Before generics arrived, a List held Objectanything at all. You could add a String and an Integer to the same list, and the compiler would not object. The price came at read time: to use an item you had to cast it back, as in String s = (String) list.get(0), and a wrong cast blew up at runtime with a ClassCastException. Generics let the compiler prove the cast is safe at build time, so the running program simply never contains that failure. They trade a little typing for an entire class of crash removed.

flowchart TD
  L["List<String> fruits"] -->|"add(apple)"| OK["accepted"]
  L -->|"add(42)"| ERR["compile error<br/>before the program runs"]
  style OK fill:#3776ab,color:#fff
  style ERR fill:#b45309,color:#fff
Generics are checked at compile time: adding a value of the wrong type is rejected before the program runs.

Reading and changing a List

A handful of methods cover most of what you do. add(x) appends to the end; get(i) returns the element at position i; size() is the count; remove(i) deletes the element at i and closes the gap; contains(x) reports membership. Positions are zero-based: the first element is get(0), so a three-item list has valid indices 0, 1, 2. Asking for get(3) on that list throws IndexOutOfBoundsException — the single most common collections mistake for newcomers.

flowchart LR
  L["List: apple, banana, cherry"] --> I0["get(0) = apple"]
  L --> I1["get(1) = banana"]
  L --> I2["get(2) = cherry"]
  L -.->|"get(3)"| OOB["IndexOutOfBounds"]
  style L fill:#c2410c,color:#fff
  style OOB fill:#b45309,color:#fff
List positions are zero-based: a three-item list has indices 0, 1, 2. Index 3 is out of bounds.
Main.java — building a List, walking it with for-each, and reading its size. Real, compiling Java.
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> fruits = new ArrayList<>();
        fruits.add("apple");
        fruits.add("banana");
        fruits.add("cherry");

        for (String f : fruits) {
            System.out.println("- " + f);
        }

        System.out.println("Total items: " + fruits.size());
        System.out.println("First item: " + fruits.get(0));
    }
}

Walking a collection: for-each

To visit every item without managing an index, use the for-each loop: for (String f : fruits) { ... }. Read the colon as infor each String f in fruits. The loop runs the body once per element, handing you each in turn, and it works identically over a List, an array, or a Set. It is the safe default: because you never touch an index, you cannot write an off-by-one error or accidentally read past the end of the collection.

Map: lookups by key

A Map pairs each key with one value, like a dictionary where you look up a word to get its definition. prices.put("apple", 2) stores 2 under the key "apple"; prices.get("apple") fetches it back. Put the same key twice and the second value overwrites the first. Ask for a key that was never stored and get returns null — not an error — which is why containsKey exists when you need to tell absent apart from legitimately stored as null.

Main.java — a Map stores key-to-value pairs; get returns null for a missing key. Real, compiling Java.
import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> stock = new HashMap<>();
        stock.put("drill", 3);
        stock.put("hammer", 5);
        System.out.println("drills: " + stock.get("drill"));
        System.out.println("has saw: " + stock.containsKey("saw"));
        System.out.println("saw count: " + stock.get("saw"));
        stock.put("drill", 4);   // overwrites the old value
        System.out.println("drills now: " + stock.get("drill"));
    }
}

Choosing between them

Reach for a List when position and order matter — a queue of tasks, a sequence of steps, the top ten scores. You reach items by where they sit. Reach for a Map when you need to look something up by name — a product by its SKU, a user by their id, a country by its code. You reach items by what key they live under. If you ever catch yourself scanning a list to find the one matching entry, you almost certainly wanted a Map instead.

Exercise

What does this program print? Read it carefully and type the exact output (five lines).

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        List<String> fruits = new ArrayList<>();
        fruits.add("apple");
        fruits.add("banana");
        fruits.add("cherry");

        for (String f : fruits) {
            System.out.println("- " + f);
        }

        Map<String, Integer> prices = new HashMap<>();
        prices.put("apple", 2);
        prices.put("banana", 3);
        prices.put("cherry", 5);

        System.out.println("Total items: " + fruits.size());
        System.out.println("Banana price: " + prices.get("banana"));
    }
}
Exercise

A list holds [apple, banana, cherry]. What does get(2) return, and what does get(3) do?

Exercise

List operations. Trace the add, get, size, remove, and contains calls and predict all four lines.

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> todo = new ArrayList<>();
        todo.add("write");
        todo.add("test");
        todo.add("ship");
        System.out.println("first: " + todo.get(0));
        System.out.println("count: " + todo.size());
        todo.remove(1);
        System.out.println("after remove: " + todo.get(1));
        System.out.println("has write: " + todo.contains("write"));
    }
}
Exercise

Map lookups. A key that was never stored and a key that is overwritten — predict all four lines.

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> stock = new HashMap<>();
        stock.put("drill", 3);
        stock.put("hammer", 5);
        System.out.println("drills: " + stock.get("drill"));
        System.out.println("has saw: " + stock.containsKey("saw"));
        System.out.println("saw count: " + stock.get("saw"));
        stock.put("drill", 4);
        System.out.println("drills now: " + stock.get("drill"));
    }
}
Exercise

What is the point of writing List<String> instead of just List?

Recap

  • List is an ordered, resizable sequence; ArrayList is the common implementation. Use add, get, size, remove, contains.
  • Indices are zero-based; the last valid index is size() - 1, and going past it throws.
  • Map stores key-to-value pairs; get returns null for a missing key, containsKey tests membership, and putting an existing key overwrites it.
  • Declare variables as the interface (List, Map) and only name the concrete class at new.
  • A HashMap's iteration order is undefined — never depend on it.

Next you will control which code runs and how often, with conditionals and loops.

Checkpoint quiz

When would you choose a Map over a List?

What does the for-each loop for (String f : fruits) do?

What does map.get("missing") return if "missing" was never stored?

Go deeper — technical resources