Skip to content
Chapter 12 · Core Java

Generics, Type Erasure, and Wildcards ​

The Problem That Generics Were Born to Solve ​

Before Java 5 introduced generics, every container class that wanted to be reusable had to store its data as Object. That sounds harmless at first, but it produces two painful problems that you run into the moment you try to do any real work.

Imagine you want a simple wrapper that can hold a value and let you get it back. Without generics, you write it like this:

java
// The pre generics approach
public class Print {
    Object value;          // Object is the parent of every class in Java

    Object getPrintValue() {
        return value;
    }

    void setPrintValue(Object v) {
        this.value = v;
    }
}

This works because Object is the parent of everything. Every class you will ever write in Java is automatically a child of Object, whether you say so or not. So an Object reference can hold an Integer, a String, a custom Bus class, anything at all.

Here is how you use it:

java
Print obj = new Print();

obj.setPrintValue(1);           // passing an Integer — accepted
obj.setPrintValue("hello");     // passing a String — also accepted, no complaint

// Now reading the value back
Object raw = obj.getPrintValue();

// Problem: you must figure out what type it is before you can use it
if (raw instanceof Integer) {
    int num = (int) raw;        // must cast, verbose and fragile
} else if (raw instanceof String) {
    String s = (String) raw;    // must cast again
}

See the two problems? First, nothing stopped you from stuffing both an Integer and a String into the same Print object on consecutive lines. The class has no way to enforce that it stores only one kind of thing. Second, every time you read a value back you must do an instanceof check followed by a cast. If you cast to the wrong type, you get a ClassCastException at runtime, not a friendly compile error. You only find the bug when the code is already running.

Generics solve both problems completely. The idea is that instead of hardcoding Object as the type, you use a placeholder called a type parameter and fill it in when you create the object.


Your First Generic Class ​

Here is the same Print class rewritten as a generic class:

java
public class Print<T> {
    T value;               // T is the placeholder for the real type

    T getPrintValue() {
        return value;      // returns whatever type T turns out to be
    }

    void setPrintValue(T v) {
        this.value = v;    // accepts only the type T stands for
    }
}

The <T> after the class name is the type parameter declaration. You are saying: this class works with some type, and I will call that type T throughout the class. When someone creates an object, they replace T with a real type.

java
// Creating a Print that holds only Integers
Print<Integer> obj = new Print<>();
obj.setPrintValue(1);        // fine — Integer expected and received
// obj.setPrintValue("hi"); // COMPILE ERROR — String is not Integer

// No cast needed when reading back
int val = obj.getPrintValue();   // already an Integer, assign directly

Now the compiler knows exactly what type is stored. It rejects the wrong type at compile time, and it knows the return type when you call getPrintValue, so no cast is needed. You traded the runtime ClassCastException for a compile error that you see immediately in your IDE.

On the right side of an assignment you can leave the angle brackets empty (new Print<>(), called the diamond) and the compiler infers the type from the left side. Both forms are correct:

java
Print<String> p1 = new Print<String>();   // explicit — fine
Print<String> p2 = new Print<>();          // diamond inference — preferred

One critical rule: T can be replaced by any non-primitive type (a class, abstract class, or interface), but never by a primitive. Print<int> is illegal. Use the wrapper class instead: Print<Integer>, Print<Double>, Print<Boolean>. You can use any capital letter as the name: T, A, B, and so on.


Inheritance with Generic Classes ​

Because a generic class is still a class, you can inherit from it. There are two flavors and they behave differently.

Pinning the Type at Inheritance Time ​

If your subclass is not itself generic, you must fix the type parameter when you write the extends clause:

java
// ColorPrint is NOT generic — it only works with String
class ColorPrint extends Print<String> {
    // T is permanently replaced with String in this class
}

ColorPrint cp = new ColorPrint();
cp.setPrintValue("red");    // fine
// cp.setPrintValue(42);   // compile error — this is a String Print

You pick the concrete type at the inheritance declaration and it is fixed forever for that subclass.

Passing the Type Parameter Through ​

If your subclass is also generic, you pass T through and the caller decides the type when they create an object:

java
// ColorPrint<T> IS generic — it passes T through to Print<T>
class ColorPrint<T> extends Print<T> {
    // T is still a placeholder, decided at object creation
}

ColorPrint<Integer> cp = new ColorPrint<>();
cp.setPrintValue(42);        // fine — T is Integer here

This is the more flexible form. The type decision travels all the way to the point where the object is created.


Multiple Type Parameters ​

Nothing limits you to one type parameter. You can declare as many as the class needs. A classic example is a key value pair:

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

    void put(K key, V value) {
        this.key = key;
        this.value = value;
    }
}

When you create a Pair, you supply both types:

java
// K is String, V is Integer
Pair<String, Integer> age = new Pair<>();
age.put("age", 25);         // String key, Integer value — enforced by compiler

The convention K and V is widely recognized. When you see Map<K, V> in the Java standard library, this is exactly the same idea.


Generic Methods ​

You do not have to make the whole class generic. A single method can declare its own type parameter, completely independent of any class level type parameter. The syntax puts the type parameter declaration before the return type:

java
public class GenericMethodDemo {
    // <T> before the return type makes this a generic method
    public <T> void setValue(T v) {
        System.out.println("Received: " + v);
    }
}

When you call this method, the compiler infers T from the argument you pass:

java
GenericMethodDemo demo = new GenericMethodDemo();
demo.setValue(42);        // T inferred as Integer
demo.setValue("hello");   // T inferred as String
demo.setValue(new Bus()); // T inferred as Bus

Two rules to remember about generic methods. First, the type parameter declaration goes before the return type, not anywhere else. Second, the type parameter T in a generic method is completely local to that method. If the class also has a type parameter called T, they are separate and independent. Renaming one of them to something else makes the code clearer.


Raw Types: The Danger Zone ​

A raw type is what you get when you use a generic class without providing the type parameter:

java
// Raw type — no type argument given
Print raw = new Print();     // compiler warns: unchecked usage
raw.setPrintValue(42);
raw.setPrintValue("hello");  // accepted — type safety completely gone

Internally the compiler substitutes Object for every type parameter, which is exactly the pre generics world you wanted to escape. Raw types exist only for backward compatibility with old code written before Java 5. Never use them in new code. When you see a raw type warning in your IDE, take it seriously.


Bounded Type Parameters ​

By default, T can be replaced with absolutely anything. Sometimes you want to restrict that. Bounded type parameters let you say: T can only be this class, or a subclass of it.

Upper Bound with extends ​

java
class NumericPrinter<T extends Number> {
    T value;

    // Because T must be Number or a subclass, we can call Number's methods
    double doubleValue() {
        return value.doubleValue();   // Number has this method
    }
}

Now the compiler enforces the constraint:

java
NumericPrinter<Integer> ip = new NumericPrinter<>();   // Integer extends Number — OK
NumericPrinter<Double>  dp = new NumericPrinter<>();   // Double extends Number — OK
// NumericPrinter<String> sp = ...;  // COMPILE ERROR: String does not extend Number

The benefit is not just restriction. It is also capability. Without the bound, you cannot call any Number method on T because the compiler has no idea what T is. With T extends Number, the compiler knows T has at least everything Number has, so those calls are valid.

One important thing: the keyword is always extends even when you are bounding by an interface. In regular class declarations you write implements for interfaces, but inside angle brackets you always write extends:

java
// Bounding by an interface — still uses extends, not implements
class Sorter<T extends Comparable<T>> {
    // T can call compareTo() because Comparable guarantees it
}

Multiple Bounds ​

You can require T to satisfy several constraints at once. The syntax uses & to combine them:

java
// T must extend ParentClass AND implement Interface1 AND implement Interface2
class MultiConstrained<T extends ParentClass & Interface1 & Interface2> {
    // ...
}

The rules here mirror Java's single inheritance: you can have at most one class bound (it must come first), followed by as many interface bounds as you like. This matches the fact that a Java class can extend only one class but implement any number of interfaces.

java
// A class that satisfies this bound
class A extends ParentClass implements Interface1, Interface2 {
    // implements everything required
}

// An object of class A can be used as type T
MultiConstrained<A> mc = new MultiConstrained<>();   // OK

// But a class that only implements Interface1, not Interface2, fails
class B extends ParentClass implements Interface1 { }
// MultiConstrained<B> mc2 = ...;  // COMPILE ERROR — B doesn't implement Interface2

Why Generics Are Invariant: The List Problem ​

Here is something that surprises almost every developer the first time they see it. Suppose you have this hierarchy:

java
// Vehicle is the parent; Bus and Car are children
class Vehicle { }
class Bus extends Vehicle { }
class Car extends Vehicle { }

With regular object references, covariance works exactly as you expect:

java
Vehicle v = new Bus();   // parent reference holds child object — perfectly valid

But with generic collections, this does NOT work:

java
List<Vehicle> vehicleList = new ArrayList<>();
List<Bus> busList = new ArrayList<>();

// vehicleList = busList;  // INVALID — does not compile
// busList = vehicleList;  // INVALID — also does not compile

Neither direction works. List<Bus> is not a subtype of List<Vehicle>, even though Bus extends Vehicle. This is called invariance.

Why does Java enforce this? The reason is safety. Suppose the assignment vehicleList = busList were allowed. Then you could do this:

java
vehicleList.add(new Car());    // Car is a Vehicle, so this looks legal

But vehicleList is actually pointing at a List<Bus>. You just added a Car to a list that is supposed to contain only buses. When someone later reads from busList and expects a Bus, they get a Car and a ClassCastException. The compiler prevents the assignment entirely to make this impossible.


Wildcards: Getting Flexibility Back ​

Invariance is safe, but it can be frustrating when you want to write a method that works with a list of vehicles and also with a list of buses. That is what wildcards are for. The wildcard is written as ? and represents an unknown type.

Upper Bounded Wildcard: Reading a Family of Lists ​

java
// Accepts List<Vehicle>, List<Bus>, List<Car> — anything that is a Vehicle
void processVehicles(List<? extends Vehicle> vehicles) {
    for (Vehicle v : vehicles) {   // reading is safe — every element is at least a Vehicle
        v.drive();
    }

    // vehicles.add(new Bus());    // COMPILE ERROR — cannot add to a wildcard list
}

The ? extends Vehicle means: a list of some unknown type that extends Vehicle. You can read elements because you know they are at least Vehicle. You cannot add elements because you do not know the exact type. The list might be a List<Bus> and adding a Car to it would be wrong. The compiler refuses to let you add anything.

Lower Bounded Wildcard: Writing into a Family of Lists ​

java
// Accepts List<Vehicle>, List<Object> — anything that is a supertype of Vehicle
void addVehicles(List<? super Vehicle> destination) {
    destination.add(new Bus());    // safe — a Bus is a Vehicle
    destination.add(new Car());    // also safe

    // Object obj = destination.get(0);  // reading gives only Object — not very useful
}

The ? super Vehicle means: a list of some unknown type that is Vehicle or above. You can add Vehicle and its subtypes because any list that holds Vehicles or more general things can safely accept a Vehicle. Reading is almost useless though because the best the compiler can promise you is Object.

Unbounded Wildcard: Any List at All ​

java
// Accepts any list regardless of element type
void printAll(List<?> list) {
    for (Object o : list) {
        System.out.println(o);   // can only call Object methods
    }
}

Use unbounded wildcards when your method only needs Object methods like toString or equals, and you truly do not care what the element type is.


Wildcards versus Generic Type Parameters ​

A common source of confusion is when to use a wildcard and when to use a type parameter. Here is a clear comparison:

java
// Wildcard version: source and destination can be DIFFERENT subtypes of Number
void computeWild(List<? extends Number> source, List<? extends Number> destination) {
    // source could be List<Integer>, destination could be List<Float> — both accepted
}

// Generic type version: source and destination must be the SAME type
<T extends Number> void computeTyped(List<T> source, List<T> destination) {
    // if source is List<Integer>, destination must also be List<Integer>
}

In practice:

Use a type parameter when you need to enforce that multiple arguments share the exact same type. Use a type parameter when you need more than one type variable (like K and V). Use a wildcard when you need a lower bound (super), because lower bound type parameters do not exist in Java. Use a wildcard when the method only cares about one position and flexibility across subtypes is more important than sameness.


Type Erasure: What the JVM Actually Sees ​

Here is the part that surprises most developers: generics exist only at compile time. By the time the compiler produces bytecode, every trace of the type parameters has been removed. The JVM has never heard of generics. This process is called type erasure.

What the compiler actually does during compilation:

For unbounded type parameters like T, the compiler replaces every occurrence with Object. For bounded type parameters like T extends Number, the compiler replaces every occurrence with the bound class, in this case Number. The compiler also inserts casts in the calling code wherever it needs to recover the specific type.

Here is what that looks like in practice:

java
// Source code you write
Print<Integer> p = new Print<>();
p.setPrintValue(42);
int val = p.getPrintValue();

// What the bytecode is equivalent to after erasure
Print p = new Print();             // raw type
p.setPrintValue(42);               // Integer autoboxed to Object inside the method
int val = (Integer) p.getPrintValue();  // compiler inserted this cast for you

The cast that would have been your problem in the pre generics world is still there in the bytecode. The difference is that the compiler generates it for you and, more importantly, guarantees it is correct because it already verified the types at compile time.

For a bounded type parameter, the erasure uses the bound instead of Object:

java
// Source code
class NumericPrinter<T extends Number> {
    T value;
    double doubleValue() { return value.doubleValue(); }
}

// After erasure, equivalent to
class NumericPrinter {
    Number value;              // T replaced with Number, the upper bound
    double doubleValue() { return value.doubleValue(); }
}

Generic methods follow the same pattern:

java
// Source: generic method
public <T> void setValue(T v) {
    System.out.println(v);
}

// After erasure, equivalent to
public void setValue(Object v) {
    System.out.println(v);
}

Check yourself ​

  1. What two problems appear when a reusable class stores its value as Object?
  2. Can T be replaced with int? What can it be replaced with?
  3. class ColorPrint extends Print<String> versus class ColorPrint<T> extends Print<T>: what is the difference?
  4. Where does the type parameter go in a generic method, and what is its scope?
  5. What is a raw type, and what does the compiler use for T in it?
  6. What does <T extends Number> allow? In a multi-bound, what must come first?
  7. Why can a List<Bus> not be assigned to a List<Vehicle>? How do wildcards help?
  8. When would you choose a generic type method over a wildcard, and when a wildcard?
  9. What does type erasure do to T and to T extends Number?
Answers
  1. Anything can be stored, so the class cannot enforce one type; and every read needs instanceof checks and casts, which can fail at runtime with ClassCastException.
  2. No. Any non-primitive type: a class, an abstract class, or an interface (so Integer, not int).
  3. The first is not generic: it fixes the type to String when extending. The second stays generic: the type is chosen when the object is created.
  4. Before the return type, e.g. public <T> void setValue(T v). It exists only inside that method.
  5. Using a generic class without a type argument, e.g. new Print(). The compiler treats T as Object, so any type is accepted.
  6. Only Number and its subclasses (Integer, Double, ...). In a multi-bound the class comes first, then any number of interfaces: <T extends ParentClass & Interface1 & Interface2>.
  7. If allowed, you could add a Car through the List<Vehicle> reference into a list meant for buses. List<? extends Vehicle> accepts a list of Vehicle or any subclass; List<? super Vehicle> accepts Vehicle or any superclass; List<?> accepts any list.
  8. A generic type method when all parameters must share the same type, or when you need several type parameters. A wildcard when parameters may differ, or when you need a lower bound (super), which type parameters do not support.
  9. At compile time generics are checked and then erased: unbounded T becomes Object, and T extends Number becomes Number.