Generic Methods
A generic method declares its own type parameters, independent of any class. It lets a single method work with many types while keeping full type safety: the compiler works out the actual types from the arguments at each call and checks that everything fits. Generic methods can live in ordinary (non-generic) classes, and they are the backbone of utility APIs such as Collections.max, List.of, Arrays.asList, and Optional.of.
Syntax
The type parameter list goes before the return type:
public static <T> T firstOrDefault(List<T> list, T fallback) {
return list.isEmpty() ? fallback : list.get(0);
}
public static <T> T firstOrDefault(List<T> list, T fallback)
└─ type └─ return └─ parameters can use T
params type
<T> declares the type parameter; after that, T can be used in the return type, the parameter types, and the body.
String s = firstOrDefault(List.of("x", "y"), "none"); // T = String
Integer n = firstOrDefault(List.<Integer>of(), 0); // T = Integer
LocalDate d = firstOrDefault(dates, LocalDate.now()); // T = LocalDate
Type Inference
At each call, the compiler infers the type arguments from the method's arguments and, when needed, from the context the result is assigned to. You rarely write them explicitly:
List<String> names = Arrays.asList("Ada", "Linus"); // T inferred as String
List<Integer> empty = Collections.emptyList(); // T inferred from the target type
In the second line there are no arguments at all; the compiler uses the declared type of empty to choose T = Integer.
Explicit type arguments
When inference cannot determine the type, or you want to be explicit, put the type arguments before the method name, after the dot:
List<String> list = Collections.<String>emptyList();
var pairs = Map.<String, Integer>of();
This is called a type witness. It requires a qualifier: Collections.<String>emptyList() or this.<String>method(), not just <String>method().
Generic Methods Versus Generic Classes
| Generic method | Generic class | |
|---|---|---|
| Declares type parameters | On the method: <T> void m(T x) | On the class: class Box<T> |
| Scope of the parameter | That method only | All instance members of the class |
| Type chosen | Per call, usually inferred | Per object, when it is created |
| Typical use | Utilities and algorithms | Containers and data holders |
A generic class's instance methods can use the class's parameter without declaring it. A method can also declare additional parameters of its own:
public class Box<T> {
private final T value;
public Box(T value) { this.value = value; }
public T get() { return value; } // uses the class's T
public <R> Box<R> map(Function<T, R> f) { // declares its own R
return new Box<>(f.apply(value));
}
}
Static methods need their own type parameters
A generic class's type parameter belongs to instances, so static methods cannot use it. They declare their own instead:
public class Box<T> {
public static <T> Box<T> of(T value) { // this T is the method's, not the class's
return new Box<>(value);
}
}
Box<String> b = Box.of("hi");
This pattern, a static generic factory, is common: List.of, Optional.of, Map.entry, Comparator.comparing.
Multiple Type Parameters
public static <A, B> List<Map.Entry<A, B>> zip(List<A> left, List<B> right) {
List<Map.Entry<A, B>> out = new ArrayList<>();
for (int i = 0; i < Math.min(left.size(), right.size()); i++) {
out.add(Map.entry(left.get(i), right.get(i)));
}
return out;
}
System.out.println(zip(List.of("Q1", "Q2", "Q3"), List.of(42_000, 51_500)));
// [Q1=42000, Q2=51500]
Each parameter is inferred independently: here A = String and B = Integer.
Bounded Type Parameters
To call methods on T, bound it to a type that has them:
public static <T extends Comparable<T>> T max(List<T> items) {
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) best = item;
}
return best;
}
The bound T extends Comparable<T> makes compareTo available and rejects, at compile time, any call with a type that cannot be compared.
A slightly more flexible bound is T extends Comparable<? super T>. It also accepts types whose compareTo is inherited from a superclass, such as subclasses of a class that implements Comparable for itself. The JDK's own Collections.max uses this form. Wildcards like ? super T are explained in the next lesson.
static <T extends Comparable<? super T>> T clamp(T value, T min, T max) {
if (value.compareTo(min) < 0) return min;
if (value.compareTo(max) > 0) return max;
return value;
}
clamp(120, 0, 100); // 100
clamp("m", "a", "k"); // "k"
clamp(3.5, 1.0, 5.0); // 3.5
One method handles integers, strings, and doubles, and any other Comparable type, without overloads.
Generic Methods with Functional Parameters
Generic methods become especially powerful when combined with functional interfaces, because the caller supplies the behavior and the method supplies the structure:
public static <T, R> List<R> mapAll(List<T> items, Function<T, R> f) {
List<R> out = new ArrayList<>(items.size());
for (T item : items) out.add(f.apply(item));
return out;
}
List<Integer> lengths = mapAll(List.of("java", "c", "bash"), String::length); // [4, 1, 4]
T is inferred from the list, and R from what the function returns. This is the same idea that the Stream API's map, filter, and collect are built on.
Generic Methods and Arrays
Generic varargs methods are convenient but produce a compiler warning about heap pollution, because varargs creates an array and arrays of generic types are not fully type-checked at runtime:
@SafeVarargs
public static <T> List<T> listOf(T... items) {
return new ArrayList<>(Arrays.asList(items));
}
@SafeVarargs tells the compiler the method only reads from the array and does not store anything unsafe in it. It may be applied only to methods that cannot be overridden (static, final, or private). List.of and Arrays.asList are annotated this way.
When to Write a Generic Method
Write a method generically when its logic does not depend on the specific type of the data it handles: searching, grouping, pairing, transforming, copying, choosing a maximum, wrapping results. If you find yourself writing the same method twice for List<String> and List<Integer>, or writing a method that takes Object and requires casts by the caller, a generic method is usually the answer.
Practical Example: A Small Utility Library
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
public class Utils {
static <T, K> Map<K, List<T>> groupBy(Collection<T> items, Function<T, K> key) {
Map<K, List<T>> groups = new TreeMap<>();
for (T item : items) {
groups.computeIfAbsent(key.apply(item), k -> new ArrayList<>()).add(item);
}
return groups;
}
static <T> Optional<T> firstMatch(List<T> items, Predicate<T> test) {
for (T item : items) {
if (test.test(item)) return Optional.of(item);
}
return Optional.empty();
}
static <A, B> List<Map.Entry<A, B>> zip(List<A> left, List<B> right) {
List<Map.Entry<A, B>> out = new ArrayList<>();
for (int i = 0; i < Math.min(left.size(), right.size()); i++) {
out.add(Map.entry(left.get(i), right.get(i)));
}
return out;
}
static <T extends Comparable<? super T>> T clamp(T value, T min, T max) {
if (value.compareTo(min) < 0) return min;
if (value.compareTo(max) > 0) return max;
return value;
}
record Student(String name, String city, int marks) { }
public static void main(String[] args) {
List<Student> students = List.of(
new Student("Ada", "Chennai", 91), new Student("Linus", "Pune", 78),
new Student("Grace", "Chennai", 85), new Student("Alan", "Delhi", 64));
Map<String, List<Student>> byCity = groupBy(students, Student::city);
byCity.forEach((city, list) -> System.out.println(city + ": " + list.size()));
Map<Boolean, List<Student>> byPass = groupBy(students, s -> s.marks() >= 75);
System.out.println("Passed: " + byPass.get(true).stream().map(Student::name).toList());
System.out.println(firstMatch(students, s -> s.marks() < 70).map(Student::name).orElse("none"));
System.out.println(zip(List.of("Q1", "Q2", "Q3"), List.of(42_000, 51_500)));
System.out.println(clamp(120, 0, 100) + " " + clamp("m", "a", "k") + " " + clamp(3.5, 1.0, 5.0));
}
}
Output:
Chennai: 2
Delhi: 1
Pune: 1
Passed: [Ada, Linus, Grace]
Alan
[Q1=42000, Q2=51500]
100 k 3.5
The class itself is not generic; each static method declares the type parameters it needs. groupBy has two: T for the items and K for the key. Calling it with Student::city infers K = String; calling it with a lambda that returns a boolean infers K = Boolean, so the same method produces both a map by city and a pass/fail split. firstMatch returns Optional<T>, so the caller gets an Optional<Student> and can map it to a name. zip infers two unrelated types, and clamp's bound lets it compare integers, strings, and doubles with one implementation. No casts appear anywhere, and every call is fully type-checked.