skip to content
everything.generics.genericTypesAndMethods

generic types and methods

writing your own Box<T>, and how inference figures out T.

chapter
chapter 05.02
reading time
8 min read

a type parameter (<T>) is a placeholder type you declare once and then use like any other type. callers fill it in with a type argument: Box<String>. classes can declare them (class Box<T>) and so can single methods (static <T> List<T> repeat(T x, int n)).

most of the time you never write the type argument at all. inference works it out from the arguments and from where the result is going.

the picture

you know how a method has parameters, so one method body works for any value you pass in? a type parameter is the same trick, one level up: one class body that works for any type you pass in.

think of a printed form with a blank: "this box holds ______". the form is the class. filling the blank (String) gives you a specific kind of box, and from then on the clerk (javac) checks everything you put in against what's written there. the form itself never changes, however many ways it gets filled in.

Box.java changed
  1. 1class Box {
  2. 2 private Object value;
  3. 3
  4. 4 Box(Object value) { this.value = value; }
  5. 5
  6. 6 Object get() { return value; }
  7. 7}
  8. 8
  9. 9Box b = new Box("ada");
  10. 10String s = (String) b.get(); // cast, every time

the pre-generics way: store an Object, hand back an Object, make every caller cast. it works for any type, and it checks none of them.

step 1/6
step through it, or click a tab. a box that holds Object turns into a Box<T>, one edit at a time. amber is what changed in each step.

the vocabulary, since people mix it up: in class Box<T>, T is the type parameter. in Box<String>, String is the type argument, and Box<String> is a parameterized type. Box by itself, with nothing in the brackets, is the raw type (why that's bad).

a whole generic class

here's a real one, with a generic method inside it. map declares its own type parameter R, separate from the class's T, because the result can be a different type than the contents:

Main.java
class Box<T> {
    private final T value;
 
    Box(T value) { this.value = value; }
 
    T get() { return value; }
 
    <R> Box<R> map(Function<T, R> f) { return new Box<>(f.apply(value)); }
 
    public String toString() { return "Box[" + value + "]"; }
}
 
void main() {
    Box<String> name = new Box<>("ada");
    Box<Integer> len = name.map(String::length);   // R = Integer
    int n = len.get();                             // no cast
    IO.println(name + " " + len + " " + n);
}
output
Box[ada] Box[3] 3

T is fixed per object: name is a Box<String> for its whole life. R is fixed per call: every call to map can pick a different one. (the real JDK version of this, Optional.map, takes a Function<? super T, ? extends R>. those question marks are wildcards, and they make it accept more functions.)

the usual names are single capital letters, so you can tell a type parameter from a class at a glance:

T, U, S
a type. the first, second, third one.
E
an element of a collection: List<E>, Set<E>.
K, V
key and value: Map<K, V>.
R
a result: Function<T, R>.
X
an exception type, like the X in Optional.orElseThrow.

generic methods

a method can have type parameters of its own, even in a class that has none. the <T> goes right before the return type, and it's in scope for the parameters, the return type and the body:

Main.java
static <T> List<T> repeat(T x, int times) {
    List<T> out = new ArrayList<>();
    for (int i = 0; i < times; i++) out.add(x);
    return out;
}
 
void main() {
    List<String> hey = repeat("hey", 3);                 // T = String
    List<Character> dots = repeat('.', 2);               // T = Character (boxed)
    List<Number> zeros = repeat(0, 2);                   // T = Number, from the target
    var empty = Collections.<String>emptyList();         // T = String, said out loud
    IO.println(hey + " " + dots + " " + zeros + " " + empty.size());
}
output
[hey, hey, hey] [., .] [0, 0] 0

nobody wrote a type argument on the first three calls. javac worked out T each time. and look at zeros: the argument is an Integer, but T came out as Number, because the result was going into a List<Number>. inference doesn't just look at the arguments.

how inference picks T

javac treats T like an unknown in an equation. every argument adds a clue ("T must be able to hold an Integer"), the target adds another ("the result must be a List<Number>"), and then it solves for the one type that satisfies all of them. try it on List.of, whose element type is called E:

List.of
var b = List.of(1, 2.0);
from the arguments: E must hold all of
IntegerDouble
from the target: E must be exactly
var: no target
E =Number & Comparable<...> & Constable & ConstantDesc

an Integer and a Double. E has to be a supertype of both, so javac computes their least upper bound: everything they have in common. that's not just Number, it's Number and three interfaces both happen to implement.

pick a call. clues from the arguments say what E must be able to hold, clues from the target say what it must be exactly. the answer is green, or javac's real error in red. every result was checked against javac 25.

the rules, roughly: if the target pins E down exactly, that's the answer (as long as the arguments fit). otherwise E is the most specific type that's a supertype of every argument (the least upper bound), which can be an intersection like Number & Comparable<...> that you could never type yourself. and with no clues at all, E is just its bound, Object.

predict the output
java
var list = new ArrayList<>();
list.add("ada");
list.add(42);
IO.println(list);
one level deeper:what target typing used to miss+

inference that looks at the target is older than you'd think (List<String> s = Collections.emptyList() worked in java 5), but it only looked at assignments. java 8 (JEP 101, generalized target-type inference) made it work through method arguments and chains, which lambdas needed: a lambda has no type of its own, so list.sort((a, b) -> ...) only compiles because javac can push the expected type into the argument. javac's exact algorithm is chapter 18 of the language spec: collect bounds, reduce them, resolve each variable, lower bounds first.

the diamond, and where it gets smart

<> (java 7) is inference for constructors. new HashMap<>() asks javac to fill the type arguments from the target, exactly like a generic method call. since java 9 it also works on anonymous classes, as long as the inferred type is something you could have written:

java
Comparator<String> byLength = new Comparator<>() {     // java 9+
    public int compare(String a, String b) { return a.length() - b.length(); }
};
quick check
in class Cache<K, V>, a method is declared <V> V load(K key). what is that method's V?
keylines
  • class Box<T>: T is a type parameter. Box<String>: String is the type argument.
  • a class's type parameters are fixed per object. a method's are picked per call.
  • inference solves for T from the arguments and the target. you rarely write it.
  • with no clues, T is Object. var + <> is how you get there by accident.
  • static members can't use the class's T. give static methods their own.