﻿---
title: Java SE Generics
date: 2023-09-15
excerpt: Study notes on Java Generics, covering wildcards, PECS, type inference, and how to design generic classes, interfaces, and methods.
tags: [Java, Generics]
updated: 2026-07-08 21:17:48
lang: en
i18n:
  cn: /java_generic
  translation: 2
---

<script type="module" src="/js/components/tab.js"></script>

## Introduction

Generics are a highly practical tool in programming. They let us write code that works with multiple data types without duplicating it for each one. Think of them as a flexible template that can be adapted to different needs.

For example, suppose you need a function that sorts a list of numbers. Without generics, you might have to write separate functions for integers, floats, and so on. With generics, a single function handles sorting for all numeric types.

> [!INFO] Background of generics in Java
>
> When a collection container class is designed and declared, the actual type of the objects it will hold cannot be determined. So before JDK 1.5, element types could only be declared as `{java} Object`, which meant a forced cast was needed when retrieving elements. Such casts are unsafe and can throw a `{java} ClassCastException` at runtime.
>
> With generics, you can specify the element type when declaring a collection. Type checking then happens at compile time, eliminating the runtime exceptions caused by forced casts and improving both safety and readability.

### Advantages of Generics

- Type Safety & Elimination of casts: Java generics provide compile-time type-checking and reduce the need for casts.
- Reduce code duplication: generics improve reusability and extensibility, making code more concise.
- Readability: generics express the code's intent more clearly, making it easier to read and maintain.

<x-tabs>

<x-tab title="Generic" active>

```java
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // Compile-time error: the collection can only hold String

for (String str : list) { // No cast needed
    System.out.println(str);
}
```

</x-tab>

<x-tab title="WithOutGeneric">

```java
// Pre-JDK 1.5 example (no generics):
List list = new ArrayList();
list.add("Hello");
list.add(123); // No error: the collection can hold any Object

for (Object obj : list) {
    String str = (String) obj; // Manual cast, may throw ClassCastException
    System.out.println(str);
}
```

</x-tab>

</x-tabs>

### Syntax

Java generics have the following syntax:

- Generic class: `{java} class MyClass<T> { ... }`
- Generic interface: `{java} interface MyInterface<T> { ... }`
- Generic method: `{java} public <T> void myMethod(T data) { ... }`

Naming conventions for generics:

- `{java} T`: Type parameter
- `{java} E`: Element or Entry type
- `{java} K`, `{java} V`: Key and Value types

> Finally, again let's take note of the naming convention used for the type parameters. We use T for type, whenever there isn't anything more specific about the type to distinguish it. This is often the case in generic methods. If there are multiple type parameters, we might use letters that neighbor T in the alphabet, such as S. If a generic method appears inside a generic class, it's a good idea to avoid using the same names for the type parameters of the method and class, to avoid confusion. The same applies to nested generic classes.
> Oracle Java Tutorials

## Use Generic In Java

Using generics in Java improves type safety and reusability, but there are caveats and restrictions to keep in mind.

### Best Practices

#### Method Overload

Overloaded methods may conflict after type erasure. Avoid defining overloads in the same class that differ only in their generic type arguments.

```java
// Compiler error: Erasure of method method(List<String>) is the same as another method in type Main
public class Main {
    public void method(List<String> list) { ... }
    public void method(List<Integer> list) { ... }
}
```

#### Utilize Type Inference

JDK 7 introduced [Type Inference](https://docs.oracle.com/javase/tutorial/java/generics/genTypeInference.html), the compiler's ability to infer generic type arguments from context. It makes code more concise and readable by avoiding repeated type declarations.

**Type inference** in Java generics refers to the compiler's ability to deduce generic type arguments from context, making code more concise and readable while avoiding redundant type declarations.

**Benefits of type inference:**

- **Better readability:** fewer redundant type arguments make code easier to understand and maintain.
- **Less code:** no need to specify type arguments explicitly, simplifying the code you write.
- **More flexibility:** code adapts to change more easily; for example, changing a method's return type does not require updating type arguments at every call site.

**Where type inference applies:**

- constructor
- method invocation
- ...

```java
// 1. Constructor
Map<String, List<String>> myMap = new HashMap<String, List<String>>();
// You can substitute the parameterized type of the constructor with an empty set of type parameters (<>):
Map<String, List<String>> myMap = new HashMap<>();


// 2. Method invocation
// Code adapted from @pdai: https://pdai.tech/md/java/basic/java-basic-x-generic.html
public class Test {
    public static void main(String[] args) {

        /** Without specifying the type argument */
        int i = Test.add(1, 2); // Both arguments are Integer, so T is Integer
        Number f = Test.add(1, 1.2); // One Integer and one Float, so the nearest common supertype is Number
        Object o = Test.add(1, "asd"); // One Integer and one String, so the nearest common supertype is Object

        /** With an explicit type argument */
        int a = Test.<Integer>add(1, 2); // Integer specified, so only Integer or its subclasses are allowed
        int b = Test.<Integer>add(1, 2.2); // Compile error: Integer specified, Float not allowed
        Number c = Test.<Number>add(1, 2.2); // Number specified, so both Integer and Float are allowed
    }

    // A simple generic method
    public static <T> T add(T x, T y) {
        return y;
    }
}
```

#### Avoid using Raw Types

The code below uses the raw type List, which is discouraged in modern Java. Raw types exist only for backward compatibility with pre-Java 5 code. They bypass generic type checking and can lead to a `{java} ClassCastException` at runtime.

```java
// Warning: List is a raw type. References to generic type List<E> should be parameterized
List list = new ArrayList();
list.add("Hello");

// Recommended: Use generic type List<String>
List<String> list = new ArrayList<>();
```

> Restrictions on Generics
>
> - Cannot Instantiate Generic Types with Primitive Types
> - Cannot Create Instances of Type Parameters
> - Cannot Declare Static Fields Whose Types are Type Parameters
> - Cannot Use Casts or instanceof With Parameterized Types
> - Cannot Create Arrays of Parameterized Types
> - Cannot Create, Catch, or Throw Objects of Parameterized Types
> - Cannot Overload a Method Where the Formal Parameter Types of Each Overload Erase to the Same Raw Type
>
> for more information, see [Generic Restrictions —— docs.oracle](https://docs.oracle.com/javase/tutorial/java/generics/restrictions.html)

## Design Effectively Using Generics

### Wildcards

In Java generics, a wildcard is a symbol representing an unknown type. The common wildcards are `{java} ?`, the upper bounded wildcard `{java} ? extends T`, and the lower bounded wildcard `{java} ? super T`.

- Upper Bounds Wildcards `{java} ? extends T`: `{java} ?` can only be `{java} T` or a subclass of `{java} T`. After compilation, `{java} ? extends T` is erased to `{java} T`.
- Lower Bounds Wildcards `{java} ? super T`: `{java} ?` can only be `{java} T` or a superclass of `{java} T`.

<img src="https://assets.vluv.space/Java/java_generic/java_generic.webp" alt="diagram" style="width: 50%"/>

#### PECS

PECS (Producer Extends, Consumer Super) is an important guiding principle in Java generic programming, used to ensure type safety and correctness.

The PECS principle says: when a parameter produces data (Producer), use the upper bounded wildcard `{java} ? extends T`; when a parameter consumes data (Consumer), use the lower bounded wildcard `{java} ? super T`.

|           | Producer Extends | Consumer Super                  | description                                                                       |
| --------- | ---------------- | ------------------------------- | --------------------------------------------------------------------------------- |
| `copy`    | source collection src | destination collection dest | `{java} public static <T> void copy(List<? super T> dest, List<? extends T> src)` |
| `sort`    | the list passed in |                                 | `{java} public static <T extends Comparable<? super T>> void sort(List<T> list)`  |
| `compare` |                  | the comparator, which reads both arguments | `{java} public static <T> int compare(T a, T b, Comparator<? super T> c)`         |

The `{java} Collections.copy()` method is a good example of PECS in action. Among its parameters, `src` is the producer, used for reading data; `dest` is the consumer, used for writing data. So `src` uses the upper bounded wildcard `{java} ? extends T`, and `dest` uses the lower bounded wildcard `{java} ? super T`. This guarantees that the element type of `dest` is a supertype of the element type of `src`, preventing a `{java} ClassCastException`.

```java
/**
 * @param dest written to, acts as the consumer, uses the super wildcard
 * @param src read from, acts as the producer, uses the extends wildcard
*/
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
    if (srcSize > dest.size())
        throw new IndexOutOfBoundsException("Source does not fit in dest");

    if (srcSize < COPY_THRESHOLD ||
        (src instanceof RandomAccess && dest instanceof RandomAccess)) {
        for (int i=0; i<srcSize; i++)
            dest.set(i, src.get(i));
    } else {
        ListIterator<? super T> di=dest.listIterator();
        ListIterator<? extends T> si=src.listIterator();
        for (int i=0; i<srcSize; i++) {
            di.next();
            di.set(si.next());
        }
    }
}
```

### Generic Class

A generic class differs from a non-generic class in that it declares type parameters. There can be multiple type parameters, separated by commas. Generic classes are commonly used to build general-purpose data structures, such as collection classes (`{java} ArrayList<T>` e.g).

<x-tabs>

<x-tab title="single-parameter" active>

```java
class MyClass<T> {
    private T data;

    public void setData(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }
}

public class Main {
    public static void main(String[] args) {
        MyClass<String> myClass = new MyClass<>();
        myClass.setData("Hello");
        String str = myClass.getData();
        System.out.println(str); // Output: Hello
    }
}
```

</x-tab>

<x-tab title="multi-parameters">

```java
class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

public class Main {
    public static void main(String[] args) {
        Pair<String, Integer> pair = new Pair<>("key", 123);
        System.out.println(pair.getKey()); // Output: key
        System.out.println(pair.getValue()); // Output: 123
    }
}
```

</x-tab>

</x-tabs>

When writing a generic class, note that static methods cannot directly reference the class's type parameter `{java} T`. The reason is closely tied to the class loading mechanism and when type arguments are bound.

> Why can't static fields use type parameters?
>
> A class's static field is a class-level variable shared by all non-static objects of the class. Hence, static fields of type parameters are not allowed. Consider the following class:
>
> ```java
> public class MobileDevice<T> {
>     private static T os;
>     // ...
> }
> // If static fields of type parameters were allowed, then the following code would be confused:
> MobileDevice<Smartphone> phone = new MobileDevice<>();
> MobileDevice<Pager> pager = new MobileDevice<>();
> MobileDevice<TabletPC> pc = new MobileDevice<>();
> ```
>
> Because the static field os is shared by phone, pager, and pc, what is the actual type of os? It cannot be Smartphone, Pager, and TabletPC at the same time. You cannot, therefore, create static fields of type parameters.

If a static member needs to work with generics, there are a few options:

```java
public class Pair<T> {
    private T key;
    private T value;
    public Pair(T key, T last) {
        this.key = value;
        this.value = valuet;
    }
    public T getFirst() { ... }
    public T getLast() { ... }

    // a. Use a generic method
    // Declare the type parameter at the method level instead of the class level.
    public static <K> Pair<K> create(K first, K last) {
        return new Pair<K>(first, last);
    }
    // b. Use a wildcard or a concrete type
    // In a static method, use a wildcard or a concrete type instead of the class's type parameter
    public static void staticMethod(List<?> list) {...}
    // Using a concrete type
    public static void staticMethod(List<String> list) {...}
}
```

### Generic Interface

Like generic classes, generic interfaces can declare type parameters. When a class implements a generic interface, it can either specify a concrete type argument or keep the type parameter generic.

Here is an example of a generic interface:

```java
import java.util.HashMap;
import java.util.Map;

interface Repository<T> {
    void add(T item);

    T getById(String id);

    void delete(String id);
}

class Computer {
    private String id;
    private String name;
    private double price;

    public Computer(String id, String name, double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    // getter
    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }

    @Override
    public String toString() {
        return "Computer{id='" + id + "', name='" + name + "', price=" + price + '}';
    }
}

class ComputerRepository implements Repository<Computer> {
    private Map<String, Computer> computers = new HashMap<>();

    @Override
    public void add(Computer computer) {
        computers.put(computer.getId(), computer);
    }

    @Override
    public Computer getById(String id) {
        return computers.get(id);
    }

    @Override
    public void delete(String id) {
        computers.remove(id);
    }
}

public class Main {
    public static void main(String[] args) {
        Repository<Computer> computerRepository = new ComputerRepository();

        // Add computers to the repository
        computerRepository.add(new Computer("1", "RedMi", 999.99));
        computerRepository.add(new Computer("2", "Lenovo", 499.99));

        // Retrieve and display a computer
        Computer laptop = computerRepository.getById("1");
        System.out.println("Retrieved: " + laptop);

        // Delete a computer
        computerRepository.delete("1");
        System.out.println("computer with ID 1 deleted.");

        // Attempt to retrieve the deleted computer
        Computer deletedcomputer = computerRepository.getById("1");
        System.out.println("After deletion, retrieved: " + deletedcomputer);
    }
}

[OUTPUT]
Retrieved: Computer{id='1', name='RedMi', price=999.99}
computer with ID 1 deleted.
After deletion, retrieved: null
```

### Generic Method

A generic method declares its type parameters in angle brackets `{java} < >` before the method's return type.

> Why use generic methods?
> A generic class must fix its type argument at instantiation; switching to another type requires a new instance, which can be inflexible. A generic method lets you specify the type at call time, which is more flexible.

The `{java} java.util.Collections` class provides many generic methods for working with collections. For example, the `{java} sort` method uses generics to sort a list of any type.

```java
public class Collections {
    public static <T extends Comparable<? super T>> void sort(List<T> list) {
        list.sort(null);
    }

    public static <T> void sort(List<T> list, Comparator<? super T> c) {
        list.sort(c);
    }
}
```

The `{java} java.util.Arrays` class provides many generic methods for working with arrays. For example, the `{java} asList` method uses generics to convert an array into a list.

```java
public class Arrays {
    @SafeVarargs
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }
}

// usage example
public class Main {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Banana", "Apple", "Cherry");
        Collections.sort(list);
        System.out.println(list); // Output: [Apple, Banana, Cherry]
    }
}
```

The `{java} java.util.stream.Stream` interface defines many non-static generic methods, such as `{java} flatMap`, `{java} map`, and `{java} collect`.

```java
public interface Stream<T> extends BaseStream<T, Stream<T>> {
    <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    <R, A> R collect(Collector<? super T, A, R> collector);
}
```

## Reference

[Generic Methods The Java™ Tutorials](https://docs.oracle.com/javase/tutorial/extra/generics/methods.html)
[Generics - Java Tutorial - Liao Xuefeng](https://liaoxuefeng.com/books/java/generics/index.html)
[Generic Restrictions](https://docs.oracle.com/javase/tutorial/java/generics/restrictions.html)
[Type Inference](https://docs.oracle.com/javase/tutorial/java/generics/genTypeInference.html)
