Generic Methods

Write methods with their own type parameters, let the compiler infer them, and use generic static factories for typed results.

Syntax<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 witness

A 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 has T. The method's parameter hides the class's one, and the two are unrelated.
  • Returning Object and making callers cast, when a type parameter would carry the type through.
  • Adding a type parameter that appears only once in the signature. If T is used once, it adds nothing; use a wildcard or a concrete type.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · java
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.

Example · java
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
// 0

Mapping 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.

Example · java
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

  • Be the first to comment on this lesson.