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>orList<E>. - Parameterized type
- A generic type with real type arguments filled in, like
Box<String>orMap<String, Integer>. - Type variable
- Another name for a type parameter as it's used inside the class body, like the
TinT get(). - Invariant
- Having no subtype relationship between different type arguments:
Box<Integer>andBox<Number>are unrelated types, even thoughIntegerextendsNumber. - Signature attribute
- Extra information in a
.classfile that remembers generic declarations likeBox<T>for the compiler and reflection. The JVM doesn't use it to run code. - Iterable
- The generic interface
Iterable<T>with aniterator()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.
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.
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.
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).
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>.
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 generic06Static 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.
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.
Try it yourself
- 1
Make the stack a stack of records
In the stack example, add
record Order(String id, int qty) {}insideMain(needs Java 16+), create aStack<Order>, push two orders and pop one. Predict whattoStringprints, then run. - 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 declaringConverter<Integer, String> bad = new Shout();. Predict the compiler message. - 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 removeimplements Iterable<T>and read the compiler error on the for-each line.
Code & diagrams
Expected output
Stack[home, search, product]
back from: product
now on: search
sum: 60
error: stack is emptyExpected output
6
price: 40
temp: 36.6
true trueExpected 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: trueThe 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.
Expected output
size: 3
guava has 5 letters
banana has 6 letters
mango has 5 lettersBreak 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;.
Break #2
Assume Box<Integer> is a Box<Number>
Write Box<Integer> ints = new Box<>(); Box<Number> nums = ints;.
Break #3
Pass a constructor argument that disagrees with the declared type
Write Box<String> b = new Box<>(42);.
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
Signatureattribute on classes, fields and methods. Reflection reads it:Box.class.getTypeParameters()returns[T], and a field declaredList<String> namesreportsjava.util.List<java.lang.String>fromgetGenericType(). 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 overridesset(String)gets a compiler-generated bridge methodset(Object)that casts and forwards. You'll see these injavapoutput 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 innerclass Itrcan useTdirectly, and its full type isLinkedBag<T>.Itr. Static nested classes are independent, which is whyHashMap.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 isObject, which is exactly what erasure replaces it with.
Remember this
- 1
You declare type parameters in angle brackets right after the class name:
class Box<T>,class Pair<K, V>. Inside the class,Tcan 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
Each parameterized type like
Box<String>is a separate compile-time type. The compiler substitutesStringforTwhen checking code that uses it, sobox.set(42)fails andbox.get()returnsString. But there is still only one class file,Box.class, and oneBoxclass at run time. The compiler records the generic signature as metadata (theSignatureattribute), and the real field and method types in the bytecode areObject. - 3
By convention type parameters are single capital letters so they never look like real class names:
T(type),E(element),KandV(key and value),N(number),R(result), andS,Ufor second and third types.Map<K, V>andFunction<T, R>in the JDK follow this. - 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>orclass LabeledBox<T> extends Box<T>. - 5
A type parameter belongs to an instance: it's chosen when an object's type is written, so
staticmembers 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 byBox<String>andBox<Integer>alike. Static methods that need a type must declare their own (Topic 8.3). - 6
Parameterized types are invariant:
Box<Integer>is not a subtype ofBox<Number>, even thoughIntegeris a subtype ofNumber. If it were, you could put aDoubleinto a box that's meant forIntegers. 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
How do you declare and use a generic class? Where can the type parameter appear inside it?
What are the two ways to implement a generic interface? Give an example of each.
Why can't a static field or static method use the class's type parameter?
Why is Box<Integer> not a subtype of Box<Number>? Compare with arrays.
Practice
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.
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.
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 Tis illegal.Done when you can explain what
javap -p -sshows about a generic class at run time.Done when you can explain why
Box<Integer>isn't aBox<Number>.Done when you can write a generic record and a generic
Iterableclass.