Skip to content
Java java5 6 min read

Generics

Generics let you write classes, interfaces, and methods that work with any type you specify at compile time, catching type mismatches before your program ever runs. Introduced in Java 5, they’re the engine behind the entire Collections Framework — and once you understand them, you’ll write far more reusable, self-documenting code.

Why Generics?

Before generics, a list held raw Object references. You could put a String and an Integer into the same list — which sounds flexible but leads to ClassCastException surprises at runtime.

// Pre-generics (raw type) — avoid this
import java.util.ArrayList;

public class RawExample {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add("Hello");
        list.add(42);                    // compiles — but dangerous
        String s = (String) list.get(1); // ClassCastException at runtime!
    }
}

With generics, the compiler rejects the bad cast entirely:

import java.util.ArrayList;

public class GenericExample {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();
        names.add("Alice");
        names.add("Bob");
        // names.add(42); // compile-time error — great!
        String first = names.get(0);     // no cast needed
        System.out.println(first);
    }
}

Output:

Alice

The payoff: zero runtime ClassCastException, no manual casting, and self-documenting code.

Generic Classes

You define a generic class by adding a type parameter in angle brackets after the class name. By convention, single uppercase letters are used: T (type), E (element), K (key), V (value).

public class Box<T> {
    private T value;

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

    public T getValue() {
        return value;
    }

    public static void main(String[] args) {
        Box<String>  strBox = new Box<>("Hello Generics");
        Box<Integer> intBox = new Box<>(100);

        System.out.println(strBox.getValue());
        System.out.println(intBox.getValue());
    }
}

Output:

Hello Generics
100

Tip: From Java 7 onward, you can use the diamond operator <> on the right side — the compiler infers the type argument so you don’t repeat yourself: new Box<>("text") instead of new Box<String>("text").

Generic Methods

A method can declare its own type parameters independently of its class. The type parameter is placed before the return type:

public class Utils {
    // Works for any array type
    public static <T> void printArray(T[] items) {
        for (T item : items) {
            System.out.print(item + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        String[]  words   = {"Java", "Generics", "Rocks"};
        Integer[] numbers = {1, 2, 3, 4, 5};

        printArray(words);
        printArray(numbers);
    }
}

Output:

Java Generics Rocks 
1 2 3 4 5 

Bounded Type Parameters

Sometimes you want to restrict which types are allowed. Use extends to set an upper bound:

public class Stats {
    // T must be a Number (or subclass: Integer, Double, etc.)
    public static <T extends Number> double sum(T[] arr) {
        double total = 0;
        for (T n : arr) {
            total += n.doubleValue();
        }
        return total;
    }

    public static void main(String[] args) {
        Integer[] ints    = {1, 2, 3, 4};
        Double[]  doubles = {1.5, 2.5, 3.0};

        System.out.println(sum(ints));    // 10.0
        System.out.println(sum(doubles)); // 7.0
    }
}

Output:

10.0
7.0

Note: In a bounded parameter like <T extends Comparable<T>>, extends is used even for interfaces — it means “T implements Comparable”.

Multiple Bounds

A type parameter can have multiple bounds, separated by &. The class (if any) must come first:

// T must extend Animal AND implement Serializable
public class Cage<T extends Animal & java.io.Serializable> { ... }

Wildcards

Wildcards (?) let you write flexible method signatures that accept a family of parameterised types.

WildcardMeaningTypical use
?Unknown typeRead-only iteration
? extends TT or any subtypeReading (producer)
? super TT or any supertypeWriting (consumer)
import java.util.List;

public class WildcardDemo {

    // Upper-bounded: reads from any list of Numbers
    static double sumList(List<? extends Number> list) {
        double total = 0;
        for (Number n : list) total += n.doubleValue();
        return total;
    }

    // Lower-bounded: adds integers into any list of Number or Object
    static void addIntegers(List<? super Integer> list) {
        list.add(10);
        list.add(20);
    }

    public static void main(String[] args) {
        List<Integer> ints = List.of(1, 2, 3);
        System.out.println(sumList(ints)); // 6.0
    }
}

Output:

6.0

Tip: A handy mnemonic is PECSProducer Extends, Consumer Super. If a collection produces values you read, use ? extends. If it consumes values you write, use ? super.

Generic Interfaces

Interfaces can be generic too. The classic example is the Comparable interface:

public class Student implements Comparable<Student> {
    String name;
    int grade;

    Student(String name, int grade) {
        this.name = name;
        this.grade = grade;
    }

    @Override
    public int compareTo(Student other) {
        return Integer.compare(this.grade, other.grade);
    }
}

Under the Hood: Type Erasure

Here is the most important thing to understand if you want to avoid subtle bugs: Java generics are erased at compile time.

The compiler:

  1. Replaces all type parameters with their bounds (or Object for unbounded parameters).
  2. Inserts casts where necessary.
  3. Generates bridge methods for polymorphism to work correctly.

The compiled bytecode contains no T, E, or K — just Object or the bound type. This is called type erasure, and it was chosen to maintain backward compatibility with pre-Java-5 bytecode.

// What you write
Box<String> b = new Box<>("hi");

// What the compiler effectively produces in bytecode
Box b = new Box("hi");
String s = (String) b.getValue();

Consequences of Type Erasure

  • You cannot create an instance of a type parameter: new T() is illegal.
  • You cannot create a generic array: new T[10] is illegal.
  • instanceof checks against a generic type are not allowed: obj instanceof List<String> won’t compile.
  • At runtime, Box<String> and Box<Integer> are the same classgetClass() returns the same object.
import java.util.ArrayList;

public class ErasureDemo {
    public static void main(String[] args) {
        var strings  = new ArrayList<String>();
        var integers = new ArrayList<Integer>();
        System.out.println(strings.getClass() == integers.getClass()); // true
    }
}

Output:

true

Warning: Avoid raw types (e.g., List instead of List<String>) in modern code. They suppress compiler checks and re-introduce the ClassCastException risks that generics were designed to prevent. The compiler will warn you — treat those warnings seriously.

Generics vs Arrays

Arrays and generics don’t mix well because arrays are covariant (an Integer[] is a String[]… wait, no — and the JVM will throw ArrayStoreException), while generics are invariant (List<Integer> is NOT a List<Number>).

FeatureArrayGeneric collection
Type-safetyRuntime checkCompile-time check
CovarianceYes (Number[] n = new Integer[5])No (List<Number>List<Integer>)
ReifiableYesNo (erased)
Preferred forPrimitives, performanceObject collections

For most use cases, prefer generic collections like ArrayList over raw arrays of objects.

In This Section

This section covers several Java 5 language features that complement Generics:

  • Annotations — Add metadata to your code that tools, frameworks, and the compiler can read at build time or runtime.
  • Autoboxing & Unboxing — How Java automatically converts between primitives (int, double) and their wrapper types (Integer, Double) so they work seamlessly with generics.
  • Varargs — Write methods that accept a variable number of arguments using the ... syntax, often combined with generics for flexible utility methods.
  • Static Import — Import static members directly so you can write sort(list) instead of Collections.sort(list), reducing boilerplate in test and utility code.
  • Collections Framework — Generics power the entire collections API; start here to see them in action.
  • ArrayList — The most-used generic collection, a great place to apply what you’ve learned.
  • Comparable — A classic generic interface you’ll implement to make your objects sortable.
  • Comparator — Another generic interface for custom sort orders, often passed as a lambda.
  • Autoboxing & Unboxing — Understand how primitives flow in and out of generic collections automatically.
  • Lambda Expressions — Modern Java combines generics with lambdas and functional interfaces for powerful, concise APIs.
Last updated June 13, 2026
Was this helpful?