Java Generics in Collections – Explained with Examples

Illustration for Java Generics in Collections – Explained with Examples
By Last updated:

Java Generics allow developers to write type-safe, reusable, and flexible code, especially when dealing with the Collections Framework. Without generics, we would be stuck with type casting and runtime errors. With generics, we get compile-time type checking, better readability, and improved developer productivity.

In this tutorial, we'll break down how generics are used in Lists, Sets, Maps, and other collections—plus explain wildcard types, bounded types, and functional programming support.


📚 What Are Generics in Java?

Generics enable classes, interfaces, and methods to operate on typed parameters without specifying the actual type at the time of implementation.

Before Generics

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // Type cast required

With Generics

List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // No cast needed

🧩 Why Generics Matter in Collections

  • Type safety: Catch errors at compile time
  • Eliminate casting: Cleaner, safer code
  • Support polymorphism: Through wildcards (?), bounded types
  • Enable code reuse: Generic methods work across multiple types

🔧 Generic Syntax in Collections

List

List<Integer> numbers = new ArrayList<>();

Set

Set<String> names = new HashSet<>();

Map

Map<String, Integer> ageMap = new HashMap<>();

🎯 Wildcards and Bounds

Unbounded Wildcard

List<?> list = new ArrayList<String>();

Used when the type is unknown but you don’t plan to modify it.

Upper Bounded Wildcard (extends)

List<? extends Number> list = new ArrayList<Integer>();

Used for reading values (covariant).

Lower Bounded Wildcard (super)

List<? super Integer> list = new ArrayList<Number>();

Used for writing values (contravariant).


💡 Generic Methods in Action

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

⚙️ Internals and Compilation

  • Generics use type erasure: types are erased at compile-time and replaced with Object or bounds.
  • No generic arrays allowed: new List<String>[] is illegal.
  • Cannot use primitive types directly (use wrapper types like Integer, Double)

🚀 Performance Considerations

Feature Without Generics With Generics
Compile-time checks
Type casting Required Avoided
Runtime errors Possible Minimized

🌍 Real-World Use Cases

  • Map<String, List<User>>: Grouping users by category
  • List<? extends Shape>: Rendering shapes from multiple subtypes
  • Set<Predicate<String>>: Managing filters dynamically

✅ Best Practices

  • Always prefer generic types for collections
  • Use wildcards in method parameters if needed (List<?>, List<? extends T>)
  • Avoid raw types (List list)
  • Use bounded generics to express constraints
  • Don’t mix raw and parameterized types

🔄 Java 8+ Functional Style with Generics

Map<String, List<String>> data = new HashMap<>();
data.values().stream()
    .flatMap(List::stream)
    .filter(s -> s.length() > 2)
    .forEach(System.out::println);

📌 What's New in Java?

Java 8

  • Lambdas + Streams with generics
  • Functional interfaces: Predicate<T>, Function<T, R>

Java 9

  • Factory methods with generics: List.of("a", "b")
  • Diamond operator <>() improvements

Java 10+

  • var works with generics for local inference

Java 21

  • Pattern matching and structured concurrency: works well with generic collections for data processing

📘 Conclusion and Key Takeaways

  • Generics make collections safer, more powerful, and reusable.
  • Understand wildcard types (?, ? extends, ? super) to write flexible APIs.
  • Use functional programming idioms with generics for expressive and concise code.
  • Avoid raw types and embrace parameterization.

❓ FAQ – Java Generics in Collections

  1. Can I use primitive types in generics?
    No, use wrapper classes like Integer, Double.

  2. What is type erasure?
    Java removes generic type information during compilation.

  3. What’s the difference between <?> and <T>?
    <?> is an unknown wildcard; <T> is a declared generic type.

  4. What happens if I mix raw and generic types?
    It compiles with a warning and can lead to runtime issues.

  5. Can I create an array of generics?
    No, generics and arrays don’t mix due to type erasure.

  6. When to use ? extends vs ? super?
    Use ? extends to read, ? super to write.

  7. Can I overload a method by changing the generic type?
    No, type erasure makes that illegal.

  8. What is a generic method?
    A method that works on generic types, declared as <T> T method(...).

  9. Are wildcards needed in return types?
    Usually not. Use them in method arguments for flexibility.

  10. What’s a common anti-pattern with generics?
    Using raw types like List instead of List<T>.