Generic methods in Java
<T> T first(List<T> list) and type inference.
List.of("a") returns a List<String>, and List.of(1) returns a List<Integer>. Same method, different types, no casts. How does one method know?Methods can be generic too
A method can declare its own type parameters, just before the return type. The <T> declares it; the T after it uses it as the return type. This works in non-generic classes too: Collections, Arrays and List.of are full of generic methods.
static <T> T first(List<T> list) {
return list.get(0);
}Type inference
You rarely name T yourself. The compiler infers T from the arguments (and sometimes from the target type). Each call infers its own T, independently.
String s = first(List.of("a", "b"));
// T = String
Integer n = first(List.of(7, 8));
// T = IntegerYour turn
What does this print?
static <T> T last(List<T> list) {
return list.get(list.size() - 1);
}
void main() {
String s = last(List.of("a", "b"));
Integer n = last(List.of(1, 2));
System.out.println(s + n);
}b2ab12Compile error
Show the answer
First call: T = String, returns "b". Second: T = Integer, returns 2. Then "b" + 2 is String concatenation: b2.
Forgetting to declare T
Leave out the <T> and the compiler thinks T is a class name, then fails with "cannot find symbol". The declaration <T> is what makes it a type parameter. And <?> can't declare anything: wildcards are for uses, not declarations.
static T first(List<T> l) // ✗ what's T?
static <T> T first(List<T> l) // ✓The type witness
When inference has nothing to go on, you can state T explicitly with a type witness: put <Type> between the dot and the method name. You'll seldom need it.
var e = Collections.<String>emptyList();
var nums = List.<Number>of(1, 2.5);Mixed arguments
T a and T b get an Integer and a String. What happens?
static <T> List<T> pair(T a, T b) {
return List.of(a, b);
}
void main() {
var p = pair(2, "b");
System.out.println(p);
}[2, b]Compile errorThrows ClassCastException
Show the answer
It compiles! T doesn't have to be one exact class: the compiler infers a common supertype both arguments satisfy (here something like Object & Serializable & Comparable<...>).
All over the JDK
List.of, Collections.max, Optional.of, Arrays.asList, Map.entry: all generic methods. Writing your own is how you build reusable helpers, like a firstOrDefault or a groupBy, that stay type-safe for every caller.
Key takeaways
- Syntax: <T> goes before the return type
- T is inferred from arguments (and sometimes the target type)
- Explicit type witness: Collections.<String>emptyList()
- Works in non-generic classes too, e.g. Collections, Arrays, List.of
Collections.emptyList() returns the very same object every time, whatever T you ask for. Thanks to erasure, one shared empty list can safely pose as a List<String>, a List<Integer>, and everything else.
Practice questions
What does this print?
static <T> T first(List<T> list) {
return list.get(0);
}
void main() {
String s = first(List.of("x", "y"));
Integer n = first(List.of(7, 8));
System.out.println(s + n);
}- x7
- xy78
- Compile error
- x 7
Check your answer
x7. Each call infers T independently: String for the first, Integer for the second. Then "x" + 7 is String concatenation: x7.
Pick what makes this a valid generic method.
static ___ void swap(T[] a, int i, int j) {
T tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}- T
- <T>
- <?>
- Object
Check your answer
<T>. <T> declares the type parameter for this method. Writing just T would refer to an undeclared type, and <?> isn't allowed as a declaration.