Command Palette

Search for a command to run...

PHASE 8Intermediate Java 5+ ~32 min· topic 2 of 6

Topic 8.2

Generic Classes and Interfaces

In one line

A generic class declares one or more type parameters, like class Box<T>, and uses them as types for its fields, parameters and return values. Each user picks the real type (Box<String>, Box<Integer>), and the compiler checks every use against it.

Think of it like this

A shop sells the same stackable storage tray in one design, and you write on the front what goes in it: "screws", "buttons", "stamps". The factory made one tray, not one per item. A generic class is that one design. You write it once with a blank label T, and each time someone uses it they write the real type on the label.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Generic class
A class that declares type parameters in angle brackets after its name, like class Box<T>.
Generic interface
An interface with type parameters, like Comparable<T> or List<E>.
Parameterized type
A generic type with real type arguments filled in, like Box<String> or Map<String, Integer>.
Type variable
Another name for a type parameter as it's used inside the class body, like the T in T get().
Invariant
Having no subtype relationship between different type arguments: Box<Integer> and Box<Number> are unrelated types, even though Integer extends Number.
Signature attribute
Extra information in a .class file that remembers generic declarations like Box<T> for the compiler and reflection. The JVM doesn't use it to run code.
Iterable
The generic interface Iterable<T> with an iterator() method. Anything that implements it can be used in a for-each loop.

Step by step

01Declare a generic class

Write the type parameters after the class name. From then on, inside the class, T is a type like any other: you declare fields, parameters and return types with it.

The constructor is written Box(T value), without <T> after the name; the class's T is already in scope.

Box.javawhole filejava
public class Box<T> {
    private T value;

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

    public T get() { return value; }

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

02Use it with different type arguments

Box<String> name = new Box<>("Asha"); makes the compiler treat T as String for everything you do with name. Box<Integer> count = new Box<>(3); is checked against Integer. Same class, different checks.

With the diamond, the compiler infers the type argument from the variable and the constructor argument. If they disagree (Box<String> b = new Box<>(42);) you get an inference error naming both bounds.

terminal
$ javac Main.java
── expected output ──
Main.java:8: error: incompatible types: cannot infer type arguments for Box<>
Box<String> b = new Box<>(42);
^
reason: inference variable T has incompatible bounds
equality constraints: String
lower bounds: Integer
where T is a type-variable:
T extends Object declared in class Box
1 error

03One class file, two views of it

javap -p prints the generic view, read from the Signature attribute. Adding -s prints each member's descriptor, the type the JVM actually uses: Ljava/lang/Object;. The field really is an Object, and get() really returns Object.

That's why Box<String> and Box<Integer> don't create new classes or extra memory: they're the same Box at run time. Topic 8.6 covers the consequences.

terminal
$ javac Box.java
javap -p -s Box
── expected output ──
Compiled from "Box.java"
public class Box<T> {
private T value;
descriptor: Ljava/lang/Object;
public Box(T);
descriptor: (Ljava/lang/Object;)V
 
public T get();
descriptor: ()Ljava/lang/Object;
 
public void set(T);
descriptor: (Ljava/lang/Object;)V
}

04Several type parameters

Separate them with commas: class Pair<K, V>. Each is chosen independently: Pair<String, Integer>. Type arguments can themselves be parameterized: Pair<String, List<Integer>>.

Since Java 16 a record can declare type parameters too. record Pair<K, V>(K key, V value) {} gives you a generic, immutable pair with equals, hashCode and toString (Topic 4.9).

Main.javawhole filejava
record Pair<K, V>(K key, V value) { }

Pair<String, Integer> score = new Pair<>("Asha", 92);
String who = score.key();       // String, no cast
int marks = score.value();      // Integer, unboxed to int
Pair<String, Pair<Integer, Integer>> range = new Pair<>("Q1", new Pair<>(1, 3));

05Generic interfaces and the two ways to implement them

A class implementing Stack<T> can fix the type: class NameStack implements Stack<String>, whose push then takes String. Or it can stay generic and pass the parameter through: class ArrayStack<T> implements Stack<T>, letting its own users choose.

Extending a generic class follows the same rule. A subclass can also add its own parameters: class TimedBox<T, U> extends Box<T>.

Main.javawhole filejava
interface Stack<T> {
    void push(T item);
    T pop();
    boolean isEmpty();
}

class NameStack implements Stack<String> { ... }      // fixes T = String
class ArrayStack<T> implements Stack<T> { ... }       // stays generic
Generic interfaces and the two ways to implement themdiagram
Rendering diagram…

06Static members can't see T

T is chosen per object type (Box<String> vs Box<Integer>), but a static field exists once for the whole class. Which T would it be? Neither, so the compiler forbids it.

A static method can still be generic by declaring its own type parameter: static <U> Box<U> of(U value). That's a generic method, the subject of Topic 8.3.

terminal
$ javac Main.java
── expected output ──
Main.java:2: error: non-static type variable T cannot be referenced from a static context
static T lastValue;
^
1 error

07Box<Integer> is not a Box<Number>

It looks natural to pass a Box<Integer> where a Box<Number> is wanted. But a Box<Number> lets you call set(3.5). If the assignment were allowed, the Box<Integer> would end up holding a Double, and a later int x = ints.get(); would crash.

So generic types are invariant. Arrays made the opposite choice (Integer[] is a Number[]) and pay for it with a runtime ArrayStoreException. You'll see how Box<? extends Number> solves the read-only case in Topic 8.5.

terminal
$ javac Main.java
── expected output ──
Main.java:8: error: incompatible types: Box<Integer> cannot be converted to Box<Number>
Box<Number> nums = ints;
^
1 error

Try it yourself

  1. 1

    Make the stack a stack of records

    In the stack example, add record Order(String id, int qty) {} inside Main (needs Java 16+), create a Stack<Order>, push two orders and pop one. Predict what toString prints, then run.

  2. 2

    Fix a type parameter on purpose

    In the converter example, write class Shout implements Converter<String, String> that returns the input in upper case, and use it. Then try declaring Converter<Integer, String> bad = new Shout();. Predict the compiler message.

  3. 3

    Count through the iterator

    In the linked-list example, add a LinkedBag<Integer> with 4, 8 and 15 and sum it with a for-each loop. Predict the total first. Then remove implements Iterable<T> and read the compiler error on the for-each line.

Code & diagrams

A generic stack used with two element types New tab
Sign in to run this example in your browser.

Expected output

Stack[home, search, product]
back from: product
now on: search
sum: 60
error: stack is empty
Implementing a generic interface: fix the type or pass it through New tab
Sign in to run this example in your browser.

Expected output

6
price: 40
temp: 36.6
true true
A generic record and nested type arguments Java 16+ New tab
Sign in to run this example in your browser.

Expected output

Pair[key=Asha, value=92], key length 4
Pair[key=Asha, value=A+]
Ravi best: 81
{evening=Pair[key=17, value=21], morning=Pair[key=6, value=12]}
equal pairs: true
A generic linked list that works in for-each New tab

The nested Node is static, so it can't see LinkedBag's T. It declares its own T, and LinkedBag uses Node<T> to connect the two.

Sign in to run this example in your browser.

Expected output

size: 3
guava has 5 letters
banana has 6 letters
mango has 5 letters

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Use T in a static field

Inside class Box<T>, add static T lastValue;.

terminal
$ javac Main.java
── what you'll see ──
Main.java:2: error: non-static type variable T cannot be referenced from a static context
static T lastValue;
^
1 error

Break #2

Assume Box<Integer> is a Box<Number>

Write Box<Integer> ints = new Box<>(); Box<Number> nums = ints;.

terminal
$ javac Main.java
── what you'll see ──
Main.java:8: error: incompatible types: Box<Integer> cannot be converted to Box<Number>
Box<Number> nums = ints;
^
1 error

Break #3

Pass a constructor argument that disagrees with the declared type

Write Box<String> b = new Box<>(42);.

terminal
$ javac Main.java
── what you'll see ──
Main.java:8: error: incompatible types: cannot infer type arguments for Box<>
Box<String> b = new Box<>(42);
^
reason: inference variable T has incompatible bounds
equality constraints: String
lower bounds: Integer
where T is a type-variable:
T extends Object declared in class Box
1 error

Myth vs fact

Myth

Each parameterization like Box<String> creates a new class at run time.

Fact

There is exactly one Box.class. Box<String> and Box<Integer> share its bytecode and its static fields; the type arguments exist only for the compiler (and as metadata for reflection).

Myth

Type parameters must be called T.

Fact

Any identifier works, but the convention is a single capital letter (T, E, K, V, R) so a reader never confuses a type variable with a real class like Item.

Myth

If Integer extends Number, then List<Integer> extends List<Number>.

Fact

Parameterized types are invariant: no subtype relation between List<Integer> and List<Number>. Wildcards (List<? extends Number>) provide the safe version of that relationship.

Myth

A non-static inner class needs its own type parameter.

Fact

An inner (non-static) class already sees the outer class's T. Only static nested classes, like Node<T> in the linked list example, need to declare their own.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Generic information survives compilation as the Signature attribute on classes, fields and methods. Reflection reads it: Box.class.getTypeParameters() returns [T], and a field declared List<String> names reports java.util.List<java.lang.String> from getGenericType(). Frameworks like Jackson and Spring rely on this to know element types of declared fields, even though objects don't carry them.

  • ▸

    A class that fixes a type argument when extending (class StringBox extends Box<String>) and overrides set(String) gets a compiler-generated bridge method set(Object) that casts and forwards. You'll see these in javap output in Topic 8.6; they're how overriding still works after erasure.

  • ▸

    Inner (non-static) classes of a generic class are implicitly parameterized by the outer type: inside LinkedBag<T>, an inner class Itr can use T directly, and its full type is LinkedBag<T>.Itr. Static nested classes are independent, which is why HashMap.Node<K,V> declares its own parameters.

  • ▸

    The type parameter section can also declare bounds (class Sorted<T extends Comparable<T>>, Topic 8.4). Without one, every type variable's bound is Object, which is exactly what erasure replaces it with.

Remember this

  1. 1

    You declare type parameters in angle brackets right after the class name: class Box<T>, class Pair<K, V>. Inside the class, T can be used almost anywhere a type can: field types (private T value;), parameter types (void set(T v)), return types (T get()), local variables, and as a type argument to other generic types (List<T> items).

  2. 2

    Each parameterized type like Box<String> is a separate compile-time type. The compiler substitutes String for T when checking code that uses it, so box.set(42) fails and box.get() returns String. But there is still only one class file, Box.class, and one Box class at run time. The compiler records the generic signature as metadata (the Signature attribute), and the real field and method types in the bytecode are Object.

  3. 3

    By convention type parameters are single capital letters so they never look like real class names: T (type), E (element), K and V (key and value), N (number), R (result), and S, U for second and third types. Map<K, V> and Function<T, R> in the JDK follow this.

  4. 4

    Generic interfaces work the same way: interface Comparable<T> { int compareTo(T o); }. A class implementing one either fixes the type (class Money implements Comparable<Money>) or passes its own parameter through (class ArrayStack<T> implements Stack<T>). Extending a generic class has the same two choices: class StringBox extends Box<String> or class LabeledBox<T> extends Box<T>.

  5. 5

    A type parameter belongs to an instance: it's chosen when an object's type is written, so static members can't use it. static T last; is an error, non-static type variable T cannot be referenced from a static context, because one static field is shared by Box<String> and Box<Integer> alike. Static methods that need a type must declare their own (Topic 8.3).

  6. 6

    Parameterized types are invariant: Box<Integer> is not a subtype of Box<Number>, even though Integer is a subtype of Number. If it were, you could put a Double into a box that's meant for Integers. Topic 8.5 shows how wildcards give you flexible, still-safe subtyping. Since Java 16 records can be generic too: record Pair<A, B>(A first, B second) {}.

Explain it without notes

01

How do you declare and use a generic class? Where can the type parameter appear inside it?

02

What are the two ways to implement a generic interface? Give an example of each.

03

Why can't a static field or static method use the class's type parameter?

04

Why is Box<Integer> not a subtype of Box<Number>? Compare with arrays.

Practice

01

Write a generic class Counter<T> that counts how many times each item is added (use a TreeMap<T, Integer> inside) and prints the counts for the strings b, a, b, c, b.

02

Write a generic interface interface Source<T> { T next(); } and a class CountingSource implements Source<Integer> that returns 1, 2, 3... Print the first three values.

03

Write a generic class Cache<K, V> with put, get and size, backed by a HashMap<K, V>. Store two Integer-to-String entries and print a hit, a miss (null) and the size.

Trade-offs

  • ↔

    A generic class is written once and reused safely for every type, but its API is harder to read than a concrete one. Make a class generic when it genuinely holds or processes values of a caller-chosen type (containers, caches, results), not just in case.

  • ↔

    Fixing a type argument in a subclass (implements Comparable<Money>) gives clear, concrete method signatures; passing it through keeps flexibility for callers. Library code usually stays generic; application code usually fixes types.

  • ↔

    Invariance keeps generic code safe but makes some natural-looking assignments illegal. Wildcards fix that at the cost of more complex signatures, so APIs often accept wildcards while internal fields use plain parameters.

Done when you can

  • Done when you can write a generic class with one and with two type parameters.

  • Done when you can implement a generic interface both by fixing the type and by passing it through.

  • Done when you can explain why static T is illegal.

  • Done when you can explain what javap -p -s shows about a generic class at run time.

  • Done when you can explain why Box<Integer> isn't a Box<Number>.

  • Done when you can write a generic record and a generic Iterable class.