Java Generic Methods
Define methods with their own type parameters in Java, independent of the enclosing class.
A generic method is a method that introduces its own type parameter in its signature, independent of any class-level parameter. This is the right tool when the type relationship belongs to one method — a utility that swaps two array elements, a factory that returns a list of whatever the caller passes in, a static helper that doesn't have an instance to attach a T to. Generic methods are how almost every static utility in java.util.Collections and java.util.Arrays is written.
Where the type parameter goes
The type parameter is declared before the return type, between the modifiers and the return:
public static <T> T identity(T value) {
return value;
}Read left to right: "public, static, declares a type parameter T, returns a T, named identity, takes a T." The <T> is what makes this a generic method rather than a method that happens to use a class-level T.
Call it like a normal method — the compiler infers the type argument from the arguments you pass:
String s = identity("hello"); // T inferred as String
Integer n = identity(42); // T inferred as IntegerIf inference fails or you want to override it, you can supply the type argument explicitly with the type witness syntax, after the dot:
String s = MyUtil.<String>identity("hello"); // rarely neededIn ten years of Java, you'll write that explicit form maybe a dozen times.
Why a method-level parameter instead of a class-level one
A class-level parameter says "this whole class is about one type." A method-level parameter says "this one operation is polymorphic in a type that doesn't need to outlive the call." They aren't substitutes — they answer different questions:
// Method-level: the class isn't generic; the method is.
public class Arrays {
public static <T> void swap(T[] arr, int i, int j) { ... }
}
// Class-level: the class is parameterised; methods share that T.
public class Box<T> {
public T get() { ... }
public void set(T value) { ... }
}Use a method-level parameter when:
- The method is
static(it has no instance, so no class-levelTto borrow). - The type relationship is local to the method — input and output share a type, but the class doesn't.
- You want different calls of the same method to use different types:
swapon aString[]andswapon anInteger[]should both work, and the class shouldn't have to commit to one.
Multiple type parameters in one method
The same rule applies: declare them between the modifiers and the return type, separated by commas:
public static <K, V> Map.Entry<K, V> entry(K key, V value) {
return new AbstractMap.SimpleImmutableEntry<>(key, value);
}
Map.Entry<String, Integer> e = entry("Ada", 100);Both K and V are inferred from the arguments. If the two parameters happen to share a type, the inferred type is whatever both arguments agree on:
public static <T> T firstOf(T a, T b) { return a; }
firstOf("x", "y"); // T = String
firstOf("x", 42); // T = Object — the closest common supertypeThat last one is sometimes a footgun. The compiler doesn't reject it; it just quietly widens T to Object. If you wanted "two arguments of the same exact type," generics can't enforce it past widening — you'd need to make the arguments separate type parameters.
A method-level parameter on a generic class
A generic class can have generic methods that introduce their own parameters, distinct from the class's. The two parameters coexist:
public class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
// U is local to this method — independent of T.
public <U> Box<U> map(java.util.function.Function<T, U> fn) {
return new Box<>(fn.apply(value));
}
}
Box<String> name = new Box<>("Ada");
Box<Integer> length = name.map(String::length); // T=String, U=Integermap's <U> is in scope only inside map. It can use T (because it's inside a Box<T>) but it can't replace it.
Type inference in practice
The compiler infers a method's type parameters from:
- The types of the explicit arguments.
- The target type — what you're assigning the result to, or the parameter type of a method you're passing the result into.
The second source is why List.of(), Collections.emptyList(), and similar return-only generics work without an explicit type witness most of the time:
List<String> empty = Collections.emptyList(); // T inferred from the left side
process(Collections.emptyList()); // T inferred from `process`'s parameterWhen neither source is available (no arguments, no target type), the compiler falls back to Object. That's almost never what you want — write the type witness or add a target type:
var x = Collections.emptyList(); // List<Object> — probably not what you meant
List<String> y = Collections.emptyList(); // List<String> ✓A real-life shape: utility methods on collections
The standard library's Collections.unmodifiableList, Collections.sort, Collections.shuffle, and friends are all generic methods on a non-generic utility class. Take sort, in spirit:
public static <T extends Comparable<T>> void sort(List<T> list) {
// ... sorts using natural order
}That signature is doing two things at once. The <T> declares a type parameter. The extends Comparable<T> is a bound — T has to be a type that knows how to compare against itself. We'll spend an entire chapter on bounded parameters; for now, just notice that the bound is what lets the method call compareTo on its elements.
A worked example: a typed swap, a typed last, a typed map
A small utility class with three generic methods — one void, one returning the same type as it took, one mapping element-wise to a new type. Together they cover the three shapes you'll write most often.
Three things to notice. swap works on both String[] and Integer[] because T is inferred per call. last returns the element type the caller passed in — no cast on the receiving side. map introduces two type parameters and ties them together through the Function<T, R> parameter — the compiler enforces that the function takes the list's element type and returns the result list's element type.
What's next
You've seen the two ways of declaring a type parameter — on a class and on a method. The next step is the third place a type parameter can live: on an interface. That's how the standard library defines List<E>, Comparator<T>, Function<T, R>, and every other contract you implement when you write polymorphic code. Continue to Java Generic Interfaces.