Free lesson · Java Basic To Advance
Introduction to Collections
Why a Framework
Brandon Carter, picking up Module 14. Before Java's Collections Framework existed, every codebase invented its own data structures. Arrays for some things. Vectors for others (Java 1.0 had Vector and Hashtable — synchronized, slow, awkward). Custom linked-list implementations everywhere. No common interface meant code couldn't be reused — a method written to process one team's list couldn't take another team's list as input.
The Collections Framework, introduced in Java 1.2 (1998), fixed this. It defined common interfaces (List, Set, Map), provided multiple implementations of each, and gave the language a shared vocabulary for data structures. Every Java developer since has used it daily.
This module covers it in depth. Module 14a (this part) focuses on Lists and Sets. Module 14b will cover Maps including a deep technical dive into HashMap internals led by Nikhil.
What the Framework Provides
The Collections Framework gives you three things:
1. Interfaces. Common types that abstract over different data structures: Collection, List, Set, Queue, Map. Code written against these interfaces works with any implementation.
2. Implementations. Concrete classes that provide the interfaces: ArrayList, LinkedList, HashSet, TreeSet, HashMap, LinkedHashMap, and many more. Each has different performance characteristics.
3. Algorithms. Static methods in Collections and Arrays for sorting, searching, shuffling, etc. Write the algorithm once, run on any collection.
The split between interfaces and implementations is the framework's most important property. You can change a data structure (ArrayList → LinkedList) without changing the code that uses it — as long as the code uses the List interface, both implementations work.
A Quick Example
import java.util.*;
public class CollectionsExample {
public static void main(String[] args) {
// Declared as List (interface), implemented as ArrayList
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
// Sort using the framework's algorithm
Collections.sort(names);
// Iterate using enhanced for-loop
for (String name : names) {
System.out.println(name);
}
// Could swap implementation without changing usage
// List<String> names = new LinkedList<>(); // still works
}
}
The variable is typed as List<String> (the interface), not ArrayList<String> (the implementation). The code uses List operations. The choice of ArrayList is an implementation detail.
This pattern — declare to the interface, instantiate the implementation — is central to good Java code.
The Core Interface Hierarchy
The framework's main interfaces:
Iterable
↑
Collection
/ | \
List Set Queue
↑
SortedSet
↑
NavigableSet
Separately:
Map
↑
SortedMap
↑
NavigableMap
Note that Map is NOT a Collection. Maps are key-value pairs, not single-element collections. They have their own hierarchy.
We cover each interface in detail through this module and 14b. The key thing now: every implementation class implements one of these interfaces, and code should be written against the interface.
Why "Generic" Collections
Java's collections are generic — parameterized by type:
List<String> strings = new ArrayList<>(); // list of Strings
List<Integer> numbers = new ArrayList<>(); // list of Integers
List<Person> people = new ArrayList<>(); // list of Person objects
The <String> part is a type parameter (we cover Generics in Module 15). It tells the compiler "this list holds Strings, only let me add Strings to it, and methods like get return String."
Without generics (Java 1.4 and earlier), all collections held Object:
List names = new ArrayList(); // raw type — pre-generics
names.add("Alice");
String first = (String) names.get(0); // explicit cast, runtime check
You had to cast every time you read from the list. Type errors weren't caught until runtime.
Generics fixed this. Code is shorter, safer, and self-documenting. Always use parameterized types — List<String>, not raw List.
Implementation Classes Overview
For lists and sets (Map will come in 14b):
List implementations:
ArrayList— backed by an array. Fast random access, slow insertion at front. The default.LinkedList— doubly-linked list. Fast insertion anywhere, slow random access.Vector— like ArrayList but synchronized. Legacy. Don't use; use ArrayList instead.Stack— extends Vector. Legacy. UseDequeinstead (we cover in Module 14b).CopyOnWriteArrayList— thread-safe via copy-on-write. For specific concurrent use cases.
Set implementations:
HashSet— backed by a hash table. Fast operations, no ordering. The default.LinkedHashSet— like HashSet but preserves insertion order.TreeSet— sorted set, backed by a red-black tree.CopyOnWriteArraySet— thread-safe via copy-on-write.EnumSet— specialized for enum types. Very efficient.
We cover each in detail through this module.
When to Use What
A quick decision guide that we'll expand throughout the module:
Lists:
- ArrayList — almost always the default
- LinkedList — when you do many insertions/removals at the front
- Specialized lists — for specific concurrent or performance needs
Sets:
- HashSet — when you need fast contains/add/remove and don't care about order
- LinkedHashSet — when you want fast operations AND insertion order
- TreeSet — when you need sorted order or range operations
This is the 90% rule — these defaults work for most use cases. The other 10% of choices come up in specific scenarios we'll cover.
A Note on Concurrency
The basic collections (ArrayList, HashSet, etc.) are NOT thread-safe. Multiple threads modifying the same collection without synchronization causes data corruption.
For thread-safe collections, the framework provides:
Collections.synchronizedList(list)— wraps a list with synchronized methodsCopyOnWriteArrayList,ConcurrentHashMap, etc. — purpose-built concurrent classes- Immutable collections via
List.of(),Set.of()— safe to share since unmodifiable
We touch on concurrency briefly in this module and cover it deeply in Module 18 (Concurrency).
The point: don't share basic collections between threads without synchronization. It's a common source of bugs.
graph TD
A["Collections Framework"] --> B["Interfaces<br/>(List, Set, Map, Queue)"]
A --> C["Implementations<br/>(ArrayList, HashSet, etc.)"]
A --> D["Algorithms<br/>(sort, search, shuffle)"]
B --> E["Code against interfaces<br/>for flexibility"]
C --> F["Choose for performance<br/>characteristics"]
D --> G["Reusable across<br/>any implementation"]
H["Module 14a"] --> I["Lists and Sets"]
J["Module 14b"] --> K["Maps and HashMap deep-dive"]
style A fill:#7c3aed,color:#fff
style B fill:#10b981,color:#fff
style C fill:#3b82f6,color:#fff
style D fill:#f59e0b,color:#fff
This diagram shows the three things the Collections Framework provides. Interfaces like List, Set, Map, and Queue define common types — code written against these interfaces works with any implementation. Implementations like ArrayList, HashSet, and HashMap are the concrete classes that provide the interfaces — each has different performance characteristics. Algorithms like sort, search, and shuffle are static methods that work on any collection through the common interfaces. The key pattern: declare your variables as the interface type, instantiate the concrete implementation. This lets you change implementations later without changing the code that uses them. Module 14a covers Lists and Sets. Module 14b covers Maps and includes Nikhil's deep technical dive on HashMap internals.
A Real Production Pattern
In AWS code, collections are everywhere. Some examples from S3 work:
public class BucketInventory {
// List of objects to process — could be huge
private final List<S3Object> pendingObjects = new ArrayList<>();
// Set of bucket names we've already processed
private final Set<String> processedBuckets = new HashSet<>();
// Map for fast lookup by key (Module 14b)
private final Map<String, S3Object> objectsByKey = new HashMap<>();
public void addObject(S3Object object) {
pendingObjects.add(object);
objectsByKey.put(object.getKey(), object);
}
public boolean shouldProcessBucket(String bucketName) {
return !processedBuckets.contains(bucketName);
}
public List<S3Object> getObjectsInBucket(String bucketName) {
return pendingObjects.stream()
.filter(o -> o.getBucket().equals(bucketName))
.toList();
}
}
Three different collection types for three different access patterns:
Listfor ordered objects to process in sequenceSetfor membership checks (have we seen this bucket?)Mapfor fast lookup by key
This is typical Java code. Choosing the right collection for each access pattern matters.
Common Pitfalls
Warning: Declaring with concrete types. Use
List<String>, notArrayList<String>, unless you specifically need ArrayList-only methods.Using raw types. Always parameterize:
List<String>, notList.Using legacy classes. Vector, Stack, Hashtable are obsolete. Use modern equivalents.
Sharing without synchronization. Don't share basic collections between threads without thinking carefully.
Choosing the wrong collection. ArrayList for queue operations is slow. LinkedList for random access is slow. Match the type to the access pattern.
Why This Matters in Interviews
Interview: Collections framework questions are universal in Java interviews. "What's the difference between ArrayList and LinkedList?" "When would you use a Set vs a List?" "What's the difference between HashMap and TreeMap?"
Topic 14.12 covers the Lists/Sets Interview Lens. Module 14b's interview lens will cover Maps.
Try It
Exercise: Explore each:
- Create an
ArrayList<String>. Add a few strings. Print them.- Change the declaration to
List<String>(the interface). Verify code still works.- Change the implementation to
LinkedList. Verify code still works.- Create a
HashSet<String>. Add duplicate strings. Verify only unique ones are stored.- Use
Collections.sort()on the list. Note the in-place sort.Each exercise builds Collections Framework intuition.