Intermediate Java
Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.
Course Starting Point
Why start with Java collections?
This course is about becoming a stronger engineer and a sharper interview candidate. We begin with Intermediate Java because collections sit at the boundary between everyday implementation and deeper engineering judgment.
As an experienced Java developer, you likely use List, Map, and Set constantly. The goal here is not to memorize APIs. It is to strengthen your ability to choose the right abstraction, avoid subtle bugs, and write code that communicates intent.
In later lessons, we will go deeper into algorithms, databases, architecture, and system design. For now, we will focus on the Java collections framework as a practical engineering toolkit.

Collection Mental Model
Interfaces express intent
The Java collections framework is built around interfaces first. Good Java code usually declares variables and parameters using the most meaningful interface, then chooses a concrete implementation separately.
Core shapes:
-
Iterable<T>: can be looped over -
Collection<T>: group of elements -
List<T>: ordered, index-based, duplicates allowed -
Set<T>: unique elements, no duplicate according to equality -
Queue<T>/Deque<T>: processing order matters -
Map<K,V>: key-value association; technically not a subtype ofCollection
Think of these as contracts. A List promises positional access. A Set promises uniqueness. A Map promises lookup by key.
The implementation, such as ArrayList, HashSet, or HashMap, is where performance and behavior details enter.

Choose The Contract
You are storing processed event IDs and need to quickly check whether a new event ID has already been seen. Duplicates should not be stored, and ordering does not matter.
Which interface best communicates the intent?
Lists In Practice
List is about order
Use a List when the sequence itself matters: pipeline stages, ranked results, ordered configuration, parsed tokens, or UI rows.
Important engineering habits:
- Prefer
List<T>in method signatures instead ofArrayList<T>unless the concrete type matters. - Be explicit about mutation. A list passed into a method can usually be changed unless you copy it.
-
List.copyOf(...)creates an immutable snapshot, which is useful at API boundaries.
The example below shows a mutable working list and a stable snapshot.
```java
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> pipeline = new ArrayList<>(List.of("ingest", "validate", "enrich"));
pipeline.add(1, "deduplicate");
Collections.sort(pipeline);
List<String> snapshot = List.copyOf(pipeline);
pipeline.set(0, "archive");
System.out.println("Mutable pipeline: " + pipeline);
System.out.println("Immutable snapshot: " + snapshot);
}
}
```
Sets Need Equality
Set depends on equality semantics
A Set does not simply ask, “Have I seen this object reference before?” For most sets, it asks whether two objects are equal according to equals, often assisted by hashCode.
This is why domain modeling matters. If your event identity is (source, id), encode that identity directly. Java records are useful because they generate value-based equals and hashCode for their components.
Be careful with mutable fields used for equality. If an object changes after entering a HashSet, future lookups can become unreliable.
```java
import java.util.*;
public class Main {
record EventKey(String source, String id) {}
public static void main(String[] args) {
Set<EventKey> seen = new HashSet<>();
System.out.println(seen.add(new EventKey("mobile", "evt-42")));
System.out.println(seen.add(new EventKey("mobile", "evt-42")));
System.out.println(seen.add(new EventKey("web", "evt-42")));
System.out.println("Unique events: " + seen.size());
}
}
```
HashSet Contract
True or false: for custom objects stored in a HashSet, consistent equals and hashCode implementations are essential for correct duplicate detection.
Maps For Association
Map models lookup
Use a Map<K,V> when you have a key and want to retrieve or update associated state. This is common in data engineering code: counting events, grouping records, caching metadata, joining streams, and indexing by ID.
Useful methods:
-
merge(key, value, remappingFunction)for counters and accumulation -
computeIfAbsent(key, function)for grouping or lazy creation -
getOrDefault(key, fallback)for safe reads
The example below counts status codes and groups log lines by service.
```java
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> logs = List.of(
"billing:200",
"search:500",
"billing:200",
"search:200",
"billing:404"
);
Map<String, Integer> statusCounts = new TreeMap<>();
Map<String, List<String>> byService = new TreeMap<>();
for (String log : logs) {
String[] parts = log.split(":");
String service = parts[0];
String status = parts[1];
statusCounts.merge(status, 1, Integer::sum);
byService.computeIfAbsent(service, ignored -> new ArrayList<>()).add(log);
}
System.out.println("Status counts: " + statusCounts);
System.out.println("By service: " + byService);
}
}
```
Implementation Tradeoffs
Choose deliberately
Once the interface communicates intent, choose the implementation based on behavior and constraints.
Need Common choice Notes Ordered sequenceArrayList
Great default for indexed reads and appends
Frequent add/remove at both ends
ArrayDeque
Often better than LinkedList for queues/stacks
Unique values, no order guarantee
HashSet
Requires stable equality/hash code
Unique values, sorted order
TreeSet
Uses natural ordering or a comparator
Key-value lookup
HashMap
General-purpose default
Key-value lookup in sorted key order
TreeMap
Useful for ranges and ordered output
This is not yet a full algorithms analysis. The habit to build now is: interface for meaning, implementation for tradeoff.

Default Lookup Class
For a general-purpose key-value collection optimized for lookup by key, the common default implementation is ____<K,V>.
APIs And Ownership
Collections cross boundaries
Collections often move between layers: parsing code, business logic, persistence, APIs, and tests. Bugs often come from unclear ownership.
Good boundary habits:
- Return
List.copyOf(...),Set.copyOf(...), orMap.copyOf(...)when callers should not mutate your internal state. - Avoid exposing internal mutable collections directly.
- Document whether
nullelements or keys are allowed. - Prefer clear domain types over deeply nested collections like
Map<String, List<Map<String, Object>>>. - Use concurrent collections only when shared mutation is truly required.
A senior-level collection choice is not just about speed. It is about correctness, readability, ownership, and change tolerance.
Next, we will build on this foundation by studying data structures and algorithms more directly.
