100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Java

Generics in Java

Understand how Java generics provide compile-time type safety with type parameters, bounded types, wildcards, and the concept of type erasure.

Multithreading & Advanced JavaIntermediate8 min readJul 7, 2026
Analogies

1. Introduction

Generics allow classes, interfaces, and methods to be written with type parameters, letting the same code work with different data types while the compiler enforces type safety. Before generics (pre-Java 5), collections stored everything as Object, requiring explicit casts that could fail at runtime with a ClassCastException; generics push that type checking to compile time instead.

🏏

Cricket analogy: Before DRS, umpires guessed lbw calls and got them wrong on the field, like pre-Java-5 code casting Objects and blowing up at runtime; generics are like DRS, catching the wrong call before it's locked in.

A common example is java.util.List<String>, which guarantees at compile time that only String objects can be added to and retrieved from that list, eliminating the need for explicit casting when reading elements out of it.

🏏

Cricket analogy: A team sheet that only allows specialist batsmen in the top order guarantees the captain never has to double-check who's walking out to bat, the same way List<String> guarantees every element is already a String.

2. Syntax

java
// Generic class with a type parameter T
class Box<T> {
    private T content;
    public void set(T content) { this.content = content; }
    public T get() { return content; }
}

// Bounded type parameter — T must extend Number
class NumericBox<T extends Number> {
    private T value;
    public double asDouble() { return value.doubleValue(); }
}

// Generic method
public static <T> T firstElement(List<T> list) {
    return list.get(0);
}

// Wildcards
List<? extends Number> readOnlyNumbers; // can read Numbers, can't add
List<? super Integer> writableInts;      // can add Integers safely

3. Explanation

A type parameter such as <T> acts as a placeholder for an actual type supplied when the generic class or method is used, e.g. Box<String> or Box<Integer>. This gives compile-time type safety: the compiler rejects attempts to insert the wrong type, and no explicit cast is required when retrieving elements, because the compiler already knows the exact type.

🏏

Cricket analogy: A kit bag labeled 'Box<Bat>' versus 'Box<Ball>' means the equipment manager instantly knows what's inside without opening it, just like Box<String> vs Box<Integer> tells the compiler the exact contents without a cast.

Bounded type parameters, written as <T extends SomeType>, restrict T to SomeType or its subtypes, which is useful when the generic code needs to call methods defined on that bound (e.g. calling doubleValue() requires T to extend Number). Wildcards (?) are used in method parameters to express flexibility: <? extends T> accepts T or any subtype (read-only, 'producer'), while <? super T> accepts T or any supertype (write-safe, 'consumer') — this is often summarized by the mnemonic PECS ('Producer Extends, Consumer Super').

🏏

Cricket analogy: Restricting a bowling attack selector to '<T extends FastBowler>' means only pacers who can genuinely clock 140kmh get picked, just as a bounded type parameter restricts T to types with the needed capability like doubleValue().

Exam trap: type erasure. Generic type information exists only at compile time; the compiler erases it and replaces type parameters with their bound (Object if unbounded) in the compiled bytecode. This means you cannot do new T(), cannot use instanceof with a parameterized type like obj instanceof List<String>, and at runtime, via reflection, list.getClass() returns java.util.ArrayList regardless of whether it was declared List<String> or List<Integer> — the generic type info is not available at runtime.

4. Example

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

public class GenericsDemo {

    static class Box<T> {
        private T content;
        public void set(T content) { this.content = content; }
        public T get() { return content; }
    }

    static <T extends Number> double sumAsDouble(List<T> numbers) {
        double sum = 0.0;
        for (T n : numbers) {
            sum += n.doubleValue();
        }
        return sum;
    }

    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        stringBox.set("Hello Generics");
        String value = stringBox.get(); // no cast needed
        System.out.println(value);

        List<Integer> ints = new ArrayList<>();
        ints.add(10);
        ints.add(20);
        ints.add(30);
        System.out.println("Sum: " + sumAsDouble(ints));

        // Type erasure demonstration
        List<String> strList = new ArrayList<>();
        List<Integer> intList = new ArrayList<>();
        System.out.println("Same runtime class? " + (strList.getClass() == intList.getClass()));
    }
}

5. Output

text
Hello Generics
Sum: 60.0
Same runtime class? true

6. Key Takeaways

  • Generics provide compile-time type safety and eliminate the need for explicit casts.
  • A type parameter like <T> is a placeholder replaced by an actual type when the generic is used.
  • Bounded type parameters (<T extends Number>) restrict what types can be used and what methods are callable.
  • Wildcards <? extends T> and <? super T> add flexibility to method parameters (PECS: Producer Extends, Consumer Super).
  • Type erasure removes generic type information at compile time — it is not available at runtime via reflection.
  • Because of type erasure, new T() and instanceof List<String> are not allowed.

Practice what you learned

Was this page helpful?

Topics covered

#Java#JavaProgrammingStudyNotes#Programming#GenericsInJava#Generics#Syntax#Explanation#Example#StudyNotes#SkillVeris