AlgoInsight

Free lesson · Java Basic To Advance

Introduction to Collections

Medium6 min readjava

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:

Set implementations:

We cover each in detail through this module.

When to Use What

A quick decision guide that we'll expand throughout the module:

Lists:

Sets:

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:

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:

This is typical Java code. Choosing the right collection for each access pattern matters.

Common Pitfalls

Warning: Declaring with concrete types. Use List<String>, not ArrayList<String>, unless you specifically need ArrayList-only methods.

Using raw types. Always parameterize: List<String>, not List.

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:

  1. Create an ArrayList<String>. Add a few strings. Print them.
  2. Change the declaration to List<String> (the interface). Verify code still works.
  3. Change the implementation to LinkedList. Verify code still works.
  4. Create a HashSet<String>. Add duplicate strings. Verify only unique ones are stored.
  5. Use Collections.sort() on the list. Note the in-place sort.

Each exercise builds Collections Framework intuition.

Back to Java Basic To Advance