Generics in Java: Type Parameters, Bounds, Wildcards and Runnable Examples

Build type-safe Java containers, then trace generic methods, upper bounds, wildcard reads and writes, invariance, erasure, and common failure cases.

KnowledgeGate Team

Exam prep & CS education

Updated 21 Sep 20266 min read

A collection that accepts anything looks flexible until a value returns with the wrong type and fails at runtime. One runnable GenericsDemo.java uses Box<String>, Box<Integer>, Pair<String, Integer>, marks [12, 18, 30], and a number sink that changes from [2.5] to [2.5, 7, 11]. It demonstrates type parameters, invariance, wildcards and erasure.

Why generics move a type error to the compiler

A generic declaration uses a type parameter. In Box<T>, T is the placeholder; Box<String> supplies the type argument. Its set() accepts a String, and get() returns one without a cast.

Box<String> topic = new Box<>("Generics"); topic.set(99); is a compile-time error because 99 is an Integer. With the raw type, Box raw = new Box("Generics"); raw.set(99); String text = (String) raw.get(); compiles with warnings, but the cast throws ClassCastException. Unlike Box<Object>, raw Box discards generic checking.

The Coding & Skill Development Courses path groups programming and data-structure topics; generic containers make reusable code type-safe.

Build and run one complete generic program

Save this program as GenericsDemo.java. A class declares type parameters after its name; a generic method declares <T> before its return type.

java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

final class Box<T> {
    private T value;

    Box(T value) {
        this.value = value;
    }

    T get() {
        return value;
    }

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

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

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

    K getKey() {
        return key;
    }

    V getValue() {
        return value;
    }
}

public class GenericsDemo {
    static <T> T first(List<T> items) {
        if (items.isEmpty()) {
            throw new IllegalArgumentException("empty list");
        }
        return items.get(0);
    }

    static double sum(List<? extends Number> values) {
        double total = 0.0;
        for (Number value : values) {
            total += value.doubleValue();
        }
        return total;
    }

    static <T extends Number> double average(List<T> values) {
        if (values.isEmpty()) {
            throw new IllegalArgumentException("empty list");
        }
        return sum(values) / values.size();
    }

    static void addDefaults(List<? super Integer> target) {
        target.add(7);
        target.add(11);
    }

    public static void main(String[] args) {
        Box<String> topic = new Box<>("Generics");
        Box<Integer> attempts = new Box<>(3);
        Pair<String, Integer> result = new Pair<>("Asha", 88);
        List<Integer> marks = new ArrayList<>(Arrays.asList(12, 18, 30));
        List<Number> sink = new ArrayList<>();
        sink.add(2.5);
        addDefaults(sink);

        System.out.println(topic.get());
        System.out.println(attempts.get());
        System.out.println(result.getKey() + " -> " + result.getValue());
        System.out.println(first(marks));
        System.out.println(sum(marks));
        System.out.println(average(marks));
        System.out.println(sink);
    }
}

Compile and run it:

Code
javac GenericsDemo.java
java GenericsDemo

The exact output is:

Code
Generics
3
Asha -> 88
12
60.0
20.0
[2.5, 7, 11]

The arithmetic is 12 + 18 + 30 = 60, so sum returns 60.0. With three marks, 60 / 3 = 20.0.

Box<T> resolving to Box<String> holding "Generics" and Box<Integer> holding 3, with Pair<String,Integer> as key "Asha" and value 88.

Read type parameters, arguments and inference

Box<T> has one class type parameter; Pair<K, V> has two. In static <T> T first(List<T> items), method-level T appears before the return type. From first(marks), the compiler infers Integer and returns 12.

The diamond uses context. In new Box<>("Generics"), the left side supplies Box<String>. Box<Number> n = new Box<>(3) is valid because Integer fits Number; Box<Integer> n = new Box<>(2.5) is invalid because 2.5 is a Double.

Names such as T, E, K and V are conventions. For key-value structures related to Pair<K, V>, see Hashing and Collision Resolution.

Put an upper bound on a type and use extends

static <T extends Number> double average(List<T> values) uses an upper bound. T can be Integer, Double, or another Number subtype, so the code can use Number operations through sum.

For marks, inference gives T = Integer: 12.0 + 18.0 + 30.0 = 60.0, the count is 3, and 60.0 / 3 = 20.0. average(Arrays.asList(1.5, 2.5)) infers T = Double and returns (1.5 + 2.5) / 2 = 2.0.

List<? extends Number> means a list of an unknown Number subtype, suitable for reading numeric values. sum(marks) reads 12, 18 and 30 as Number and returns 60.0.

After List<? extends Number> source = marks, Number firstValue = source.get(0) gives 12, but source.add(40) fails because the element type is unknown. The list is not necessarily immutable; reads and some removals remain available.

Understand invariance and apply PECS

List<Integer> is not a subtype of List<Number>. If List<Number> numbers = marks were legal, code could add 2.5 and violate the original List<Integer> contract. The assignment fails although Integer is a Number.

PECS gives the direction. List<? extends Number> produces values readable as Number but cannot accept 40. List<? super Integer> consumes integers: the List<Number> sink starts as [2.5], accepts 7 and 11, and ends as [2.5, 7, 11].

From List<? super Integer>, a read is known only as Object because the list could hold Integer, Number, or Object. Stacks and Queues: Operations and Uses provides another setting for producer-consumer thinking.

PECS flow: sum reads 12, 18 and 30 from List<? extends Number> as 60.0; addDefaults writes 7 and 11 into List<? super Integer>.

Know what type erasure changes and forbids

Generic checks happen at compile time. Runtime does not create separate classes for Box<String> and Box<Integer>, so topic.getClass() == attempts.getClass() is true. The compiler inserts casts and bound checks; some declarations remain as class-file metadata.

Erasure also explains several restrictions:

  • new T() is illegal. Accept a factory or an already-created value.

  • new T[5] and new List<String>[5] are illegal. Prefer a generic collection.

  • value instanceof List<String> is illegal, while value instanceof List<?> is allowed.

  • A static field cannot use the class's T because it belongs to the class, not an instance with a chosen argument.

  • Box<int> is illegal because type arguments are reference types. Use Box<Integer>.

Overloads process(List<String> x) and process(List<Integer> x) cannot coexist because both erase to process(List). This clash differs from the raw-type ClassCastException above.

Diagnose common errors and practise assessment shapes

Match each symptom to its fix:

  • topic.set(99) fails because the box requires String. Pass a string or use another box.

  • List<Number> widened = marks fails because lists are invariant. Use a suitable wildcard view.

  • source.add(40) fails because extends hides the element type. Add through the original List<Integer> when valid.

  • Integer x = consumer.get(0) fails because a super read is known only as Object.

  • A raw Box may compile with warnings but loses safety. Parameterise it.

Assessments may ask you to place <T>, infer a type, test compilation, choose extends or super, predict a raw failure, or find an erasure clash.

Try these before checking the answers:

  1. For Box<Double> rate = new Box<>(2.5);, give the static type, returned type, and value.

  2. Evaluate first(Arrays.asList("Java", "DSA")).

  3. Evaluate sum(Arrays.asList(4, 6, 10)).

  4. Choose a parameter type that reads both List<Integer> and List<Double> as numbers without adding.


Answers

  1. Static type Box<Double>, return type Double, value 2.5.

  2. String value "Java".

  3. 20.0, because 4 + 6 + 10 = 20.

  4. List<? extends Number>.

Short version and the next Java step

Declare T, infer or supply its argument, bound it when operations need a common parent, remember invariance, read from extends, write to super, and account for erasure. Change marks to [5, 15, 25, 35], then predict first = 5, sum = 80.0, and average = 20.0 before running. Continue with the Java Course: Concepts, MCQs & Coding Questions, then apply type-safe containers in the DSA using Java course.