Fundamentals 20 min read

Understanding Java Generics: Theory, Wildcards, and Practical Examples

This article provides a comprehensive guide to Java generics, covering compilation vs runtime, generic classes, interfaces, methods, wildcard types, bounded and unbounded usage, and practical code examples demonstrating how to design flexible, type‑safe collection utilities and apply generic constraints effectively.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Understanding Java Generics: Theory, Wildcards, and Practical Examples

Introduction

Java generics are a core language feature that enable type‑safe collections and APIs. Introduced in JDK 1.5, they exist only at compile time; at runtime the type information is erased to Object . Understanding how generics work and how to use them correctly is essential for writing robust Java code.

Compilation vs Runtime

During compilation the source file ( .java ) is transformed into bytecode ( .class ) which the JVM loads and executes. Generics are checked by the compiler and disappear after compilation (type erasure).

What Is a Generic?

A generic, also called a parameterized type, lets you define a class, interface, or method with a placeholder type (the "bowl") that the user later substitutes with a concrete type (the "contents"). This improves safety by allowing the compiler to verify that only compatible objects are stored in collections.

Generic Class Example

public class GenericClass
{
    // member variable
    private T t;

    public void function(T t) {
        // implementation
    }

    // not a generic method, just uses the class's type parameter
    public T functionTwo(T t) {
        return t;
    }
}

Generic Interface Example

public interface GenericInterface
{
    T get();
    void set(T t);
    T delete(T t);
    default T defaultFunction(T t) {
        return t;
    }
}

Generic Method Example

public class GenericFunction {
    public
void function(T t) { }
    public
T functionTwo(T t) { return t; }
    public
String functionThree(T t) { return ""; }
}

Wildcards

Wildcards ( ?> ) allow a generic type to be flexible about the actual type argument. Three forms exist:

<?> : unbounded wildcard – any type.

<? extends T> : upper‑bounded wildcard – any subtype of T ; read‑only.

<? super T> : lower‑bounded wildcard – any supertype of T ; write‑only.

These are often summarized by the PECS principle (Producer‑extends, Consumer‑super).

Wildcard Usage Scenarios

When a method only needs to read from a collection, use an upper‑bounded wildcard; when it only writes to a collection, use a lower‑bounded wildcard. If the method both reads and writes, use a plain generic type without wildcards.

Unbounded Wildcard Example

public static
int size(Collection
list) {
    return list.size();
}

public static int sizeTwo(Collection
list) {
    return list.size();
}

Both methods work, but sizeTwo expresses that the element type is irrelevant.

Upper‑Bounded Wildcard Example (read‑only)

public static
int beMixedSum(Set
s1, Set
s2) {
    int i = 0;
    for (T t : s1) {
        if (s2.contains(t)) i++;
    }
    return i;
}

public static int beMixedSumTwo(Set
s1, Set
s2) {
    int i = 0;
    for (Object o : s1) {
        if (s2.contains(o)) i++;
    }
    return i;
}

Both compute the intersection size; the wildcard version makes the intent clearer.

Upper‑Bounded Wildcard in a Utility Class

public class CollectionUtils
{
    // generic version – works only when the source and target have the exact same type
    public List
listCopy(Collection
collection) {
        List
newCollection = new ArrayList<>();
        for (T t : collection) {
            newCollection.add(t);
        }
        return newCollection;
    }

    // upper‑bounded version – can copy from a collection of any subtype of T
    public List
listCopyTwo(Collection
collection) {
        List
newCollection = new ArrayList<>();
        for (T t : collection) {
            newCollection.add(t);
        }
        return newCollection;
    }
}

Using ? extends T allows copying from List or List into a List target.

Lower‑Bounded Wildcard Example (write‑only)

public class CollectionUtils
{
    // lower‑bounded target – can write any subtype of T into the target collection
    public void copy(List
target, List
src) {
        if (src.size() > target.size()) {
            for (int i = 0; i < src.size(); i++) {
                target.set(i, src.get(i));
            }
        }
    }
}

This method can copy a List into a List because the target accepts any supertype of Son .

Bounded Type Parameters

Sometimes you need to restrict a generic type to a specific hierarchy. Use a bounded type parameter like <T extends Grandpa> to allow only Grandpa and its subclasses.

public class GenericClass
{
    public void test(T t) { /* ... */ }
}

public static void main(String[] args) {
    GenericClass
g1 = new GenericClass<>();
    g1.test(new Grandpa());
    g1.test(new Father());
    g1.test(new Son());

    GenericClass
g2 = new GenericClass<>();
    g2.test(new Father());
    g2.test(new Son());

    GenericClass
g3 = new GenericClass<>();
    g3.test(new Son());
}

Recursive Generics

Recursive generics are useful when a type must be comparable to itself. The classic example is a max utility that works on any Comparable element.

public class Person implements Comparable
{
    private int age;
    public Person(int age) { this.age = age; }
    public int getAge() { return age; }
    @Override
    public int compareTo(Person o) { return this.age - o.age; }
}

public class CollectionUtils {
    public static
> E max(List
list) {
        E result = null;
        for (E e : list) {
            if (result == null || e.compareTo(result) > 0) {
                result = e;
            }
        }
        return result;
    }
}

public static void main(String[] args) {
    List
persons = new ArrayList<>();
    persons.add(new Person(12));
    persons.add(new Person(19));
    persons.add(new Person(20));
    persons.add(new Person(5));
    persons.add(new Person(18));
    Person oldest = CollectionUtils.max(persons);
}

Best Practices

Use plain generics when a method both reads and writes. Apply upper‑bounded wildcards for read‑only parameters and lower‑bounded wildcards for write‑only parameters. Avoid raw types because they discard compile‑time type safety.

Understanding these rules helps you design flexible, reusable APIs and prevents common generic‑related bugs.

JavagenericsCollectionstype safetyWildcard
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.