skip to content

When should you introduce a type parameter on a method (a generic method) rather than on the whole class, and how does inference make it ergonomic?

level: seniorimportance: should knowfreq 40%

answer

  1. Class param = type is part of object state; method param = type only for the call
  2. Static methods must use method-level params (can't see class params)
  3. Use when return type depends on an arg, or two args share a type
  4. Type-param list goes before the return type
  5. Inference hides <T>; type witness is the rare escape hatch

basics

~20 s

Use a generic method when only one method needs the placeholder type and the class itself doesn't store it. Declare the type parameter before the return type, and callers usually don't have to pass it because the compiler infers it from the arguments.

solid answer

~50 s

Put a type parameter on the *class* when the type is part of the object's state — e.g. a `Box<T>` that holds a T. Put it on the *method* (a generic method) when the type only matters for that single call and isn't stored, e.g. a utility like `static <T> List<T> singletonList(T item)` or `static <T> T firstOrNull(List<T> xs)`. Generic methods keep the class non-generic and let each call work with a different type. The big ergonomics win is **inference**: callers write `Collections.emptyList()` or `firstOrNull(names)` and the compiler infers T from the target type or arguments, so the `<T>` is invisible at the call site. You can force it with a type witness (`Collections.<String>emptyList()`) when inference can't decide, but that's rare. Static factory/utility methods, methods relating two argument types, and methods whose return type depends on an argument are the canonical cases for method-level type parameters.

code

java · 15 lines
java
// Class-level: T is stored, part of the object
class Box<T> { private T value; T get() { return value; } }

// Method-level generic: T exists only for this call, class stays non-generic
class Lists {
    static <T> T firstOrNull(List<T> xs) {
        return xs.isEmpty() ? null : xs.get(0);
    }
}

List<String> names = List.of("a", "b");
String first = Lists.firstOrNull(names);   // T inferred as String, no <> needed at call

// Type witness only when inference can't decide:
List<String> empty = java.util.Collections.<String>emptyList();

go deeper

for a junior

Can call a generic method like Collections.emptyList() without thinking about the <T>.

for a middle

Can write a simple generic static method and knows the type-param list goes before the return type.

for a senior

Articulates the class-vs-method decision rule, knows static methods need their own params, and explains inference and the type witness.

for a principal

Designs APIs that minimize ceremony via inference, avoids needless type parameters, and reserves wildcards vs type parameters appropriately for variance.

## Two places a type parameter can live A placeholder type can be declared on a **class/interface** or on a **method**: ```java class Box<T> { // class-level: T is part of the object's state private T value; T get() { return value; } } static <T> T firstOrNull(List<T> xs) { // method-level: T only for this call return xs.isEmpty() ? null : xs.get(0); } ``` ## The decision rule **Class-level** type parameter: when the type is part of the instance's identity/state — the object *holds* or *is about* a T across many method calls. `List<E>`, `Map<K,V>`, `Optional<T>` all need the type in their state, so it belongs on the type. **Method-level** type parameter (generic method): when the type matters only for the duration of one call and is **not stored** on the object. Signs you want a method-level parameter: - The method is **static** (a static method can't see class type parameters anyway, so any genericity it needs must be its own). - The method **relates two of its arguments' types** (e.g. `<T> void copy(List<T> dst, List<? extends T> src)`). - The method's **return type depends on an argument's type** (e.g. `<T> T cast(Object o, Class<T> type)`). - It's a **utility/factory** producing a value of a caller-chosen type (`Collections.emptyList()`, `Arrays.asList`, `Optional.of`). Keeping such methods generic at the method level avoids polluting the whole class with a type parameter it doesn't need, and lets a single non-generic (or differently-generic) class serve many element types. ## Declaration syntax The method's type-parameter list goes **before the return type**: ```java static <T> List<T> repeat(T value, int n) { ... } ``` For an instance method on a generic class you can even add *extra* method-level parameters beyond the class ones: ```java class Box<T> { <U> Box<U> map(Function<? super T, ? extends U> f) { ... } // U is method-level } ``` ## Why inference makes it ergonomic The reason generic methods are pleasant to use is **type-argument inference**. The caller almost never writes the `<T>`; the compiler deduces it from: - the **argument types**: `firstOrNull(names)` where `names` is `List<String>` infers T=String; - the **target type**: `List<String> xs = Collections.emptyList()` infers T=String from the assignment. So the genericity is free at the call site. When inference genuinely can't decide (e.g. `Collections.emptyList()` passed straight into a method overload with no clear target), you supply an explicit **type witness**: ```java foo(Collections.<String>emptyList()); ``` This explicit form is rare; needing it often is a hint the API or call could be clearer. ## A concrete contrast ```java // Wrong instinct: making the whole utility class generic class Utils<T> { T firstOrNull(List<T> xs) {...} } // forces Utils<String>, Utils<Integer>... pointless // Right: a generic STATIC method, class stays plain class Utils { static <T> T firstOrNull(List<T> xs) {...} } String s = Utils.firstOrNull(names); // T inferred, no Utils<> needed ``` ## Takeaways - Type-on-class = the object stores/represents the type; type-on-method = the type lives only for one call. - Static methods, methods relating argument types, and return-type-from-argument methods are the prime generic-method cases. - Inference (from arguments or target type) hides the `<T>` at call sites; the type witness is the escape hatch.

  • Why can't a static method use the enclosing class's type parameter?
    Class type parameters are bound per-instance, but a static method has no instance, so there's no T to refer to. A static method that needs genericity must declare its own method-level type parameter.
  • What's a 'type witness' and when do you need it?
    An explicit type argument at the call site, e.g. `Collections.<String>emptyList()`. You need it only when inference can't determine the type from the arguments or target — uncommon in well-typed code.

saying these in an interview costs you the question

  • Making the whole class generic for a type only one method uses
  • Thinking a static method can use the class's type parameter (it can't)
  • Believing callers must always pass method type arguments explicitly
  • Adding a type parameter that appears only once in the signature (often a code smell / should be a wildcard)

context