Generics with Collections in Java: Lists, Sets, and Maps

Illustration for Generics with Collections in Java: Lists, Sets, and Maps
By Last updated:

The Collections Framework is one of the most widely used features in Java. Before Generics (Java 5), collections stored elements as Object, requiring explicit casting during retrieval. This often led to runtime errors such as ClassCastException. With generics, Java introduced compile-time type safety, making collections more robust, reusable, and easier to maintain.

Think of generics in collections like labeled storage boxes. Without labels (Object), you could put anything inside and risk pulling out the wrong item. With labels (List<String>), you ensure only the right type goes in or out, reducing errors.

This guide explores how generics power Lists, Sets, and Maps, their use of wildcards, type bounds, and real-world best practices.


Core Definition and Purpose of Java Generics

Generics allow developers to:

  1. Ensure Type Safety – Catch invalid types at compile time.
  2. Promote Reusability – Write one collection API usable for all types.
  3. Simplify Code – Eliminate unnecessary casts.

Introduction to Type Parameters: <T>, <E>, <K, V>

  • <T>: General type (Type).
  • <E>: Element (used in List<E>, Set<E>).
  • <K, V>: Key-Value pair (used in Map<K, V>).

Example:

List<String> names = new ArrayList<>();
names.add("Alice");
String first = names.get(0); // No casting needed

Generic Classes with Examples

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

Generic Methods: Declaration and Usage

public static <T> void printList(List<T> list) {
    for (T item : list) {
        System.out.println(item);
    }
}

Bounded Type Parameters: extends and super

public static <T extends Number> double sum(List<T> numbers) {
    double result = 0;
    for (T num : numbers) result += num.doubleValue();
    return result;
}

Wildcards Explained: ?, ? extends, ? super

  • ? – Unknown type.
  • ? extends T – Upper bound (producer).
  • ? super T – Lower bound (consumer).

PECS Principle: Producer Extends, Consumer Super.

public static void printNumbers(List<? extends Number> list) {
    for (Number n : list) System.out.println(n);
}

public static void addIntegers(List<? super Integer> list) {
    list.add(42);
}

Generics in Collections Framework

1. Lists

List<String> list = new ArrayList<>();
list.add("Java");
list.add("Generics");
for (String s : list) System.out.println(s);
  • Ordered, allow duplicates.
  • ArrayList, LinkedList.

2. Sets

Set<Integer> set = new HashSet<>();
set.add(10);
set.add(20);
set.add(10); // Ignored (duplicate)
  • No duplicates, no order guarantee.
  • HashSet, TreeSet, LinkedHashSet.

3. Maps

Map<String, Integer> map = new HashMap<>();
map.put("Age", 30);
map.put("Year", 2025);
System.out.println(map.get("Age"));
  • Key-Value storage.
  • HashMap, TreeMap, LinkedHashMap.

Raw Types vs Parameterized Types

List list = new ArrayList(); // Raw type (unsafe)
list.add(100);
list.add("Java"); // Runtime risk

List<String> safeList = new ArrayList<>(); // Type-safe

Type Inference and Diamond Operator

Map<String, Integer> map = new HashMap<>(); // Diamond operator infers types

Type Erasure in Java

Generics exist at compile time only. At runtime, type information is erased:

  • Compiler enforces type checks.
  • JVM runs raw types.

This explains why you cannot new T() or instanceof List<String>.


Reifiable vs Non-Reifiable Types

  • Reifiable Types: Known at runtime (List<?>).
  • Non-Reifiable Types: Lost at runtime (List<String>).

Recursive Type Bounds

public static <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) > 0 ? a : b;
}

Designing Fluent APIs and Builders with Generics

class Builder<T extends Builder<T>> {
    public T withName(String name) { return (T) this; }
}

Generics with Enums and Annotations

EnumSet<Day> days = EnumSet.of(Day.MONDAY, Day.FRIDAY);
@SuppressWarnings("unchecked")

Generics with Exceptions

  • Cannot catch (T e).
  • Cannot throw new T().

Generics and Reflection

Reflection extracts generic info with ParameterizedType:

Field field = MyClass.class.getDeclaredField("list");
Type type = field.getGenericType();

Case Studies

Type-Safe Cache

class Cache<K, V> {
    private Map<K, V> store = new HashMap<>();
    public void put(K key, V value) { store.put(key, value); }
    public V get(K key) { return store.get(key); }
}

Flexible Repository Pattern

interface Repository<T, ID> {
    void save(T entity);
    T findById(ID id);
}

Event Handling

interface EventListener<T> {
    void onEvent(T event);
}

Best Practices for Generics in Collections

  • Prefer generics over raw types.
  • Use wildcards for API flexibility.
  • Apply PECS properly.
  • Favor List over ArrayList in variable declarations (use interfaces).
  • Keep type signatures clean.

Common Anti-Patterns

  • Suppressing warnings unnecessarily.
  • Using raw types in modern code.
  • Overly complex nested generics.

Performance Considerations

Generics do not slow performance — they are enforced at compile time and erased at runtime. Collections remain efficient while safer to use.


📌 What's New in Java for Generics?

  • Java 5: Generics introduced in Collections.
  • Java 7: Diamond operator (<>).
  • Java 8: Streams use generics heavily (map, filter).
  • Java 10: var works with generics.
  • Java 17+: Sealed classes work with generic hierarchies.
  • Java 21: Virtual threads enable scalable concurrency with generics.

Conclusion and Key Takeaways

Generics revolutionized the Collections Framework by making List, Set, and Map type-safe and reusable. They prevent runtime ClassCastException, simplify APIs, and support advanced use cases with bounds, wildcards, and type inference. By mastering generics with collections, developers can write cleaner, safer, and more flexible code.


FAQ on Generics with Collections

Q1: Why can’t I use raw types in collections?
Because they bypass compile-time safety, leading to runtime errors.

Q2: What’s the difference between List<Object> and List<?>?
List<Object> accepts only Object or subtypes, List<?> accepts any type.

Q3: Can I store primitives in collections?
No, use wrapper types (Integer, Double).

Q4: Why can’t I create arrays of generics?
Because arrays are reifiable, but generics are erased.

Q5: How does the PECS principle apply to collections?
Use ? extends for producers, ? super for consumers.

Q6: How do generics prevent ClassCastException?
By enforcing compile-time type checks.

Q7: Can I use wildcards in Maps?
Yes, e.g., Map<? extends Number, ? super String>.

Q8: Are generics slower in collections?
No, erasure ensures no runtime cost.

Q9: How are generics used in Streams API?
Stream methods (map, filter) rely on generics for type safety.

Q10: What’s the best practice for declaring collections?
Use interfaces (List, Set, Map) in variable declarations, not implementations.