---
title: "Generics in Java - Generic Classes, Methods, Wildcards, PECS | Modern Age Coders"
description: "Master Java generics including generic classes, generic methods, bounded type parameters, wildcards (? extends, ? super), PECS principle, type erasure, and generic interfaces. Includes 42+ practice questions for interviews and placement preparation."
slug: generics-in-java
canonical: https://learn.modernagecoders.com/resources/java/generics-in-java/
category: "Java"
keywords: ["java generics", "generic class java", "wildcard java", "bounded type parameter java", "java pecs principle", "java type erasure", "java generic method", "java generics tutorial", "java generics interview questions"]
---
# Generics in Java

**Difficulty:** Advanced  
**Reading time:** 45 min  
**Chapter:** 20  
**Practice questions:** 42

## What Are Generics in Java?

**Generics** allow you to write classes, interfaces, and methods that operate on a type parameter instead of a specific type. Instead of writing separate `IntBox`, `StringBox`, and `DoubleBox` classes, you write one `Box` that works with any type.

```
// Without generics (before Java 5)
ArrayList list = new ArrayList();
list.add("Hello");
list.add(42);            // No error at compile time!
String s = (String) list.get(1);  // ClassCastException at runtime!

// With generics
ArrayList list = new ArrayList<>();
list.add("Hello");
// list.add(42);          // Compilation error! Type safety enforced.
String s = list.get(0);  // No cast needed.
```

Generics were introduced in Java 5 to provide **compile-time type safety** and **eliminate the need for explicit casting**. The type parameter (like `T`) is a placeholder that gets replaced by a concrete type when the generic class or method is used.

```
// Generic class
class Box {
    private T value;
    
    Box(T value) { this.value = value; }
    T getValue() { return value; }
}

// Usage
Box stringBox = new Box<>("Hello");
Box intBox = new Box<>(42);

String s = stringBox.getValue();  // No cast
int n = intBox.getValue();        // No cast (auto-unboxing)
```

## Why Do Generics Matter?

Before generics, Java collections held `Object` references, requiring unsafe casting at every retrieval. Generics transformed Java from a loosely typed collections world into a type-safe one.

### 1. Type Safety at Compile Time

Without generics, type errors surface at runtime as `ClassCastException`. With generics, the compiler catches type mismatches before the code runs. When Arjun writes `ArrayList`, the compiler guarantees that only `Student` objects go in. No surprises at runtime.

### 2. Elimination of Casting

Without generics: `String s = (String) list.get(0);`. With generics: `String s = list.get(0);`. The compiler knows the type, so no explicit cast is needed. This reduces boilerplate and potential errors.

### 3. Code Reusability

A single generic class or method works with all types. Kavya writes `Pair<K, V>` once and uses it as `Pair<String, Integer>`, `Pair<Student, Grade>`, or any other combination. One implementation, infinite type combinations.

### 4. Algorithm Abstraction

Generic methods allow writing type-independent algorithms. A `max` method works for any `Comparable` type without knowing whether it compares Integers, Strings, or custom objects.

### 5. Foundation of the Collections Framework

Every collection class (`ArrayList`, `HashMap<K,V>`, `HashSet`) uses generics. Understanding generics is essential to understanding collections.

### 6. Interview Importance

Generics, particularly wildcards and the PECS principle, are among the trickiest Java interview topics. Questions about type erasure, bounded types, and wildcard captures are common in placement interviews at product-based companies.

## Detailed Explanation

### 1. Generic Classes

A generic class declares one or more type parameters in angle brackets after the class name:

```
class Pair<K, V> {
    private K key;
    private V value;
    
    Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    K getKey() { return key; }
    V getValue() { return value; }
    
    @Override
    public String toString() {
        return key + " = " + value;
    }
}

// Usage
Pair<String, Integer> age = new Pair<>("Arjun", 20);
Pair<String, String> city = new Pair<>("Kavya", "Bangalore");

String name = age.getKey();    // No cast
int years = age.getValue();    // No cast (auto-unboxing)
```

Common type parameter naming conventions: `T` (Type), `E` (Element), `K` (Key), `V` (Value), `N` (Number), `S, U` (additional types).

### 2. Generic Methods

A generic method declares its own type parameter(s), independent of the class's type parameters:

```
class Utility {
    // Generic method:  before return type
    static  void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
    
    static  T getFirst(T[] array) {
        return array[0];
    }
}

// Usage: type is inferred from arguments
Integer[] ints = {1, 2, 3, 4, 5};
String[] strs = {"A", "B", "C"};

Utility.printArray(ints);   // 1 2 3 4 5
Utility.printArray(strs);   // A B C

Integer first = Utility.getFirst(ints);  // 1
String firstStr = Utility.getFirst(strs);  // A
```

### 3. Bounded Type Parameters

You can restrict type parameters to certain types using `extends`:

```
// T must be a subclass of Number
class NumericBox {
    private T value;
    
    NumericBox(T value) { this.value = value; }
    
    double doubleValue() {
        return value.doubleValue();  // Safe: Number has doubleValue()
    }
}

NumericBox intBox = new NumericBox<>(42);
NumericBox dblBox = new NumericBox<>(3.14);
// NumericBox strBox = new NumericBox<>("Hi");  // Compilation error!
```

### 4. Multiple Bounds

A type parameter can have multiple bounds using `&`:

```
// T must extend Number AND implement Comparable
class SortableBox> {
    private T value;
    
    SortableBox(T value) { this.value = value; }
    
    boolean isGreaterThan(SortableBox other) {
        return this.value.compareTo(other.value) > 0;
    }
}

SortableBox a = new SortableBox<>(10);
SortableBox b = new SortableBox<>(5);
System.out.println(a.isGreaterThan(b));  // true
```

With multiple bounds, the class must come first, followed by interfaces: `T extends ClassName & Interface1 & Interface2`.

### 5. Wildcards

Wildcards (`?`) represent an unknown type. They are used in method parameters when you want flexibility:

#### Unbounded Wildcard: `?`

```
// Accepts a list of any type
void printList(List<?> list) {
    for (Object element : list) {
        System.out.println(element);
    }
}

printList(List.of(1, 2, 3));       // Works
printList(List.of("A", "B"));      // Works
printList(List.of(new Student()));  // Works
```

#### Upper Bounded Wildcard: `? extends T`

```
// Accepts List of Number or any subclass of Number
double sum(List<? extends Number> list) {
    double total = 0;
    for (Number n : list) {
        total += n.doubleValue();
    }
    return total;
}

sum(List.of(1, 2, 3));           // List works
sum(List.of(1.5, 2.5, 3.5));     // List works
// sum(List.of("A", "B"));       // List fails (String is not Number)
```

#### Lower Bounded Wildcard: `? super T`

```
// Accepts List of Integer or any superclass of Integer
void addIntegers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    list.add(3);
}

List intList = new ArrayList<>();
List numList = new ArrayList<>();
List objList = new ArrayList<>();

addIntegers(intList);  // Works
addIntegers(numList);  // Works (Number is super of Integer)
addIntegers(objList);  // Works (Object is super of Integer)
```

### 6. PECS Principle (Producer Extends, Consumer Super)

This is the golden rule for deciding between `extends` and `super`:

- **Producer Extends**: If you only READ from the collection (it produces items), use `? extends T`
- **Consumer Super**: If you only WRITE to the collection (it consumes items), use `? super T`

```
// Producer: reading from source (extends)
static  void copy(List<? extends T> source, List<? super T> dest) {
    for (T item : source) {  // Reading from source (producer)
        dest.add(item);      // Writing to dest (consumer)
    }
}

List ints = List.of(1, 2, 3);
List nums = new ArrayList<>();
copy(ints, nums);  // Copies Integer list into Number list
```

Think of it this way: `extends` gives you a read-only view (you know you are getting at least T). `super` gives you a write-only view (you know you can put at least T).

### 7. Type Erasure

Generics are a compile-time feature. At runtime, all generic type information is erased (replaced by `Object` or the bound). This is called **type erasure**.

```
// At compile time
ArrayList strings = new ArrayList<>();
ArrayList ints = new ArrayList<>();

// At runtime (after erasure)
ArrayList strings = new ArrayList();  // Both become raw ArrayList
ArrayList ints = new ArrayList();

// They have the same class at runtime!
System.out.println(strings.getClass() == ints.getClass());  // true
```

Consequences of type erasure:

- Cannot use `new T()` (type is erased at runtime)
- Cannot use `instanceof` with generic types (`obj instanceof List` is illegal)
- Cannot create generic arrays (`new T[10]` is illegal)
- Cannot have static fields of generic type

### 8. Generic Interfaces

Interfaces can be generic:

```
interface Repository {
    void save(T entity);
    T findById(int id);
    List findAll();
}

class StudentRepository implements Repository {
    private List students = new ArrayList<>();
    
    @Override
    public void save(Student s) { students.add(s); }
    
    @Override
    public Student findById(int id) {
        return students.stream()
            .filter(s -> s.getId() == id)
            .findFirst()
            .orElse(null);
    }
    
    @Override
    public List findAll() { return students; }
}
```

### 9. Restrictions on Generics

- No primitive types: `Box` is illegal; use `Box`
- No `new T()`: Cannot instantiate type parameter
- No `new T[]`: Cannot create generic arrays
- No static fields of type T: `static T value;` is illegal
- No `instanceof` with parameterized types: `obj instanceof List` is illegal (use `obj instanceof List<?>`)
- Cannot extend `Throwable`: `class MyException extends Exception` is illegal

## Code Examples

### Generic Class: Box<T>

```java
class Box<T> {
    private T item;

    Box(T item) {
        this.item = item;
    }

    T getItem() {
        return item;
    }

    void setItem(T item) {
        this.item = item;
    }

    @Override
    public String toString() {
        return "Box[" + item + "]";
    }
}

public class Main {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>("Hello");
        Box<Integer> intBox = new Box<>(42);
        Box<Double> doubleBox = new Box<>(3.14);

        System.out.println(stringBox);
        System.out.println(intBox);
        System.out.println(doubleBox);

        // Type safety: compiler catches errors
        String s = stringBox.getItem();  // No cast needed
        int n = intBox.getItem();        // Auto-unboxing
        System.out.println("String: " + s);
        System.out.println("Int: " + n);

        // stringBox.setItem(123);  // Compilation error: type mismatch
    }
}
```

`Box` can hold any type. When instantiated as `Box`, only Strings are accepted. The compiler prevents type mismatches at compile time, and no casting is needed when retrieving values.

**Output:**

```
Box[Hello]
Box[42]
Box[3.14]
String: Hello
Int: 42
```

### Generic Method: Working with Any Array Type

```java
public class Main {
    // Generic method: <T> before return type
    static <T> T getMiddle(T[] array) {
        return array[array.length / 2];
    }

    static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    static <T> String arrayToString(T[] array) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < array.length; i++) {
            if (i > 0) sb.append(", ");
            sb.append(array[i]);
        }
        return sb.append("]").toString();
    }

    public static void main(String[] args) {
        Integer[] ints = {10, 20, 30, 40, 50};
        String[] strs = {"Arjun", "Kavya", "Ravi"};

        System.out.println("Middle int: " + getMiddle(ints));
        System.out.println("Middle str: " + getMiddle(strs));

        System.out.println("Before swap: " + arrayToString(ints));
        swap(ints, 0, 4);
        System.out.println("After swap: " + arrayToString(ints));

        System.out.println("Before swap: " + arrayToString(strs));
        swap(strs, 0, 2);
        System.out.println("After swap: " + arrayToString(strs));
    }
}
```

Generic methods declare type parameters before the return type. The compiler infers `T` from the arguments. `getMiddle` works with Integer[] and String[] without separate implementations. `swap` swaps elements in any array type.

**Output:**

```
Middle int: 30
Middle str: Kavya
Before swap: [10, 20, 30, 40, 50]
After swap: [50, 20, 30, 40, 10]
Before swap: [Arjun, Kavya, Ravi]
After swap: [Ravi, Kavya, Arjun]
```

### Bounded Type Parameters

```java
class MathBox<T extends Number> {
    private T value;

    MathBox(T value) {
        this.value = value;
    }

    double sqrt() {
        return Math.sqrt(value.doubleValue());
    }

    boolean isPositive() {
        return value.doubleValue() > 0;
    }

    T getValue() {
        return value;
    }
}

public class Main {
    // Bounded generic method
    static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) >= 0 ? a : b;
    }

    public static void main(String[] args) {
        MathBox<Integer> intBox = new MathBox<>(16);
        MathBox<Double> dblBox = new MathBox<>(2.25);

        System.out.println("sqrt(16) = " + intBox.sqrt());
        System.out.println("sqrt(2.25) = " + dblBox.sqrt());
        System.out.println("16 positive? " + intBox.isPositive());

        // MathBox<String> strBox = new MathBox<>("Hi");  // Error: String not Number

        System.out.println("max(10, 20) = " + max(10, 20));
        System.out.println("max(A, Z) = " + max("A", "Z"));
        System.out.println("max(3.14, 2.71) = " + max(3.14, 2.71));
    }
}
```

`T extends Number` restricts T to Number and its subclasses. This allows calling Number methods like `doubleValue()`. The `max` method uses `T extends Comparable` to ensure elements can be compared.

**Output:**

```
sqrt(16) = 4.0
sqrt(2.25) = 1.5
16 positive? true
max(10, 20) = 20
max(A, Z) = Z
max(3.14, 2.71) = 3.14
```

### Wildcards: Unbounded, Upper, and Lower

```java
import java.util.List;
import java.util.ArrayList;

public class Main {
    // Unbounded: read-only, any type
    static void printAll(List<?> list) {
        for (Object item : list) {
            System.out.print(item + " ");
        }
        System.out.println();
    }

    // Upper bounded: read Numbers (producer extends)
    static double sum(List<? extends Number> list) {
        double total = 0;
        for (Number n : list) {
            total += n.doubleValue();
        }
        return total;
    }

    // Lower bounded: write Integers (consumer super)
    static void addNumbers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
    }

    public static void main(String[] args) {
        // Unbounded
        printAll(List.of("A", "B", "C"));
        printAll(List.of(1, 2, 3));

        // Upper bounded
        System.out.println("Sum of ints: " + sum(List.of(1, 2, 3)));
        System.out.println("Sum of doubles: " + sum(List.of(1.5, 2.5)));

        // Lower bounded
        List<Number> numList = new ArrayList<>();
        addNumbers(numList);
        System.out.println("Numbers added: " + numList);

        List<Object> objList = new ArrayList<>();
        addNumbers(objList);
        System.out.println("Objects added: " + objList);
    }
}
```

`List<?>` accepts any list (read-only). `List<? extends Number>` reads from any Number subtype (producer). `List<? super Integer>` writes Integers to any supertype list (consumer). This demonstrates the PECS principle.

**Output:**

```
A B C 
1 2 3 
Sum of ints: 6.0
Sum of doubles: 4.0
Numbers added: [1, 2, 3]
Objects added: [1, 2, 3]
```

### PECS: Producer Extends, Consumer Super

```java
import java.util.List;
import java.util.ArrayList;

public class Main {
    // Copy from source (producer: extends) to dest (consumer: super)
    static <T> void copy(List<? extends T> source, List<? super T> dest) {
        for (T item : source) {
            dest.add(item);
        }
    }

    // Find max in a producer list
    static <T extends Comparable<T>> T findMax(List<? extends T> list) {
        T max = list.get(0);
        for (T item : list) {
            if (item.compareTo(max) > 0) {
                max = item;
            }
        }
        return max;
    }

    public static void main(String[] args) {
        List<Integer> ints = List.of(3, 1, 4, 1, 5);
        List<Number> nums = new ArrayList<>();

        copy(ints, nums);  // Integer -> Number
        System.out.println("Copied: " + nums);

        System.out.println("Max int: " + findMax(ints));
        System.out.println("Max string: " + findMax(List.of("Ravi", "Arjun", "Kavya")));
    }
}
```

The `copy` method reads from `source` (producer, uses `extends`) and writes to `dest` (consumer, uses `super`). `findMax` reads from a producer list. PECS is the key to writing flexible, type-safe generic methods.

**Output:**

```
Copied: [3, 1, 4, 1, 5]
Max int: 5
Max string: Ravi
```

### Generic Interface: Repository Pattern

```java
import java.util.ArrayList;
import java.util.List;

interface Repository<T> {
    void save(T entity);
    T findById(int id);
    List<T> findAll();
    int count();
}

class Student {
    int id;
    String name;
    Student(int id, String name) { this.id = id; this.name = name; }
    @Override
    public String toString() { return "Student{" + id + ", " + name + "}"; }
}

class StudentRepository implements Repository<Student> {
    private List<Student> store = new ArrayList<>();

    @Override
    public void save(Student s) {
        store.add(s);
        System.out.println("Saved: " + s);
    }

    @Override
    public Student findById(int id) {
        for (Student s : store) {
            if (s.id == id) return s;
        }
        return null;
    }

    @Override
    public List<Student> findAll() { return store; }

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

public class Main {
    public static void main(String[] args) {
        Repository<Student> repo = new StudentRepository();
        repo.save(new Student(1, "Arjun"));
        repo.save(new Student(2, "Kavya"));
        repo.save(new Student(3, "Ravi"));

        System.out.println("Find 2: " + repo.findById(2));
        System.out.println("Count: " + repo.count());
        System.out.println("All: " + repo.findAll());
    }
}
```

The `Repository` interface defines a generic CRUD contract. `StudentRepository` implements it with `Student` as the type parameter. The same interface could be implemented for `Course`, `Teacher`, or any entity.

**Output:**

```
Saved: Student{1, Arjun}
Saved: Student{2, Kavya}
Saved: Student{3, Ravi}
Find 2: Student{2, Kavya}
Count: 3
All: [Student{1, Arjun}, Student{2, Kavya}, Student{3, Ravi}]
```

### Type Erasure Demonstration

```java
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> strings = new ArrayList<>();
        ArrayList<Integer> integers = new ArrayList<>();

        // At runtime, both are just ArrayList (type erasure)
        System.out.println(strings.getClass().getName());
        System.out.println(integers.getClass().getName());
        System.out.println(strings.getClass() == integers.getClass());

        // Cannot use instanceof with parameterized types
        List<?> list = new ArrayList<String>();
        System.out.println(list instanceof List);     // OK
        System.out.println(list instanceof List<?>);   // OK
        // System.out.println(list instanceof List<String>);  // Compilation error!

        // Generics provide safety at compile time, not runtime
        List rawList = strings;   // Raw type (no generics)
        rawList.add(42);          // No compile error with raw type!
        // String s = strings.get(0);  // ClassCastException at runtime!
        System.out.println("Raw type bypasses generic safety");
    }
}
```

At runtime, `ArrayList` and `ArrayList` are both just `ArrayList` (type parameters are erased). This is why you cannot use `instanceof` with parameterized types. Raw types bypass generic safety but risk runtime errors.

**Output:**

```
java.util.ArrayList
java.util.ArrayList
true
true
true
Raw type bypasses generic safety
```

## Common Mistakes

### Using Primitive Types as Type Parameters

**Wrong:**

```
ArrayList<int> numbers = new ArrayList<>();  // Compilation error!
Box<double> box = new Box<>(3.14);            // Compilation error!
```

Compilation error: type argument cannot be a primitive type.

**Correct:**

```
ArrayList<Integer> numbers = new ArrayList<>();  // Use wrapper class
Box<Double> box = new Box<>(3.14);                // Autoboxing handles conversion
```

Generics work only with reference types (objects), not primitive types. Use wrapper classes: `Integer` for `int`, `Double` for `double`, `Boolean` for `boolean`, etc. Java autoboxes and unboxes automatically.

### Trying to Instantiate a Type Parameter

**Wrong:**

```
class Factory<T> {
    T create() {
        return new T();  // Compilation error!
    }
}
```

Compilation error: cannot instantiate the type T.

**Correct:**

```
import java.util.function.Supplier;

class Factory<T> {
    private Supplier<T> constructor;

    Factory(Supplier<T> constructor) {
        this.constructor = constructor;
    }

    T create() {
        return constructor.get();
    }
}

// Usage: pass constructor reference
Factory<ArrayList> factory = new Factory<>(ArrayList::new);
ArrayList list = factory.create();
```

Due to type erasure, the JVM does not know what `T` is at runtime, so `new T()` is impossible. The workaround is to pass a `Supplier` (a factory function) that creates the object.

### Adding to a List<? extends T> (Producer Is Read-Only)

**Wrong:**

```
List<? extends Number> numbers = new ArrayList<Integer>();
numbers.add(42);  // Compilation error!
```

Compilation error: cannot add Integer to List<? extends Number>.

**Correct:**

```
// Use ? extends for READING (producer)
List<? extends Number> producer = List.of(1, 2, 3);
Number n = producer.get(0);  // Reading is safe

// Use ? super for WRITING (consumer)
List<? super Integer> consumer = new ArrayList<Number>();
consumer.add(42);  // Writing is safe
```

`List<? extends Number>` could be a `List`, `List`, or `List`. The compiler cannot guarantee that adding an Integer is safe (what if it is a List<Double>?). PECS: Producer Extends (read-only), Consumer Super (write-only).

### Comparing Generic Types with == (Type Erasure Confusion)

**Wrong:**

```
ArrayList<String> a = new ArrayList<>();
ArrayList<Integer> b = new ArrayList<>();

// This is TRUE due to type erasure!
if (a.getClass() == b.getClass()) {
    System.out.println("Same class at runtime!");
}

// This does NOT compile
// if (a instanceof ArrayList<String>) { }
```

Logical error: thinking generic types are different at runtime. They are not.

**Correct:**

```
// Use instanceof with unbounded wildcard
Object obj = new ArrayList<String>();
if (obj instanceof ArrayList<?>) {
    System.out.println("It is an ArrayList");
}
// Cannot determine if it is ArrayList<String> vs ArrayList<Integer> at runtime
```

Due to type erasure, `ArrayList` and `ArrayList` are the same class at runtime. You cannot use `instanceof` with parameterized types. Use `instanceof ArrayList<?>` (unbounded wildcard) instead.

### Creating Generic Arrays

**Wrong:**

```
class Container<T> {
    T[] items = new T[10];  // Compilation error!
}
```

Compilation error: generic array creation.

**Correct:**

```
class Container<T> {
    private Object[] items;  // Use Object array internally
    private int size = 0;

    @SuppressWarnings("unchecked")
    Container(int capacity) {
        items = new Object[capacity];
    }

    void add(T item) {
        items[size++] = item;
    }

    @SuppressWarnings("unchecked")
    T get(int index) {
        return (T) items[index];
    }
}
```

Generic arrays cannot be created because arrays carry type information at runtime (covariant), while generics are erased. The workaround is to use `Object[]` internally and cast when returning. The `@SuppressWarnings("unchecked")` annotation silences the compiler warning.

## Summary

- Generics allow writing classes, interfaces, and methods that work with any type while maintaining compile-time type safety. They were introduced in Java 5.
- Generic classes use type parameters: class Box { T value; }. Common conventions: T (Type), E (Element), K (Key), V (Value), N (Number).
- Generic methods declare their own type parameters: static  T max(T a, T b). The type is inferred from arguments.
- Bounded type parameters restrict types:  limits T to Number and its subclasses. Multiple bounds use &: >.
- Wildcards (?) represent unknown types. Unbounded: List<?>. Upper bounded: List<? extends Number>. Lower bounded: List<? super Integer>.
- PECS (Producer Extends, Consumer Super): Use extends when reading from a collection (producer), super when writing to it (consumer). This is the golden rule for wildcard usage.
- Type erasure removes generic type information at runtime. ArrayList and ArrayList are both ArrayList at runtime. This limits generics: no new T(), no instanceof with parameterized types, no generic arrays.
- Generics prevent ClassCastException by catching type errors at compile time. Without generics, every retrieval from a collection requires an unsafe cast.
- Generic interfaces (Repository) define type-safe contracts. Implementing classes specify the concrete type.
- Restrictions: No primitive types (use wrappers), no new T(), no T[] creation, no static generic fields, no generic exceptions.
- All Java collections (ArrayList, HashMap<K,V>, HashSet) use generics. Understanding generics is essential for using collections effectively.

## Related Topics

- undefined
- undefined

---

*Source: https://learn.modernagecoders.com/resources/java/generics-in-java/*
*Practice questions: https://learn.modernagecoders.com/resources/java/generics-in-java/practice/*
