Career OS

Learn · OOP & Java

Java Collections

Java hands you ready-made containers so you never rebuild a list or a lookup table by hand. Knowing which to pick is half of writing clean code — and a constant interview question.

Before we start

📋 What you’ll learn
  • What a collection is and the four you must know
  • List vs Set vs Map vs Queue — and when to reach for each
  • Why a Set can’t hold duplicates (the trick underneath)
  • How these build directly on Arrays & Hashing
✅ After this you’ll be able to
  • Pick the right collection for a problem, on purpose
  • Explain why a Set removes duplicates automatically
  • Use List, Set, Map and Queue in real Java code

Why you’re learning it: collections appear in every Java program and every interview (“List vs Set?”). They’re the practical face of your Arrays & Hashing lesson. ⏱️ ~25 min.

The idea — the right container for the job

You wouldn’t carry soup in a basket or books in a bucket. Data is the same: each collection is a container shaped for a job. Pick by asking two questions — do I need order? and do I find things by position, by uniqueness, or by a key? Here are the four, drawn out:

📋 List — an ordered, numbered shelf

Keeps order, allows duplicates, reach any item by its index. (Like the array you already know — but it grows.)

A0
B1
A2
C3

Note the two As — a List is happy with duplicates.

🚫 Set — a guest list (each name once)

Add the same thing twice and the second is silently ignored. No order, but instant “is this in here?”.

ABC ← add “A” again → A

🗺️ Map — labelled drawers (key → value)

Store a value under a key, fetch it instantly by that key. A dictionary; your phone contacts.

"d@x.com"User #123
"a@x.com"User #124

🎟️ Queue — a line at the counter (FIFO)

First in, first served. Add at the back, remove from the front.

front → job 1job 2job 3 ← back

Choose the right one

NeedUse(Java class)Reach for it when…
ListListArrayLista sequence — a playlist, rows from a query
SetSetHashSetno duplicates — unique visitors, “seen before?”
MapMapHashMaplook up by name/id — user by email
QueueQueueLinkedList / ArrayDequeprocess in order — job/task queues

The key insight

A Set has no duplicates because underneath it’s a Map using the items themselves as keys — and keys are unique by definition. A HashMap gives O(1) lookups for the exact reason from your hashing lesson (the hash points straight to the slot). So collections aren’t new magic — they’re the data structures you already learned, gift-wrapped.

Why not just use arrays?

You already know arrays. Here’s exactly where they hurt — and why collections exist:

📏 Fixed size

Need a 6th slot in a size-5 array? Make a bigger one and copy everything by hand.

✂️ No remove

Deleting index 2 means manually shifting everything after it down.

🔍 No “exists?”

You write the search loop yourself, every single time.

🗝️ No key lookup

“What’s Ravi’s balance?” forces a scan of the whole array.

Collections are classes that do all of this for you — growing, shifting, searching — battle-tested for 25 years.

The family tree

Collections are interfaces (the contract) with multiple implementations (the engine). You declare the interface, instantiate the implementation:

List<String> names = new ArrayList<>();   // contract on the left, engine on the right

Why? Code that depends on List doesn’t care which engine is underneath — swap engines later without rewriting callers.

flowchart TD C[Collection] --> L[List: ordered, duplicates ok] C --> S[Set: no duplicates] L --> AL[ArrayList] L --> LL[LinkedList] S --> HS[HashSet] M[Map: key-value, separate family] --> HM[HashMap]

Note Map is drawn apart — technically it’s not a Collection — but everyone discusses them together because you use them together.

See them in real Java

📋 ArrayList — the workhorse (an array that resizes itself)

List<Integer> expenses = new ArrayList<>();
expenses.add(250);          // chai and samosa run
expenses.add(1200);         // electricity bill
expenses.add(250);          // duplicates are fine in a List
expenses.remove(Integer.valueOf(1200));     // remove by value, not index
expenses.get(0);            // indexed access: 250
expenses.size();            // 2
expenses.contains(250);     // true

How it grows (interview favorite): inside is a plain array (default capacity 10). Add the 11th element and the ArrayList makes a new array ~1.5× bigger and copies everything across. That copy is occasionally costly, but rare enough that adding stays cheap on average. get(i) is instant — pure array math, jump straight to the slot.

flowchart LR A[Internal array full at capacity 10] --> B[Allocate new array at capacity 15] B --> C[Copy all 10 elements over] C --> D[Add element 11]

The <Integer> in angle brackets is generics — it tells the compiler “this list holds Integers only,” so it blocks a String at compile time.

🔗 LinkedList — exists, rarely wins

Stores elements as a chain of nodes, each pointing to the next. Textbooks praise “fast insertion in the middle.” Honest reality:

Know it exists, say why it loses, default to ArrayList. Senior answer: “I’d reach for LinkedList only with a measured reason — and I’ve never had one.”

🚫 HashSet — no duplicates, fast contains

Set<String> handles = new HashSet<>();
handles.add("darshan@upi");    // true — added
handles.add("ravi@upi");       // true — added
handles.add("darshan@upi");    // false — already there, ignored
handles.size();                // 2
handles.contains("ravi@upi");  // true — and FAST

Two things to bank: (1) add returns false if the element was already present — a free “have I seen this before?” check. (2) contains is fast regardless of size — 10 elements or 10 million cost roughly the same, while a List’s contains scans every element. Warning: a HashSet has no order — iterate it and items come out however hashing decided.

🗺️ HashMap — key → value lookup

Map<String, Integer> balances = new HashMap<>();
balances.put("darshan@upi", 5000);
balances.put("ravi@upi", 12000);
balances.put("darshan@upi", 4500);   // same key — value REPLACED, not duplicated

balances.get("darshan@upi");            // 4500
balances.getOrDefault("priya@upi", 0);  // 0 — safe default for missing keys
balances.containsKey("ravi@upi");       // true

HashMap internals — THE interview classic

The question interviewers actually ask: how does get find the value without scanning everything? Inside, a HashMap is an array of buckets. The journey of balances.get("ravi@upi"):

  1. Call hashCode() on the key — gives a number, say 83214041.
  2. Squash that number into a bucket index — roughly hash mod array length — say bucket 6.
  3. Jump straight to bucket 6. No scanning, no searching.
  4. Two keys can land in the same bucket (a collision), so a bucket holds a short chain of entries. Walk that chain, using equals() to find the exact key.
  5. Return that entry’s value.
flowchart TD K[Key ravi at upi] --> H[hashCode gives a number] H --> B[Number mod array length gives bucket 6] B --> J[Jump straight to bucket 6] J --> E{equals check on each entry in the bucket} E --> V[Match found: return 12000] E --> N[No match: return null]

Why equals and hashCode must agree: equal objects must have equal hash codes. Break it, and your key hashes to bucket 6 on put but you search bucket 3 on get — the entry is there, and you can never find it. The map looks like it’s losing data. It isn’t — your hashCode is lying. Say “hash to bucket, then equals within the bucket” in an interview and you’re ahead of most freshers.

A first taste of Big-O

Big-O answers one thing: when the data gets 1000× bigger, does the operation get 1000× slower — or stay the same? Two shapes for now: O(1) constant (same speed at any size — jumping to a slot) and O(n) linear (doubles when data doubles — scanning everything).

OperationCostWhy
list.get(index)O(1)Array math — jump straight to the slot
list.contains(x)O(n)Must check every element until found
set.contains(x)O(1) avgHash to bucket, check a tiny chain
map.get(key)O(1) avgSame hash-to-bucket jump
map.put(key, val)O(1) avgSame again

The practical takeaway: call list.contains inside a loop over another big list and you’ve built an O(n²) bomb — fine with 50 rows in testing, 30-second page loads with 50,000 in production. Swapping that inner list for a HashSet is one of the most common real-world performance fixes there is.

The iteration trap every dev hits once

You cannot structurally change a collection while a for-each is walking it:

for (Integer amount : amounts) {
    if (amount < 200) {
        amounts.remove(amount);   // BOOM — ConcurrentModificationException
    }
}

The iterator notices the collection shifted under its feet and throws — on purpose, because continuing would give garbage. The modern fix is one line:

amounts.removeIf(amount -> amount < 200);   // safe, and clearer about intent

That arrow is a lambda — read it as “for each amount, is it under 200?” Full lambda treatment comes later.

Two traps you’ll hit in week one

🧊 List.of is immutable

List.of(...) returns a read-only list. Call add/remove on it and you get UnsupportedOperationException. Wrap it: new ArrayList<>(List.of(...)) — copy first, then modify.

🎲 No order from hashing

HashSet/HashMap trade order for speed. Need sorted or insertion order? Java has other implementations that cost a little speed for it — parked for later.

How to choose — the decision tree

Three questions, in order: lookup by key? uniqueness? order? Default to ArrayList only when nothing else claims the job — not as a reflex.

flowchart TD Q1{Look things up by a key?} -->|yes| HM[HashMap] Q1 -->|no| Q2{Must duplicates be impossible?} Q2 -->|yes| HS[HashSet] Q2 -->|no| Q3{Does order or index matter?} Q3 -->|yes| AL[ArrayList] Q3 -->|no| Q4{Will you call contains a lot?} Q4 -->|yes| HS2[HashSet] Q4 -->|no| AL2[ArrayList]

Where you’ll use it — real life

🧾 Everywhere

Holding query rows, config by key, unique ids — daily Java.

🚫 Dedup

Drop a list into a Set and duplicates vanish for free.

🔎 Lookups

A Map turns “find the user with this id” into one call.

📥 Job queues

Your Relay task-queue project is a Queue in action.

Now YOU do the reps

First, the recognition drill — pick the collection before opening the answer:

You must remove duplicate user-IDs from a list. Which collection?

Set — add them all; duplicates drop automatically.

You need to look up an order by its order-number, fast. Which?

Map — key = order number, value = the order.

You’re processing sign-ups strictly in the order they arrived. Which?

Queue — FIFO, first come first served.

You need the top 10 songs in ranked order, duplicates possible. Which?

List — ordered and duplicates allowed; index = rank.

Then code them (both use collections directly):

Interview self-check

Answer each out loud before opening it — these are the exact questions Indian Java interviews ask:

Name three things collections give you that plain arrays don’t.

Automatic resizing, easy removal, built-in contains, key-value lookup (Map), uniqueness enforcement (Set) — any three.

What happens inside an ArrayList when you add beyond its capacity?

It allocates a new internal array ~1.5× larger, copies all existing elements into it, then adds the new one. Rare enough that adds stay cheap on average.

Why does ArrayList beat LinkedList in most real workloads?

ArrayList’s elements sit side by side in one array, which CPUs read fast, and get is direct array math. LinkedList scatters nodes across memory and must walk the chain for access.

Walk through the full journey of map.get("ravi@upi").

Call hashCode() on the key, squash it into a bucket index, jump straight to that bucket, walk the short chain there using equals() to match the exact key, return the value (or null).

What’s a hash collision, and how does HashMap deal with it?

Two different keys hashing to the same bucket. The bucket holds a small chain of entries, and equals picks the right one within it.

Why is set.contains O(1) but list.contains O(n) — and when does it hurt?

The set hashes straight to a bucket; the list checks elements one by one. It hurts when contains runs inside a loop over big data — O(n) inside O(n) is O(n²), the classic “fast in dev, dead in production” pattern.

What exception do you get removing from a list in a for-each, and the clean fix?

ConcurrentModificationException — the iterator detects the collection changed mid-walk. Fix: list.removeIf(condition).

Store unique visitor IDs, check membership millions of times — which collection?

HashSet — uniqueness is enforced automatically and contains stays O(1) no matter how many millions of IDs are in it.

Build this yourself

Two small terminal tools, both solving problems collections were born for:

🗣️ The 2-minute explain test

Out loud: “When do I use a List vs a Set vs a Map vs a Queue, why does a Set have no duplicates, and how does a HashMap retrieve a value (including collisions and why equals + hashCode must agree)?” Then log it in your Journal.

Why AI can’t do this for you: AI will pick a collection for you — usually ArrayList, usually without thinking. It won’t be in the room when the page that was instant in dev takes 30 seconds on production data, and the fix is knowing an O(n) contains inside a loop should have been a HashSet from day one. Choosing data structures for data you haven’t seen yet is judgment. The HashMap interview question exists precisely to separate people with that judgment from people who prompt for it.


Next: Exceptions →

Saves your progress on this device.
00:00