Java Programming

Generics and the Collection Framework

PGCP-BDA

generic type

A generic type or method introduces type parameters so one implementation can enforce compile-time relationships among several concrete types.

type parameter

A named type variable declared by a generic class, interface, method or constructor.

bounded type

A type parameter restricted by an upper bound, such as T extends Number, so bounded members can be used safely.

wildcard

A use-site generic type argument written ?, optionally bounded with extends or super to control readable and writable types.

type erasure

Java implements most generics by checking and translating parameterized types at compile time.

collection framework

The collection framework defines interfaces and implementations for lists, sets, queues, deques and maps together with iteration and algorithms.

List Set Queue Map

List preserves positional elements, Set enforces uniqueness, Queue orders processing and Map associates unique keys with values.

iterator

An object that visits collection elements sequentially through hasNext and next without exposing the collection’s representation.

type safety

Type safety rejects incompatible operations before execution and reduces casts, while runtime checks still protect erased generic boundaries.

The collection hierarchy

Collection<E> is the root interface for groups of elements. Its main subinterfaces include List, Set, Queue and Deque. Map<K,V> belongs to the collections framework but does not extend Collection because it stores key–value associations rather than individual elements.

AbstractionCentral promiseTypical use
ListOrdered sequence; duplicates allowedRows, history, indexed values
SetUnique elementsMembership, deduplication
QueueElements waiting for processingTasks, breadth-first traversal
DequeInsert and remove at both endsStack or double-ended queue
MapUnique keys mapped to valuesLookup tables, indexes

Program variables should normally use the least specific interface that expresses the required operations:

List<String> names = new ArrayList<>();
Set<String> tags = new HashSet<>();
Map<Long, Student> students = new HashMap<>();

This keeps the implementation replaceable and prevents code from depending accidentally on irrelevant details.

Erasure and generic restrictions

Java implements most generics through type erasure. Type arguments guide compilation, while ArrayList<String> and ArrayList<Integer> normally share the same runtime class. The compiler inserts casts and may generate bridge methods to preserve polymorphism.

Consequences include:

  • code cannot use new T() because the runtime constructor is unknown;
  • code cannot create new T[10] directly;
  • instanceof List<String> is illegal because the argument is erased;
  • a class cannot implement the same generic interface with two different arguments;
  • overloads whose signatures erase to the same form clash.

Generic type arguments must be reference types, so primitives use wrappers. Varargs with non-reifiable generic types can cause heap-pollution warnings.

Generic classes and methods

Generics express relationships between types at compile time:

class Box<T> {
    private T value;
    void set(T value) { this.value = value; }
    T get() { return value; }
}

Box<String> box = new Box<>();
box.set("Java");

A generic method declares its type parameter before the return type:

static <T> T first(List<T> values) {
    if (values.isEmpty()) throw new NoSuchElementException();
    return values.get(0);
}

Bounds state required capabilities:

static <T extends Comparable<? super T>> T maximum(List<T> values) {
    return Collections.max(values);
}

Multiple bounds place a class first, followed by interfaces: <T extends Base & Runnable>.

Lower-bounded and unbounded wildcards

? super Integer means an unknown type that is Integer or one of its supertypes:

static void addDefaults(List<? super Integer> destination) {
    destination.add(0);
    destination.add(1);
}

It accepts List<Integer>, List<Number> or List<Object>. Reading yields Object because the exact element type is unknown. “Consumer super” describes a destination that consumes Integer values.

List<?> means a list of an unknown type. It is useful when operations do not depend on the element type, such as size, iteration as Object or clearing. It is safer than raw List because the unknown type remains enforced.

Upper-bounded wildcards

? extends Number means “some unknown type that is Number or a subtype.” Values can be read as Number:

static double total(List<? extends Number> values) {
    double sum = 0;
    for (Number value : values) sum += value.doubleValue();
    return sum;
}

The method accepts List<Integer>, List<Double> and similar lists. It cannot safely add an Integer because the actual list might be a List<Double>. It may add null, though doing so is rarely useful.

The mnemonic “producer extends” describes a structure from which the method consumes produced values.

Backed views and immutable factories

map.keySet(), values() and entrySet() usually return views backed by the map. Removing a key through the key-set view removes the corresponding map entry. Adding directly to that key set is unsupported because no value is available.

subList is also a backed view, so structural changes require care. Arrays.asList(array) creates a fixed-size list backed by the array: set is supported, while add and remove are not.

List.of, Set.of and Map.of create unmodifiable collections that reject null. “Unmodifiable” does not make mutable element objects immutable. Collections.unmodifiableList(source) is a read-only view; changes made through the original source remain visible.

Queue and Deque operations

Queue operations come in exception-throwing and special-value forms:

PurposeThrows on failureSpecial-value form
Insertadd(e)offer(e)
Remove headremove()poll()
Examine headelement()peek()

poll and peek return null for an empty queue, so queues using these conventions generally should not store null.

ArrayDeque efficiently supports both ends. Use addFirst, removeFirst, addLast and removeLast for deque behaviour. It also replaces the legacy Stack: push, pop and peek operate at the front.

Invariance

Generic types are invariant. Although Integer extends Number, List<Integer> is not a subtype of List<Number>.

If it were, this unsafe sequence would become possible:

// Hypothetical and illegal:
List<Integer> integers = new ArrayList<>();
// List<Number> numbers = integers;
// numbers.add(3.14); // would corrupt the integer list

A List<Number> may contain Integer, Double or other Number values. A List<Integer> permits only Integer values. Wildcards express safe variance at a use site.

Raw types and heap pollution

A raw type omits a generic argument:

List raw = new ArrayList<String>();
raw.add(42);                       // warning
List<String> text = raw;           // unchecked warning
String value = text.get(0);        // ClassCastException

Raw types exist mainly for compatibility with code written before generics. They disable useful compile-time checks and can move a type error far from its cause. Do not silence unchecked warnings until the operation has been proven safe and isolated.

Collection algorithms and utilities

Collections is a utility class containing algorithms and wrappers; Collection is an interface. Common operations include sort, binarySearch, reverse, shuffle, min, max and unmodifiable or synchronized wrappers.

Binary search requires data already sorted under the same ordering used by the search. Violating this prerequisite gives an undefined insertion-position result rather than performing a linear fallback.

List.copyOf and related copy factories create unmodifiable snapshots of elements, while wrapper methods create views. The distinction determines whether later source changes are visible.

ArrayList and LinkedList

ArrayList stores references in a resizable array. Indexed access is constant time, appending is amortized constant time and middle insertion or removal shifts later elements.

“Amortized O(1)” means that occasional growth requires copying, but the average cost across a sequence of appends remains constant. It does not promise that every individual append has identical cost.

LinkedList stores linked nodes and implements both List and Deque. Adding or removing at a known end is constant time, but locating an index requires traversal. It also uses extra memory per node and has poor memory locality. Consequently, ArrayList is the better default for most list workloads. Use a deque implementation such as ArrayDeque for frequent end operations.

Vector is a legacy synchronized resizable list. Its individual operations are synchronized, but a multi-step action still needs an atomic design.

Continue learning

Related notes

Put this topic into timed practice

Open mock tests when you want full-exam pacing, or keep drilling in practice mode.