Module 7 · Collections & Generics ⏱ 19 min

List vs Set vs Map: Choosing the Right Type

By the end of this lesson you will be able to:
  • Predict whether a collection keeps order and allows duplicates
  • Distinguish the three Set implementations by their iteration order
  • Choose List, Set, or Map for a given workload instead of defaulting to List

You already know List and Map from the collections lesson. Between them they cover a huge amount of ground — until you meet a problem neither solves cleanly. Suppose you log every search term a user types and want only the unique words, ignoring repeats. Stuff them into an ArrayList and you get the duplicates back, in order, with no built-in way to notice a repeat. You could call contains before each add, but that scans the whole list every time — painfully slow as it grows.

The right tool is a Set: a container whose central promise is that it never holds a duplicate. Java's collections are a kit of parts, and each one is optimised for a different promise about order and uniqueness. Choosing well is a matter of knowing what those promises are.

Three containers, three promises

Every collection answers two questions differently: does it keep things in order, and does it allow duplicates? A List says yes to both — it remembers the order you added things and happily keeps repeats, which is why it is the right home for a queue or a sequence of steps. A Set says no to duplicates — adding an element that is already present changes nothing, which makes it the right home for unique tags or ids. A Map stores key-to-value pairs and says no to duplicate keys — put the same key twice and the second value overwrites the first, never creating two entries. If you ever catch yourself scanning a list to find the one matching entry, you almost certainly wanted a Map; if you are filtering repeats out by hand, you wanted a Set.

flowchart TD
  L["List<br/>ordered, allows duplicates"] --> UL["use when position or order matters"]
  S["Set<br/>no duplicates"] --> US["use when uniqueness matters"]
  M["Map<br/>unique key to value"] --> UM["use when you look up by key"]
  style L fill:#c2410c,color:#fff
  style S fill:#3776ab,color:#fff
  style M fill:#c2410c,color:#fff
Each container type is built around a different promise about order and duplicates. Match the promise to the question your code asks.

Three flavours of Set

Set is the interface; you choose an implementation, and the choice decides what order you get the elements back in. A HashSet is the everyday choice — fast, and with no defined order at all, so iterating it can hand elements back in a sequence that depends on internal hashing and can even change between runs. A LinkedHashSet adds one thing to that: it remembers the order you inserted the elements, so iteration is fast and predictable. A TreeSet goes further and keeps its elements sorted in their natural order — alphabetically for strings, ascending for numbers — at the cost of slower add and lookup. Reach for HashSet when you only care about membership, LinkedHashSet when insertion order matters, and TreeSet when you need sorted iteration.

flowchart LR
  HS["HashSet<br/>fast, undefined order"] -->|"iterate"| UNK["any order"]
  LHS["LinkedHashSet<br/>insertion order"] -->|"iterate"| INS["as added"]
  TS["TreeSet<br/>sorted order"] -->|"iterate"| SRT["ascending"]
  style HS fill:#b45309,color:#fff
  style LHS fill:#3776ab,color:#fff
  style TS fill:#3776ab,color:#fff
The three Set implementations share the no-duplicates promise but differ in iteration order: HashSet is undefined, LinkedHashSet is insertion order, TreeSet is sorted.

Choosing between them

Match the container to the question your code asks most often. Do you ask what is at position i, or what came third? That is order by position — reach for a List. Do you ask is this already in here or give me the distinct values? That is uniqueness — reach for a Set. Do you ask what value goes with this key — a product by its code, a user by their id? That is a lookup — reach for a Map.

Getting this right at the start saves painful rewrites later. A program that searches a ten-thousand-element list for each lookup will crawl; the same lookups against a HashMap finish before you notice. The choice of container is, in effect, the choice of algorithm.

Main.java — a LinkedHashSet drops the duplicate and keeps insertion order. Real, compiling Java.
import java.util.LinkedHashSet;
import java.util.Set;

public class Main {
    public static void main(String[] args) {
        Set<String> seen = new LinkedHashSet<>();
        seen.add("apple");
        seen.add("banana");
        seen.add("apple");   // duplicate - ignored
        seen.add("cherry");
        for (String s : seen) {
            System.out.println(s);
        }
    }
}

The trap: undefined order and custom equality

Two surprises live in Set. The first is that a HashSet makes no promise about order. The same set of strings can iterate in a different sequence on a different JVM, so any code that depends on HashSet order is a bug waiting to surface. If order matters, say so explicitly with LinkedHashSet or TreeSet.

The second is subtler and bites the moment your set holds your own objects rather than strings. A Set decides is this a duplicate by calling equals and hashCode — methods every object inherits. Two Strings with the same letters are equal, so the set correctly treats them as one entry. But two of your own objects are equal only if you wrote an equals method that says so; otherwise each is unique merely by being a separate object, and your set quietly fills with near-identical entries.

flowchart TD
  A["add the same value twice"] --> Q["which container?"]
  Q --> L["List: both kept<br/>size grows"]
  Q --> S["Set: second dropped<br/>size unchanged"]
  Q --> M["Map: value overwritten<br/>size unchanged"]
  style Q fill:#c2410c,color:#fff
Adding the same value twice behaves differently per container: a List keeps both, a Set drops the second, a Map overwrites the value.
Main.java — a TreeSet ignores insertion order and iterates its strings sorted. Real, compiling Java.
import java.util.Set;
import java.util.TreeSet;

public class Main {
    public static void main(String[] args) {
        Set<String> sorted = new TreeSet<>();
        sorted.add("mango");
        sorted.add("apple");
        sorted.add("banana");
        for (String s : sorted) {
            System.out.println(s);
        }
    }
}
Exercise

A List keeps duplicates. Trace the adds and predict the exact output (four lines).

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

public class Main {
    public static void main(String[] args) {
        List<String> items = new ArrayList<>();
        items.add("a");
        items.add("b");
        items.add("a");
        System.out.println("size: " + items.size());
        for (String s : items) {
            System.out.println(s);
        }
    }
}
Exercise

You store the unique tags on a photo and want them back in the order you first added them. Which collection fits best?

Exercise

LinkedHashSet drops the duplicate. The second apple is ignored; predict the exact output (three lines).

import java.util.LinkedHashSet;
import java.util.Set;

public class Main {
    public static void main(String[] args) {
        Set<String> seen = new LinkedHashSet<>();
        seen.add("apple");
        seen.add("banana");
        seen.add("apple");
        seen.add("cherry");
        for (String s : seen) {
            System.out.println(s);
        }
    }
}
Exercise

You add several strings to a HashSet and then iterate it. What is true of the order you see?

Exercise

TreeSet iterates sorted. The items are added out of order; predict the exact output (three lines).

import java.util.Set;
import java.util.TreeSet;

public class Main {
    public static void main(String[] args) {
        Set<String> sorted = new TreeSet<>();
        sorted.add("mango");
        sorted.add("apple");
        sorted.add("banana");
        for (String s : sorted) {
            System.out.println(s);
        }
    }
}

Recap

  • A List keeps order and duplicates; a Set keeps no duplicates; a Map keeps unique keys mapped to values.
  • HashSet is fast but has undefined iteration order — never depend on it.
  • LinkedHashSet keeps insertion order; TreeSet keeps elements sorted.
  • Choose by the question you ask: position → List, uniqueness → Set, lookup by key → Map.
  • A Set judges duplicates with equals and hashCode, so your own classes need those methods defined to deduplicate correctly.

Next you will walk these collections element by element — and meet the explosion that awaits anyone who edits one while they iterate.

Checkpoint quiz

You need to look up an employee's record by their staff id, fast. Which container is built for that?

What distinguishes a LinkedHashSet from a HashSet?

Go deeper — technical resources