Java: Generics

Nagesh Chauhan 26 Jun 2026 5 min read
1
Generics provide compile-time type safety by allowing classes, interfaces, and methods to operate on different data types without sacrificing type checking.

Introduced in Java 5, generics eliminate most explicit type casting, improve code reuse, and detect type-related errors during compilation rather than at runtime.

Why Generics?

Before generics, collections stored elements as Object.
List list = new ArrayList();
list.add("Java");
list.add(100);
String language = (String) list.get(0);
This approach has two major problems.

1. It requires explicit type casting.
2. Type mismatches are detected only at runtime.

Generics solve both issues.
List<String> languages = new ArrayList<>();

languages.add("Java");
String language = languages.get(0);
The compiler prevents inserting incompatible types.

Generic Classes

A class becomes generic by declaring one or more type parameters.
class Box<T> {

    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}
Usage:
Box<String> box = new Box<>();
box.set("Java");
String value = box.get();
The same class can work with any reference type.

Generic Interfaces

Interfaces may also declare type parameters.
interface Repository<T> {
    void save(T entity);
    T findById(long id);
}
Implementations specify the actual type.
class EmployeeRepository
        implements Repository<Employee> {

    @Override
    public void save(Employee employee) {

    }

    @Override
    public Employee findById(long id) {
        return null;
    }
}

Generic Methods

Individual methods can declare their own type parameters.
public class Utility {
    public static <T> void print(T value) {
        System.out.println(value);
    }
}
Usage:
Utility.print("Java");
Utility.print(100);
Utility.print(true);
The type parameter belongs only to the method.

Multiple Type Parameters

A generic declaration may define multiple type parameters.
class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
}
Usage:
Pair<Integer, String> pair =
        new Pair<>(1, "John");

Type Inference

The compiler can infer generic types.
List<String> names = new ArrayList<>();
The diamond operator <> eliminates redundant type declarations.

Bounded Type Parameters

A type parameter may restrict acceptable types.
class Calculator<T extends Number> {

    public double square(T value) {
        return value.doubleValue()
                * value.doubleValue();
    }
}
Only subclasses of Number are permitted.
Calculator<Integer> calculator =
        new Calculator<>();
The following is invalid.
Calculator<String> calculator =
        new Calculator<>();

Multiple Bounds

A generic type may define multiple bounds.
class Processor<
        T extends Number & Comparable<T>> {

}
The class bound must appear first, followed by interface bounds.

Wildcards

Wildcards provide flexibility when working with generic types. Java supports three wildcard forms.

- Unbounded Wildcards
- Upper-Bounded Wildcards
- Lower-Bounded Wildcards

Unbounded Wildcards

The ? wildcard represents an unknown type.
public void print(List<?> list) {
    for (Object value : list) {
        System.out.println(value);
    }
}
Any List type can be passed.
print(List.of(1, 2, 3));
print(List.of("Java"));
Elements cannot be added except null.

Upper-Bounded Wildcards

Upper bounds restrict types to a superclass.
public double sum(List<? extends Number> numbers) {
    double total = 0;
    for (Number number : numbers) {
        total += number.doubleValue();
    }
    return total;
}
Acceptable types include:
List<Integer>
List<Double>
List<Float>
Upper-bounded collections are generally read-only.

Lower-Bounded Wildcards

Lower bounds specify a superclass.
public void addNumbers(
        List<? super Integer> numbers) {
    numbers.add(10);
    numbers.add(20);
}
Acceptable types include:
List<Integer>

List<Number>
List<Object>
Reading returns Object because the exact type is unknown.

PECS Principle

The most important wildcard guideline is PECS.

- Producer Extends
- Consumer Super
- If a collection produces data, use ? extends T.
- If a collection consumes data, use ? super T.
void read(List<? extends Number> list)
void write(List<? super Integer> list)
PECS is one of the most important generic design principles.

Generic Arrays

Java does not allow direct creation of generic arrays. The following is illegal.
T[] array = new T[10];
Similarly:
new List<String>[10];
Both produce compilation errors. This restriction exists because arrays are reified while generics use type erasure.

Type Erasure

Java implements generics using Type Erasure. Generic type information exists only during compilation. The compiler replaces type parameters with their bounds.
class Box<T> {
    T value;
}
After compilation it becomes conceptually similar to:
class Box {
    Object value;
}
The compiler inserts the required casts automatically. Because of type erasure, generic type information is unavailable at runtime.

Restrictions of Generics

Generic type parameters cannot use primitive types.
List<int> numbers;
Use wrapper classes instead.
List<Integer> numbers;
Generic exceptions are not allowed.
class MyException<T> extends Exception {
}
The above declaration is illegal. Static fields cannot use a class type parameter.
class Box<T> {
    static T value;
}
This also produces a compilation error.

Raw Types

A generic class without specifying type arguments becomes a raw type.
List list = new ArrayList();
Raw types disable compile-time type checking and generate warnings. They should be avoided except when maintaining legacy code.

Generic Constructors

Constructors may declare their own type parameters.
class Box<T> {
    <U> Box(U value) {
        System.out.println(value);
    }
}
The constructor type parameter is independent of the class type parameter.

Generic Records

Records also support generics.
public record Pair<K, V>(K key, V value) {
}
Generic records behave exactly like generic classes.

Generic Functional Interfaces

Functional interfaces commonly use generics.
Predicate<String>
Function<Integer, String>
Supplier<Employee>
Consumer<String>
Generics enable these interfaces to remain reusable across different types.

Collections and Generics

The Collections Framework makes extensive use of generics.
List<String>
Set<Employee>
Map<Integer, Employee>
Queue<Order>
Generics eliminate most explicit casting while providing compile-time validation.

Generic Utility Methods

The Java Collections Framework provides many generic methods.
Collections.sort(list);
Collections.max(list);
Collections.min(list);
Collections.binarySearch(list, value);
These methods rely heavily on bounded type parameters.

Best Practices

Use generics instead of raw types whenever possible.

Prefer generic interfaces over concrete implementations, use meaningful type parameter names, avoid unnecessary wildcards, follow the PECS principle, and leverage type inference with the diamond operator.

Understanding type erasure and wildcard bounds is essential for designing flexible, reusable APIs.

Final Notes

Generics bring compile-time type safety, eliminate unnecessary casting, and enable reusable classes, interfaces, and methods.

Mastering bounded type parameters, wildcard usage, the PECS principle, type inference, raw types, and type erasure is fundamental for understanding the Java Collections Framework and designing robust, type-safe APIs.
Nagesh Chauhan

Nagesh Chauhan

Principal Engineer | Java ยท Spring Boot ยท Python ยท Microservices ยท AI/ML

Principal Engineer with 14+ years of experience in designing scalable systems using Java, Spring Boot, and Python. Specialized in microservices architecture, system design, and machine learning.

Share this Article

๐Ÿ’ฌ Comments

Join the Discussion