Java Generics: Type Parameters, Wildcards, and Legacy Code Integration

Generic Type Definitions

The java.util package includes interfaces like List and Iterator with generic type parameters:

public interface List<E> {
    void add(E element);
    Iterator<E> iterator();
}

public interface Iterator<E> {
    E next();
    boolean hasNext();
}

The angle brackets contain formal type parameters declarations. These parameters can be used throughout the generic declaration similar to regular types.

When using parameterized types like List<Integer>, all occurrences of the formal type parameter (E) are replaced with actual type arguments (Integer).

You might visualize List<Integer> as representing a version where E has been uniformly substituted with Integer:

public interface IntegerList {
    void add(Integer element);
    Iterator<Integer> iterator();
}

While this intuition helps, it's misleading because generic declarations never expand this way. There are no multiple copies in source, binary, or memory. Unlike C++ templates, generic declarations compile once into a single class file.

Type parameters resemble formal parameters in methods or constructors. Just as methods have formal value parameters describing the kind of values they operate on, generic declarations have formal type parameters. When calling a method, actual arguments replace formal parameters. When using a generic declaration, actual type arguments replace formal type parameters.

Naming conventions suggest using concise, single-character but descriptive names for formal type parameters, avoiding lowercase characters to distinguish them from regular classes and interfaces. Many container types use E for elements.

Generics and Subtyping

Consider this code snippet:

List<String> stringList = new ArrayList<String>(); // 1
List<Object> objectList = stringList; // 2

Line 1 is valid. Line 2 raises the question: Is a List<String> a subtype of List<Object>? Intuitively, one might answer "yes".

However, consider these subsequent lines:

objectList.add(new Object()); // 3
String s = stringList.get(0); // 4: Attempting to assign an Object to a String!

Here, we alias both lists. Through the objectList alias, we can insert arbitrary objects into what was originally a String list. The result is that stringList no longer contains only strings, leading to runtime errors.

Java compilers prevent this scenario. Line 2 would cause a compilation error.

Generally, if Foo is a subtype of Bar, and G is a generic type declaration, then G<Foo> is not a subtype of G<Bar>. This contradicts our entuitive expectations about collections.

Wildcards

Consider writing a routine to print all elements in a collection. Previously, you might have written:

void displayCollection(Collection input) {
    Iterator iter = input.iterator();
    for (int index = 0; index < input.size(); index++) {
        System.out.println(iter.next());
    }
}

A naive generic attempt would be:

void displayCollection(Collection<Object> input) {
    for (Object element : input) {
        System.out.println(element);
    }
}

The issue is that the generic version is less practical than the original. While the old code accepts any collection type, the new code only accepts Collection<Object>, which isn't a supertype of all collection types!

The supertype of all collection types is Collection<?> (read as "collection of unknown type"). This is called a wildcard type:

void displayCollection(Collection<?> input) {
    for (Object element : input) {
        System.out.println(element);
    }
}

Now we can call it with any collection type. Inside displayCollection(), we can still read elements and assign them as Object. This is always safe since regardless of the collection's actual type, it contains objects. However, adding arbitrary objects is unsafe:

Collection<?> collection = new ArrayList<String>();
collection.add(new Object()); // Compilation error

Since we don't know what type the collection elements represent, we cannot add objects. The add() method accepts parameters of type E (the collection's element type). When the actual type parameter is ?, it represents some unknown type. Any argument passed to add must be a subtype of this unknown type. Since we don't know what that type is, we cannot pass anything except null.

Bounded Wildcards

Consider a drawing application handling shapes like rectangles and circles:

public abstract class Shape {
    public abstract void render(Canvas canvas);
}

public class Circle extends Shape {
    private int x, y, radius;
    public void render(Canvas canvas) {
        // implementation
    }
}

public class Rectangle extends Shape {
    private int x, y, width, height;
    public void render(Canvas canvas) {
        // implementation
    }
}

These classes can render on canvases:

public class Canvas {
    public void render(Shape shape) {
        shape.render(this);
    }
}

Assuming shapes are represented as a list, a convenient method in Canvas would draw all of them:

public void renderAll(List<Shape> shapes) {
    for (Shape shape : shapes) {
        shape.render(this);
    }
}

Type rules indicate that renderAll() only works with List<Shape>, so it cannot be called on List<Circle>. This is unfortunate since the method only reads from the list.

What we want is for the method to accept any type of shape list:

public void renderAll(List<? extends Shape> shapes) {
    // implementation
}

We've changed List<Shape> to List<? extends Shape>. Now renderAll() accepts any list of Shape subtypes, allowing calls on List<Circle>.

List<? extends Shape> is an example of bounded wildcards. ? represents an unknown type, but here we know this unknown type is actually a subtype of Shape. We say Shape is the wildcard's upper bound.

Using wildcard flexibility comes at a cost. Writing to shapes within the method body is now ilegal:

public void appendRectangle(List<? extends Shape> shapes) {
    // Compile-time error!
    shapes.add(0, new Rectangle());
}

The second argument of shapes.add() has type ? extends Shape – an unknown Shape subtype. Since we don't know its type, we don't know if it's a supertype of Rectangle, making it unsafe to pass Rectangle here.

Generic Methods

Consider writing a method that accepts an object array and a collection, placing all array objects into the collection:

static void transferFromToArray(Object[] source, Collection<?> target) {
    for (Object obj : source) { 
        target.add(obj); // compile-time error
    }
}

As learned before, using Collection<?> won't work because you cannot add objects to unknown-type collections.

The solution is generic methods. Like type declarations, method declarations can be generic – parameterized by one or more type parameters.

static <T> void transferFromToArray(T[] source, Collection<T> target) {
    for (T obj : source) {
        target.add(obj); // Correct
    }
}

We can call this method using any collection type whose element type is a supertype of the array elements.

Object[] objectArray = new Object[100];
Collection<Object> objectCollection = new ArrayList<Object>();

// T inferred to be Object
transferFromToArray(objectArray, objectCollection); 

String[] stringArray = new String[100];
Collection<String> stringCollection = new ArrayList<String>();

// T inferred to be String
transferFromToArray(stringArray, stringCollection);

// T inferred to be Object
transferFromToArray(stringArray, objectCollection);

Integer[] integerArray = new Integer[100];
Float[] floatArray = new Float[100];
Number[] numberArray = new Number[100];
Collection<Number> numberCollection = new ArrayList<Number>();

// T inferred to be Number
transferFromToArray(integerArray, numberCollection);

// T inferred to be Number
transferFromToArray(floatArray, numberCollection);

// T inferred to be Number
transferFromToArray(numberArray, numberCollection);

// T inferred to be Object
transferFromToArray(numberArray, objectCollection);

// compile-time error
transferFromToArray(numberArray, stringCollection);

Notice that we don't need to pass actual type arguments to generic methods. The compiler infers type parameters from actual arguments' types.

The question is: when should you use generic methods versus wildcard types? Let's examine some Collection library methods:

interface Collection<E> {
    public boolean containsAll(Collection<?> collection);
    public boolean addAll(Collection<? extends E> collection);
}

We could use generic methods here:

interface Collection<E> {
    public <T> boolean containsAll(Collection<T> collection);
    public <T extends E> boolean addAll(Collection<T> collection);
}

However, in containsAll and addAll, the type parameter T is used only once. The return type doesn't depend on the type parameter, nor do other method parameters. This indicates that type parameters are used for polymorphism – allowing various actual argument types at different call sites. In such cases, wildcards should be used instead.

Generic methods allow expressing dependencies between method parameters and/or return types. If no such dependencies exist, generic methods shouldn't be used.

Both generic methods and wildcards can be combined. Here's how Collections.copy() method signature looks:

class Collections {
    public static <T> void copy(List<T> destination, List<? extends T> source) {
        // implementation
    }
}

Note the dependency between two parameter types. Any object copied from source list source must be assignable to destination list destination's element type T. Therefore, source's element type can be any subtype of T.

Interoperability with Legacy Code

Most existing code was written using earlier Java versions without generics support.

When calling legacy code from generic code, the legacy code expects Collection types. The actual argument might be Collection<Part>. This works because when generic types like Collection are used without type parameters, they're called raw types.

Raw types behave similarly to wildcard types but with less strict type checking, allowing generics to interact with existing legacy code.

Calling legacy code from generic code is inherently dangerous; mixing generic and non-generic code invalidates the safety guarantees of the generic type system. However, you're still better off than not using generics at all.

Erasure and Translation

public String exploit(Integer value) {
    List<String> stringList = new LinkedList<String>();
    List rawList = stringList;
    rawList.add(value); // Compile-time unchecked warning
    return stringList.iterator().next();
}

Here we alias a string list with a raw list, insert an Integer, and try to extract a String. This is clearly wrong. If we ignore warnings and execute this code, it fails at runtime:

public String exploit(Integer value) {
    List rawList = new LinkedList();
    List anotherList = rawList;
    anotherList.add(value); 
    return (String) rawList.iterator().next(); // runtime error
}

We'll receive a ClassCastException when trying to cast the extracted element to String.

Generics are implemented through erasure, a front-end transformation. The generic version gets converted to a non-generic version. Even with unchecked warnings, Java Virtual Machine's type safety and integrity are never compromised.

Erasure eliminates all generic type information. Type parameters in angle brackets are discarded, so List<String> becomes List. Remaining type variable usages are replaced with their upper bounds (usually Object). Type casts are inserted where needed.

Fine Print Details

Shared Runtime Class

All instances of a generic class share the same runtime class regardless of their actual type parameters:

List<String> firstList = new ArrayList<String>();
List<Integer> secondList = new ArrayList<Integer>();
System.out.println(firstList.getClass() == secondList.getClass()); // prints true

Static variables and methods are shared among all instances, which is why referencing type parameters in static contexts is illegal.

Casting and InstanceOf

You typically cannot ask whether an instance is of a particular generic type invocation:

Collection collection = new ArrayList<String>();
// Illegal.
if (collection instanceof Collection<String>) { ... }

Similarly, casts like this produce unchecked warnings:

// Unchecked warning,
Collection<String> stringCollection = (Collection<String>) collection;

Type variables also receive warnings:

// Unchecked warning.
<T> T problematicCast(T param, Object input) {
    return (T) input;
}

Arrays

Array component types cannot be type variables or parameterized types unless they're (unbounded) wildcard types:

// Not really allowed.
List<String>[] stringListArray = new List<String>[10];
Object container = stringListArray;
Object[] objectArray = (Object[]) container;
List<Integer> integerList = new ArrayList<Integer>();
integerList.add(new Integer(3));
// Unsound, but passes runtime store check
objectArray[1] = integerList;

// Runtime error: ClassCastException.
String result = stringListArray[1].get(0);

Wildcard arays are permitted:

// OK, array of unbounded wildcard type.
List<?>[] wildcardListArray = new List<?>[10];
Object container = wildcardListArray;
Object[] objectArray = (Object[]) container;
List<Integer> integerList = new ArrayList<Integer>();
integerList.add(new Integer(3));
// Correct.
objectArray[1] = integerList;
// Runtime error, but cast is explicit.
String result = (String) wildcardListArray[1].get(0);

Class Literals as Runtime Type Tokens

The java.lang.Class class became generic in JDK 5.0. Now Class has a type parameter T representing the type of the class object it represents.

For example, String.class has type Class<String>, and Serializable.class has type Class<Serializable>. This improves type safety for reflective code.

Since Class's newInstance() method now returns T, you get more precise types when creating objects reflectively:

Collection<EmployeeInfo> 
    employees = sqlUtility.select(EmployeeInfo.class, "select * from emps");
...
public static <T> Collection<T> select(Class<T> clazz, String sqlStatement) { 
    Collection<T> result = new ArrayList<T>();
    /* Run sql query using jdbc. */
    for (/* Iterate over jdbc results. */ ) { 
        T item = clazz.newInstance(); 
        /* Use reflection and set all of item's
         * fields from sql results. 
         */  
        result.add(item);
    } 
    return result; 
}

More Complex Wildcard Usage

Consider interface Sink as an example of a write-only data structure:

interface Sink<T> {
    void flush(T item);
}

We might imagine using it as follows:

public static <T> T writeAll(Collection<T> source, Sink<T> sink) {
    T lastItem;
    for (T item : source) {
        lastItem = item;
        sink.flush(lastItem);
    }
    return lastItem;
}
...
Sink<Object> sink;
Collection<String> stringCollection;
String result = writeAll(stringCollection, sink); // Illegal call.

The call to writeAll() is illegal because no valid type parameter can be inferred.

We can fix this by modifying writeAll()'s signature:

public static <T> T writeAll(Collection<? extends T> source, Sink<T> sink) {...}
...
// Call is OK, but wrong return type.
String result = writeAll(stringCollection, sink);

Now the call is legal, but assignment is wrong because the inferred return type is Object.

The solution uses bounded wildcards with lower bounds. Syntax ? super T means an unknown type that is a supertype of T:

public static <T> T writeAll(Collection<T> source, Sink<? super T> sink) {
    // implementation
}
String result = writeAll(stringCollection, sink); // Success!

Wildcard Capture

Given:

Set<?> unknownSet = new HashSet<String>();
...
/* Add an element item to a Set collection. */ 
public static <T> void addToSet(Set<T> collection, T item) {
    // implementation
}

The following call is illegal:

addToSet(unknownSet, "abc"); // Illegal.

There's a special rule called wildcard capture that allows the compiler to infer the unknown wildcard type as a generic method's type parameter in specific situations where the code is provably safe.

Converting Legacy Code to Use Generics

Converting legacy APIs to generics requires careful consideration. The generic API must not be overly restrictive while maintaining binary compatibility with legacy clients.

For example, Collections.max() has the signature:

public static <T extends Object & Comparable<? super T>> 
        T max(Collection<? extends T> collection)

This accommodates reading from input collections containing subtypes of T.

Tags: java generics wildcards Type Safety Legacy Code

Posted on Sat, 25 Jul 2026 16:24:37 +0000 by bicho83