Generic Methods
Write methods with their own type parameters, let the compiler infer them, and use generic static factories for typed results.
<T> T first(List<T> items)
<K, V> Map<V, K> invert(Map<K, V> map)
static <T> Result<T> ok(T value)
Collections.<String>emptyList() // explicit type witnessA generic method declares its own type parameters, written before the return type. The parameter only exists for that one call, so the method can sit in a class that is not generic at all. List.of, Collections.emptyList, Optional.of and Objects.requireNonNull are generic methods.
<T> T firstOrDefault(List<T> items, T fallback) {
return items.isEmpty() ? fallback : items.get(0);
}
static <T> ApiResponse<T> ok(T data) { ... }Inference does the work
You almost never write the type argument at the call site. The compiler infers T from the arguments, and when the arguments are not enough it looks at the target: in List<Long> ids = List.of(); the empty list becomes a List<Long> because of the variable it is assigned to. When there is no target, for example with var or at the start of a method chain, inference can settle on Object. Comparator.comparing(u -> u.email()).reversed() does not compile, because u is inferred as Object before reversed() is applied. Then you either give a target type, type the lambda parameter ((User u) -> u.email()) or write an explicit type witness: Collections.<String>emptyList().
The same applies to the diamond: var list = new ArrayList<>(); is an ArrayList<Object>, which accepts anything. With var, put the type argument on the right.
Static factories
A static method cannot use its class's type parameter, so generic classes declare a fresh one on their factories: static <T> ApiResponse<T> ok(T data). The <T> on the method is a new parameter that happens to share the name. Factories read better than constructors (ApiResponse.ok(user), Result.failure("timeout")) and infer the type from the argument.
When a method should be generic
Make a method generic when the output type depends on the input type. A mapper that turns a list of entities into a list of DTOs, a helper that returns the first element, a lookup that returns what it was given: all need T so the caller gets the real type back. If the method only reads values and returns something fixed, such as a count or a string, a wildcard parameter (List<?>) is often simpler than a type parameter.
Common mistakes
- Declaring
<T>on a method inside a generic class that already hasT. The method's parameter hides the class's one, and the two are unrelated. - Returning
Objectand making callers cast, when a type parameter would carry the type through. - Adding a type parameter that appears only once in the signature. If
Tis used once, it adds nothing; use a wildcard or a concrete type.
Example
When to use it
- Give a Result<T> or ApiResponse<T> type static factories such as ok(value) and error(message) for handler code.
- Write one mapAll helper that converts any list of database entities into response DTOs.
- Add a typed firstOrDefault helper for configuration lists such as allowed origins or fallback regions.
More examples
Static factories on a generic record
The static methods declare their own T, inferred from the argument for ok and from the assignment target for error. Callers never write a type argument.
import java.util.List;
record User(long id, String email) {}
record ApiResponse<T>(T data, List<String> errors) {
static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(data, List.of());
}
static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(null, List.of(message));
}
boolean success() { return errors.isEmpty(); }
}
void main() {
ApiResponse<User> found = ApiResponse.ok(new User(7, "[email protected]"));
ApiResponse<User> missing = ApiResponse.error("user 9 not found");
IO.println(found);
IO.println(missing.success() + " " + missing.errors());
}
// Output:
// ApiResponse[data=User[id=7, [email protected]], errors=[]]
// false [user 9 not found]When inference needs a target
With nothing to infer from, the diamond falls back to Object and the list takes any value. A type on the right, a typed variable on the left, or an explicit witness gives the compiler the information it needs.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
void main() {
var loose = new ArrayList<>(); // ArrayList<Object>
loose.add("a");
loose.add(1); // accepted, probably a bug
IO.println(loose);
var strict = new ArrayList<String>();
strict.add("a");
// strict.add(1); // does not compile
IO.println(strict);
List<Long> ids = List.of(); // target type picks Long
IO.println(ids.isEmpty());
IO.println(Collections.<String>emptyList().size());
}
// Output:
// [a, 1]
// [a]
// true
// 0Mapping entities to DTOs
T and R link the input list, the mapper and the output list, so the caller gets List<UserDto> or List<String> back without casts. Dropping passwordHash in the DTO keeps it out of the JSON response.
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
record UserEntity(long id, String email, String passwordHash) {}
record UserDto(long id, String email) {}
<T, R> List<R> mapAll(List<T> items, Function<T, R> mapper) {
List<R> out = new ArrayList<>(items.size());
for (T item : items) {
out.add(mapper.apply(item));
}
return out;
}
void main() {
List<UserEntity> rows = List.of(
new UserEntity(1, "[email protected]", "hash-1"),
new UserEntity(2, "[email protected]", "hash-2"));
List<UserDto> dtos = mapAll(rows, u -> new UserDto(u.id(), u.email()));
List<String> emails = mapAll(rows, UserEntity::email);
IO.println(dtos);
IO.println(emails);
}
// Output:
// [UserDto[id=1, [email protected]], UserDto[id=2, [email protected]]]
// [[email protected], [email protected]]
Discussion