Index

Advanced Collection Patterns and Best Practices

Java Collections

15.1 Immutable Collections (Java 9 List.of, Set.of, Map.of)

Immutable collections are collections whose elements cannot be added, removed, or modified once they are created. In Java, immutability offers several benefits:

Java 9 Factory Methods

Starting with Java 9, the List, Set, and Map interfaces introduced convenient static factory methods: List.of(), Set.of(), and Map.of(). These allow concise creation of immutable collections.

import java.util.List;
import java.util.Set;
import java.util.Map;

public class ImmutableCollectionsDemo {
    public static void main(String[] args) {
        List<String> fruits = List.of("apple", "banana", "cherry");
        Set<Integer> numbers = Set.of(1, 2, 3);
        Map<String, Integer> ageMap = Map.of("Alice", 30, "Bob", 25);

        System.out.println(fruits);
        System.out.println(numbers);
        System.out.println(ageMap);
    }
}

Output:

[apple, banana, cherry]
[1, 2, 3]
{Alice=30, Bob=25}

These collections are immutable. Any attempt to modify them throws an UnsupportedOperationException.

fruits.add("orange"); // Throws UnsupportedOperationException

Differences from Collections.unmodifiableXXX()

Prior to Java 9, immutability was achieved using wrappers like:

List<String> modifiable = new ArrayList<>();
List<String> unmodifiable = Collections.unmodifiableList(modifiable);

However, this only creates a view of the original collection that prevents direct modifications. If the original collection is changed, the unmodifiable view reflects those changes—meaning it's not truly immutable.

In contrast, List.of() and its siblings create genuinely immutable collections that cannot be changed by any reference.

Key Characteristics

List<String> badList = List.of("a", null); // Throws NullPointerException

Use Cases

Summary

Immutable collections introduced in Java 9 through factory methods (List.of, Set.of, Map.of) are concise, null-safe, and truly immutable. They improve program safety and simplify concurrent code by eliminating unintended side effects. When you need fixed, read-only data structures, prefer these new factory methods over older wrapper-based approaches.

Index

15.2 Builder Patterns for Collections

The Builder Pattern is a design pattern that provides a flexible and readable way to construct complex objects step-by-step. When applied to collections, builders offer a fluent and expressive way to assemble data structures—especially when the collection requires a complex setup or when immutability is desired.

Why Use Builder Patterns for Collections?

When to Prefer Builder Over Constructors or Factories

Traditional constructors or factory methods like List.of() are great for simple, fixed sets of data. But when:

…a builder pattern becomes more appropriate and maintainable.

Using Java's Built-in Collectors with Streams

In Java, the Collectors utility class includes a toUnmodifiableList(), which works well with streams to simulate a builder-like pipeline:

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class StreamBuilderExample {
    public static void main(String[] args) {
        List<String> names = Stream.of("Alice", "Bob", "Charlie")
                .filter(name -> name.startsWith("A") || name.startsWith("C"))
                .collect(Collectors.toUnmodifiableList());

        System.out.println(names); // [Alice, Charlie]
    }
}

This method creates an immutable collection through a builder-like fluent chain of operations.

Implementing a Custom Builder

You can also create your own builder for more controlled scenarios:

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

class ListBuilder<T> {
    private final List<T> items = new ArrayList<>();

    public ListBuilder<T> add(T item) {
        items.add(item);
        return this;
    }

    public ListBuilder<T> addAll(List<T> otherItems) {
        items.addAll(otherItems);
        return this;
    }

    public List<T> build() {
        return Collections.unmodifiableList(new ArrayList<>(items));
    }
}

public class CustomBuilderDemo {
    public static void main(String[] args) {
        List<String> names = new ListBuilder<String>()
                .add("Apple")
                .add("Banana")
                .add("Cherry")
                .build();

        System.out.println(names); // [Apple, Banana, Cherry]
        // names.add("Date"); // Throws UnsupportedOperationException
    }
}

This builder allows step-by-step construction and produces an immutable list at the end.

Best Practices

Summary

Builder patterns offer a flexible and expressive way to construct complex or immutable collections. They’re ideal when collection construction is conditional, iterative, or involves computed values. Java 9’s unmodifiable collections and fluent stream collectors work well for simple needs, while custom builders give fine-grained control and readability in more complex scenarios.

Index

15.3 Custom Collection Implementations

While Java’s standard collection classes (like ArrayList, HashSet, HashMap) cover most needs, there are scenarios where creating a custom collection becomes valuable. These include:

Key Interfaces for Custom Collections

To implement a custom collection, Java provides several interfaces:

When building a custom collection, you'll typically extend an abstract base class like:

These classes provide default implementations for many methods, allowing you to focus only on the essential overrides.

Design Considerations

1. Mutability: Decide if your collection should allow modifications. If immutable, throw UnsupportedOperationException in methods like add() or remove().

2. Thread-safety: Will the collection be accessed concurrently? If so, consider using synchronization or concurrent-friendly data structures.

3. Performance: Analyze time and space complexity. Avoid unnecessary copying or boxing.

Example: A Custom Read-Only List That Logs Access

The following is a simple wrapper over a list that logs every get() call:

import java.util.AbstractList;
import java.util.List;

public class LoggingList<E> extends AbstractList<E> {
    private final List<E> internalList;

    public LoggingList(List<E> list) {
        this.internalList = list;
    }

    @Override
    public E get(int index) {
        E value = internalList.get(index);
        System.out.println("Accessed index " + index + ": " + value);
        return value;
    }

    @Override
    public int size() {
        return internalList.size();
    }

    // Making it read-only
    @Override
    public E set(int index, E element) {
        throw new UnsupportedOperationException("This list is read-only");
    }

    @Override
    public boolean add(E e) {
        throw new UnsupportedOperationException("This list is read-only");
    }

    @Override
    public E remove(int index) {
        throw new UnsupportedOperationException("This list is read-only");
    }

    public static void main(String[] args) {
        List<String> original = List.of("Alpha", "Beta", "Gamma");
        LoggingList<String> loggingList = new LoggingList<>(original);

        // Test access
        System.out.println(loggingList.get(1));  // Logs and prints "Beta"
        // loggingList.add("Delta");             // Throws UnsupportedOperationException
    }
}

Explanation:

Summary

Custom collection implementations allow developers to tailor behavior, enforce rules, or optimize performance beyond what standard collections offer. By extending abstract base classes and carefully considering mutability and concurrency, you can integrate custom logic while staying compatible with Java’s collection framework. Always ensure your implementation adheres to the contracts of the interfaces to avoid unexpected behavior.

Index

15.4 Runnable Examples: Creating immutable collections and custom data structures

This section provides practical, self-contained examples that illustrate how to:

  1. Create immutable collections using Java 9+ factory methods,
  2. Use a builder pattern to construct collections,
  3. Implement a simple custom collection with specialized behavior.

Creating Immutable Collections with List.of, Set.of, Map.of

import java.util.List;
import java.util.Set;
import java.util.Map;

public class ImmutableDemo {
    public static void main(String[] args) {
        List<String> list = List.of("Java", "Python", "Go");
        Set<Integer> set = Set.of(1, 2, 3);
        Map<String, Integer> map = Map.of("A", 1, "B", 2);

        // Uncommenting below lines will throw UnsupportedOperationException
        // list.add("Rust");
        // set.remove(2);
        // map.put("C", 3);

        System.out.println("Immutable List: " + list);
        System.out.println("Immutable Set: " + set);
        System.out.println("Immutable Map: " + map);
    }
}

Explanation: Java 9 introduced convenient static methods to create immutable collections. These are thread-safe, concise, and ideal for constants or read-only views.

Builder Pattern for Collection Construction

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

class CollectionBuilder<T> {
    private final List<T> items = new ArrayList<>();

    public CollectionBuilder<T> add(T item) {
        items.add(item);
        return this;
    }

    public List<T> buildImmutable() {
        return Collections.unmodifiableList(new ArrayList<>(items));
    }
}

public class BuilderDemo {
    public static void main(String[] args) {
        List<String> list = new CollectionBuilder<String>()
                .add("One")
                .add("Two")
                .add("Three")
                .buildImmutable();

        System.out.println("Built Immutable List: " + list);
        // list.add("Four"); // Throws UnsupportedOperationException
    }
}

Explanation: This builder provides a readable, chainable way to add elements and create an immutable result, improving maintainability.

Simple Custom Collection: LoggingSet

import java.util.AbstractSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

class LoggingSet<E> extends AbstractSet<E> {
    private final Set<E> internalSet = new HashSet<>();

    @Override
    public boolean add(E e) {
        System.out.println("Adding: " + e);
        return internalSet.add(e);
    }

    @Override
    public Iterator<E> iterator() {
        return internalSet.iterator();
    }

    @Override
    public int size() {
        return internalSet.size();
    }
}

public class CustomSetDemo {
    public static void main(String[] args) {
        Set<String> logSet = new LoggingSet<>();
        logSet.add("Apple");
        logSet.add("Banana");
        logSet.add("Apple"); // Duplicate, won't be added again

        System.out.println("Set contents: " + logSet);
    }
}

Explanation: This LoggingSet overrides add() to print added elements. It wraps HashSet and can be extended further for validation, statistics, etc.

Summary

These examples demonstrate best practices for modern Java collections:

Together, these patterns promote safer, more maintainable, and purpose-fit collection usage in real-world Java applications.

Index