🧪 Generics · Intermediate

Generic methods in Java

<T> T first(List<T> list) and type inference.

🧩 The mysteryList.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 = Integer
🔮 Predict it

Your 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);
}
  1. b2
  2. ab12
  3. Compile error
Show the answer

First call: T = String, returns "b". Second: T = Integer, returns 2. Then "b" + 2 is String concatenation: b2.

⚠️ The trap

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);
🔮 Predict it

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);
}
  1. [2, b]
  2. Compile error
  3. Throws 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<...>).

💼 In the real world

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

  1. Syntax: <T> goes before the return type
  2. T is inferred from arguments (and sometimes the target type)
  3. Explicit type witness: Collections.<String>emptyList()
  4. Works in non-generic classes too, e.g. Collections, Arrays, List.of
🤯 Did you know?

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);
}
  1. x7
  2. xy78
  3. Compile error
  4. 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;
}
  1. T
  2. <T>
  3. <?>
  4. 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.

Next: tired of typing Map<String, List<Integer>> twice on one line? Meet the diamond.