A generic class parameterizes every one of its members at once. A generic method parameterizes a single call. The distinction matters because most polymorphic code in a real Java codebase is the second kind: static utilities, converters, and helpers whose type relationship holds for one invocation and has nothing to do with the enclosing class.

The idea

A generic method’s type parameter lives in its own scope, declared before the return type, and the compiler almost always works out what it should be from the call itself. The inference is deliberately local: it looks at the arguments, at the type the surrounding context expects, and at nothing else. That restriction is what keeps Java’s inference predictable at the cost of occasionally needing help, and it is the design line separating this from Hindley-Milner inference, which propagates constraints across a whole definition and can infer types no annotation ever mentions.

Declaring one

The type parameter list goes after the modifiers and before the return type:

static <U> void addBox(U u, java.util.List<Box<U>> boxes) { ... }

U is in scope for the parameters, the return type, and the body, and it is unrelated to any type parameter the enclosing class may have. Two calls to the same generic method in the same expression can bind U to different types.

Invoking it usually looks like an ordinary call:

BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);

The compiler infers U as Integer from the arguments. The explicit form exists and is nearly always unnecessary:

BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);

That angle-bracket prefix before the method name is called a type witness. Its position looks wrong the first time you see it, because it sits after the receiver’s dot rather than in front of the whole expression, and it is one of the few places in Java where a qualified name is mandatory: you have to write BoxDemo.<Integer>addBox(...) or this.<Integer>addBox(...) rather than the bare method name.

What the compiler is allowed to look at

The tutorial states the inference algorithm’s inputs and its hard limit in one sentence: “The inference algorithm uses only invocation arguments, target types, and possibly an obvious expected return type to infer types. The inference algorithm does not use results from later in the program.”

That final clause is the whole design. A variable’s inferred type argument is fixed at the point of the call, from information available at that point. Nothing you write three lines later can reach back and change it. The consequence is that inference failures are local and readable: when the compiler cannot infer a type argument, the problem is in the call or its immediate context, never in a distant use.

The algorithm also finds “the most specific type that works with all arguments.” Pass an Integer and a Double to a method expecting two Ts and T does not fail to infer; it infers something that accommodates both, which in practice is a compiler-generated intersection involving Number and Comparable. That is a frequent source of error messages naming types nobody wrote, and reading them is easier once you know the compiler is reporting a least upper bound rather than a type from your source.

When you actually need the witness

The remaining case is a call whose arguments carry no information about the type parameter and whose context does not either. Collections.emptyList() in a position that is not an assignment or a typed argument is the standard example: there is no argument to infer from, so the compiler needs either a target type or an explicit Collections.<String>emptyList(). Java 8’s expansion of target typing removed most of these, which is why the witness has largely disappeared from modern code.

Generic method versus generic class

Both are useful, and choosing wrongly produces awkward APIs.

A type parameter belongs on the class when the parameter is part of the object’s identity and multiple members must agree on it: a Box<T> stores a T and returns a T, and the two must be the same type across calls. A type parameter belongs on the method when it relates that call’s inputs and output and nothing persists: Collections.max does not need a Collections<T>.

The failure mode is putting a parameter on the class that only one method uses. Every caller then has to name a type argument at construction that is irrelevant to most of what they do with the object, and the type shows up in every field declaration and every signature that mentions the class.

Sources

  • “Type Inference,” The Java Tutorials. https://docs.oracle.com/javase/tutorial/java/generics/genTypeInference.html . Supports the definition of type inference, the quoted statement that the algorithm uses only invocation arguments, target types, and possibly an obvious expected return type and does not use results from later in the program, the claim that it finds the most specific type that works with all arguments, the addBox declaration and both its inferred and explicit-type-witness invocations, and the Collections.emptyList() target-typing example.
  • “Generic Methods,” The Java Tutorials. https://docs.oracle.com/javase/tutorial/java/generics/methods.html . Supports that a generic method declares its type parameter list before the return type and that the parameter’s scope is limited to the method.