foreach-ui logo
codeLanguages
account_treeDSA

Quick Actions

quizlock Random Quiz
trending_uplock Progress
  • 1
  • 2
  • 3
  • 4
  • quiz
Java
  • Implement custom generic collection classes and data structures
  • Create generic algorithms for sorting, searching, and manipulation
  • Build utility methods for advanced collection operations
  • Optimize collection performance and memory usage

Generic Collections Deep Dive

You know generics, type bounds, and wildcards. Time to apply them to real scenarios—building custom data structures and understanding how the Collections Framework actually works.

Understanding Collection Type Hierarchy

Here's how generics work across the collection hierarchy:

Iterable<E>
    └── Collection<E>
            ├── List<E>
            │       ├── ArrayList<E>
            │       └── LinkedList<E>
            ├── Set<E>
            │       ├── HashSet<E>
            │       └── TreeSet<E>
            └── Queue<E>
                    └── Deque<E>

Map<K,V>
    ├── HashMap<K,V>
    └── TreeMap<K,V>

Each interface and class is generic, allowing type-safe operations throughout.

Building Custom Generic Collections

A Type-Safe Stack

Let's build a stack that demonstrates key generic concepts:

public class Stack<E> {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_CAPACITY = 16;
    
    public Stack() {
        elements = new Object[DEFAULT_CAPACITY];
    }
    
    public void push(E element) {
        ensureCapacity();
        elements[size++] = element;
    }
    
    @SuppressWarnings("unchecked")
    public E pop() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        E result = (E) elements[--size];
        elements[size] = null;  // Help garbage collection
        return result;
    }
    
    @SuppressWarnings("unchecked")
    public E peek() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        return (E) elements[size - 1];
    }
    
    public boolean isEmpty() {
        return size == 0;
    }
    
    private void ensureCapacity() {
        if (size == elements.length) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

Why Object[] instead of E[]? Due to type erasure, you can't create a generic array directly (new E[capacity] is illegal). The workaround is using Object[] with careful casting.

Adding Iteration Support

To make our stack usable in for-each loops:

public class Stack<E> implements Iterable<E> {
    // ... previous code ...
    
    @Override
    public Iterator<E> iterator() {
        return new StackIterator();
    }
    
    private class StackIterator implements Iterator<E> {
        private int current = size - 1;  // Start from top
        
        @Override
        public boolean hasNext() {
            return current >= 0;
        }
        
        @Override
        @SuppressWarnings("unchecked")
        public E next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            return (E) elements[current--];
        }
    }
}

Now you can write:

Stack<String> stack = new Stack<>();
stack.push("first");
stack.push("second");
stack.push("third");

for (String item : stack) {
    System.out.println(item);  // third, second, first
}

The Pair and Tuple Pattern

Many operations return multiple values. Generic pairs and tuples solve this elegantly:

Immutable Pair

public final class Pair<A, B> {
    private final A first;
    private final B second;
    
    private Pair(A first, B second) {
        this.first = first;
        this.second = second;
    }
    
    public static <A, B> Pair<A, B> of(A first, B second) {
        return new Pair<>(first, second);
    }
    
    public A getFirst() { return first; }
    public B getSecond() { return second; }
    
    // Transform methods
    public <C> Pair<C, B> mapFirst(Function<A, C> mapper) {
        return Pair.of(mapper.apply(first), second);
    }
    
    public <C> Pair<A, C> mapSecond(Function<B, C> mapper) {
        return Pair.of(first, mapper.apply(second));
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Pair)) return false;
        Pair<?, ?> pair = (Pair<?, ?>) o;
        return Objects.equals(first, pair.first) && 
               Objects.equals(second, pair.second);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(first, second);
    }
    
    @Override
    public String toString() {
        return "(" + first + ", " + second + ")";
    }
}

Usage shows the power of type inference:

// Method returning multiple values
public Pair<User, List<Order>> getUserWithOrders(Long userId) {
    User user = userRepository.findById(userId);
    List<Order> orders = orderRepository.findByUser(userId);
    return Pair.of(user, orders);
}

// Usage
Pair<User, List<Order>> result = getUserWithOrders(123L);
User user = result.getFirst();
List<Order> orders = result.getSecond();

// Transform the pair
Pair<String, Integer> nameAndCount = result
    .mapFirst(User::getName)
    .mapSecond(List::size);

Generic Utility Methods

Type-Safe Collection Operations

public class CollectionUtils {
    
    // Find first element matching predicate
    public static <T> Optional<T> findFirst(Collection<T> collection, 
                                            Predicate<T> predicate) {
        for (T element : collection) {
            if (predicate.test(element)) {
                return Optional.of(element);
            }
        }
        return Optional.empty();
    }
    
    // Partition a collection by predicate
    public static <T> Pair<List<T>, List<T>> partition(Collection<T> collection,
                                                        Predicate<T> predicate) {
        List<T> matching = new ArrayList<>();
        List<T> nonMatching = new ArrayList<>();
        
        for (T element : collection) {
            if (predicate.test(element)) {
                matching.add(element);
            } else {
                nonMatching.add(element);
            }
        }
        
        return Pair.of(matching, nonMatching);
    }
    
    // Zip two lists together
    public static <A, B> List<Pair<A, B>> zip(List<A> listA, List<B> listB) {
        int size = Math.min(listA.size(), listB.size());
        List<Pair<A, B>> result = new ArrayList<>(size);
        
        for (int i = 0; i < size; i++) {
            result.add(Pair.of(listA.get(i), listB.get(i)));
        }
        
        return result;
    }
    
    // Create frequency map
    public static <T> Map<T, Long> frequency(Collection<T> collection) {
        Map<T, Long> result = new HashMap<>();
        for (T element : collection) {
            result.merge(element, 1L, Long::sum);
        }
        return result;
    }
}

Usage Examples

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Find first even number
Optional<Integer> firstEven = CollectionUtils.findFirst(numbers, n -> n % 2 == 0);
// Optional[2]

// Partition into even and odd
Pair<List<Integer>, List<Integer>> partitioned = 
    CollectionUtils.partition(numbers, n -> n % 2 == 0);
// ([2, 4, 6, 8, 10], [1, 3, 5, 7, 9])

// Zip names and ages
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<Integer> ages = Arrays.asList(25, 30, 35);
List<Pair<String, Integer>> people = CollectionUtils.zip(names, ages);
// [(Alice, 25), (Bob, 30), (Charlie, 35)]

// Count word frequency
List<String> words = Arrays.asList("apple", "banana", "apple", "cherry", "banana", "apple");
Map<String, Long> wordCounts = CollectionUtils.frequency(words);
// {apple=3, banana=2, cherry=1}

Generic Comparators and Sorting

Building Flexible Comparators

public class ComparatorUtils {
    
    // Compare by extracted key
    public static <T, U extends Comparable<U>> Comparator<T> comparing(
            Function<T, U> keyExtractor) {
        return (a, b) -> keyExtractor.apply(a).compareTo(keyExtractor.apply(b));
    }
    
    // Chain comparators (then compare by)
    public static <T> Comparator<T> thenComparing(
            Comparator<T> first, 
            Comparator<T> second) {
        return (a, b) -> {
            int result = first.compare(a, b);
            return result != 0 ? result : second.compare(a, b);
        };
    }
    
    // Null-safe comparator
    public static <T> Comparator<T> nullsFirst(Comparator<T> comparator) {
        return (a, b) -> {
            if (a == null && b == null) return 0;
            if (a == null) return -1;
            if (b == null) return 1;
            return comparator.compare(a, b);
        };
    }
}

Usage:

class Person {
    String name;
    int age;
    String city;
    // constructor, getters...
}

List<Person> people = /* ... */;

// Sort by name
people.sort(ComparatorUtils.comparing(Person::getName));

// Sort by age, then by name
Comparator<Person> byAgeThenName = ComparatorUtils.thenComparing(
    ComparatorUtils.comparing(Person::getAge),
    ComparatorUtils.comparing(Person::getName)
);
people.sort(byAgeThenName);

Choosing the Right Collection

Understanding when to use which collection is crucial:

List Implementations

Implementation Best For Avoid When
ArrayList<E> Random access, iteration Frequent insertions/deletions in middle
LinkedList<E> Insertions/deletions at ends Random access by index

Set Implementations

Implementation Best For Characteristics
HashSet<E> Fast lookup, no order needed O(1) operations, unordered
LinkedHashSet<E> Fast lookup with insertion order Slightly slower than HashSet
TreeSet<E> Sorted elements O(log n) operations

Map Implementations

Implementation Best For Characteristics
HashMap<K,V> General purpose O(1) operations, unordered
LinkedHashMap<K,V> Insertion/access order Good for LRU caches
TreeMap<K,V> Sorted keys O(log n) operations

Working with Generics and Inheritance

Covariant Return Types

When overriding methods, you can return a more specific type:

interface Container<E> {
    Container<E> copy();
}

class ArrayList<E> implements Container<E> {
    @Override
    public ArrayList<E> copy() {  // More specific return type
        return new ArrayList<>(this);
    }
}

Generic Methods in Interfaces

interface Transformer<T> {
    // Regular method using class type parameter
    T transform(T input);
    
    // Generic method with its own type parameter
    <R> R transformTo(T input, Function<T, R> converter);
}

Building custom generic collections means using Object[] internally and casting carefully. Implement Iterable<E> for for-each support. Make them immutable when you can. Provide static factory methods for cleaner APIs. The Collections Framework uses all these patterns—now you know why.

© 2026 forEach. All rights reserved.

Privacy Policy•Terms of Service