JAVA / GENERICS AND TYPE ERASURE
Type parameters and writing a generic class
Declare your own class with type parameters, use them for fields and method signatures, and instantiate it so the compiler checks each type argument for you.
What you will learn
- Declare class Box<T> and use T for fields, constructor parameters, and return types
- Read Box<String> as the compiler substituting String for T across the whole class
- Instantiate with a diamond, new Box<>(x), so the type argument is written once
- Explain why T is out of scope in static members and why new T() cannot compile
Understanding Type parameters and writing a generic class
A type parameter is a declaration-site name for a type the class does not yet know. Writing class Box<T> introduces T for the entire class body, so fields, constructor parameters, return types and local variables can be declared as T just like any other type name. At each use site, Box<String> or Box<Integer>, the compiler substitutes the type argument for T and rechecks the class against that substituted view. One source file therefore yields as many distinct compile-time types as there are type arguments callers write.
The payoff is that the type travels with the variable instead of living in your head. Given Box<String> b, the expression b.get() is already a String, so no cast is needed coming out and nothing but a String goes in, both decided before the program runs. The pre-generics version of the same class held an Object and forced every caller to cast, which turned a typo into a ClassCastException at some unrelated later moment. Writing new Box<>("delta") lets the diamond take the argument from the variable's declared type, so the argument is spelled out only once.
T is bound when a type is written, not when a class is loaded, and that one fact explains most of the rules around it. Instance fields, constructors and instance methods may use T because every object came from exactly one parameterized type; static fields and static methods may not, because they exist once for Box no matter what arguments callers wrote. For the same reason T is a name to the compiler rather than a class object, so new T() has no constructor to call and new T[n] has no element type to allocate; a generic class takes its T values in from outside.
class Box<T> {
private T value;
Box(T value) { // the constructor is named Box, never Box<T>
this.value = value;
}
public T get() {
return value;
}
public void replace(T next) {
this.value = next;
}
@Override
public String toString() {
return "Box(" + value + ")";
}
}
public class Main {
public static void main(String[] args) {
Box<String> word = new Box<>("delta");
Box<Integer> count = new Box<>(7);
String s = word.get(); // already a String, no cast
int n = count.get(); // Integer, unboxed on assignment
System.out.println(s.length() + " " + (n * 2));
word.replace("epsilon");
System.out.println(word + " " + count);
}
}A type parameter is a placeholder the class's own code uses as a type, and each use site binds it to one concrete type argument that the compiler enforces.
Worked examples
Two type parameters
A class can declare several parameters and reuse them in a different order in its own return types.
class Pair<A, B> {
private final A first;
private final B second;
Pair(A first, B second) {
this.first = first;
this.second = second;
}
public A first() { return first; }
public B second() { return second; }
public Pair<B, A> swapped() {
return new Pair<>(second, first);
}
@Override
public String toString() {
return "(" + first + ", " + second + ")";
}
}
public class Main {
public static void main(String[] args) {
Pair<String, Integer> entry = new Pair<>("ada", 1843);
Pair<Integer, String> flipped = entry.swapped();
System.out.println(entry);
System.out.println(flipped);
System.out.println(flipped.first() + 1);
System.out.println(entry.first().toUpperCase());
}
}Example explained
Line 1class Pair<A, B> declares two independent parameters whose names live only inside this class, exactly like method parameter names.
Line 2swapped() is typed Pair<B, A>, so calling it on a Pair<String, Integer> hands back a Pair<Integer, String> with no cast anywhere.
Line 3flipped.first() + 1 compiles as arithmetic because first() is Integer at that use site, while entry.first() is String and offers toUpperCase().
A static member cannot see T
Shows what static members of a generic class may hold and why a T-typed static field is rejected.
class Registry<T> {
private static int created = 0; // one counter shared by every Registry<...>
// private static T last; // rejected: T means nothing without an instance
private final T item;
Registry(T item) {
this.item = item;
created++;
}
public T item() {
return item;
}
public static int count() {
return created;
}
}
public class Main {
public static void main(String[] args) {
Registry<String> a = new Registry<>("cable");
Registry<Integer> b = new Registry<>(42);
Registry<String> c = new Registry<>("plug");
System.out.println(a.item() + " " + b.item() + " " + c.item());
System.out.println(Registry.count());
}
}Example explained
Line 1created is one variable for the whole class, so Registry<String> and Registry<Integer> increment the same counter.
Line 2The commented-out static T last would need a type before any object exists, but T is only fixed by writing new Registry<String>(...).
Line 3Registry.count() is callable with no type argument at all, which is legal precisely because its signature never mentions T.
Fixing or forwarding the parameter in a subclass
A subclass either supplies the type argument once or passes its own parameter up to the generic superclass.
class Cell<T> {
private T value;
Cell(T value) {
this.value = value;
}
public T get() {
return value;
}
public void set(T value) {
this.value = value;
}
}
class TextCell extends Cell<String> { // the argument is fixed here
TextCell(String value) {
super(value);
}
public int length() {
return get().length(); // get() is already String
}
}
class LoggedCell<T> extends Cell<T> { // the parameter is passed through
private int writes = 0;
LoggedCell(T value) {
super(value);
}
@Override
public void set(T value) {
writes++;
super.set(value);
}
public int writes() {
return writes;
}
}
public class Main {
public static void main(String[] args) {
TextCell title = new TextCell("hello");
System.out.println(title.length());
LoggedCell<Integer> cell = new LoggedCell<>(1);
cell.set(2);
cell.set(3);
System.out.println(cell.get() + " " + cell.writes());
}
}Example explained
Line 1extends Cell<String> supplies the argument once, so TextCell itself is not generic and its get() returns String to every caller.
Line 2get().length() inside TextCell needs no cast because the superclass was already parameterized when this body was compiled.
Line 3LoggedCell<T> extends Cell<T> forwards its own parameter upward, so new LoggedCell<>(1) binds both T's to Integer at the same time.
Important notes
Type arguments must be reference types, so Box<int> is a compile error; write Box<Integer> and let autoboxing bridge, as in int n = count.get().
Inside the class you cannot write new T() or new T[n]; back a generic container with an ArrayList<T> or an Object[] field and take T values in through constructors and methods.
Common mistakes
Writing the constructor as Box<T>(T value): the compiler reads it as a method named Box with its own parameter and no return type, and reports "invalid method declaration; return type required".
Making a T-typed field static to share it, as in static T last: this fails with "non-static type variable T cannot be referenced from a static context", because one shared field cannot be String for Box<String> and Integer for Box<Integer>.
Dropping the argument and using the raw type, Box b = new Box("x"): b.get() is typed Object again, so String s = b.get() will not compile and the compiler no longer checks what you put in.
Try it yourself
Change, predict, then run
Write class Slot<T> with one private T field, boolean isEmpty(), void put(T value) that throws IllegalStateException when the slot is already occupied, and T take() that returns the value and clears the field. Use it as Slot<Double>, print a taken value, then add slot.put("x") and read the compile error before removing it.
Open the Java workspaceCheck your understanding
You add private static T lastCreated; to class Box<T> and it will not compile. What is the actual reason?
- Static fields cannot be private in a generic class.
- Box<String> and Box<Integer> each get their own copy of the field, and the compiler cannot decide which one to initialize first.
- Static members belong to Box itself, which exists once no matter which type arguments callers write, so T has nothing to bind to.
- T must be given a bound before any field can be declared with it.
Show answer
T is bound by the type a caller writes, so Box<String> binds it to String; a static member is reached through Box alone, where no argument was ever supplied, leaving no T for the field to have. Option 2 in the list is the inverse of the truth: there is exactly one copy of a static field shared by every parameterization, which is precisely why it cannot be typed T.