Topic 8.3
Generic Methods
In one line
A generic method declares its own type parameters, written before the return type: static <T> T first(List<T> list). The compiler works out T from the arguments and the target at each call, so one method serves every type with full checking.
Think of it like this
A photocopier. It copies whatever you put in: a letter, a drawing, a recipe. What comes out is the same kind of page you put in. You don't need a "letter copier" and a "recipe copier". A generic method is like that copier: it works for any type, and the type that comes out is tied to the type that went in.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Generic method
- A method that declares its own type parameters, written in angle brackets before the return type, like
<T> T first(List<T> list). - Type parameter section
- The
<T>or<K, V>part that declares a method's or class's type parameters. - Type inference
- The compiler working out the type arguments for you from the arguments and the place the result goes.
- Target type
- The type the compiler expects an expression to have, from the variable it's assigned to or the parameter it's passed to.
- Type witness
- Writing a method's type argument yourself, like
Collections.<String>emptyList(). - Intersection type
- A type that is several types at once, like "
SerializableandComparable". The compiler uses it when inference needs a common supertype of different arguments. - Shadowing
- When a new declaration uses the same name as an outer one, hiding it inside its scope, like a method's
<T>hiding the class'sT. - Static factory
- A static method that creates and returns an object, like
List.of(...)orOptional.of(...), used instead of callingnewdirectly.
Step by step
01Declare the type parameter before the return type
The <T> comes after public static and before the return type T. Read it as: "for any type T, this method takes a list of T and returns a T".
Forget the <T> and the compiler treats T as an ordinary class name, which doesn't exist. You get two cannot find symbol errors, one for each use.
public class Main { // not a generic class
static <T> T first(List<T> list) { // a generic method
return list.get(0);
}
static <K, V> Map<V, K> invert(Map<K, V> map) { ... } // two type parameters
}02Inference from the arguments
When you call first(names) with a List<String>, the compiler matches List<T> against List<String> and concludes T = String. The call's type is String, so first(names).length() compiles with no cast.
Inference runs separately at every call. The same method called with a List<Integer> returns Integer. Nothing about the method changes; the compiler just checks each call site with its own T.
03Inference from the target type (Java 8)
Some generic methods have no argument mentioning T: Collections.emptyList(), List.of(). The compiler then uses the target: List<String> e = Collections.emptyList(); infers T = String from the left side.
Java 8 made inference much stronger: it now flows through method arguments, so printAll(Collections.emptyList()) where printAll takes List<String> compiles. In Java 7 that needed a type witness. This target typing is also what makes lambdas work (Phase 10).
List<String> empty = Collections.emptyList(); // T inferred from the target
printAll(Collections.emptyList()); // Java 8+: inferred from the parameter type
List<String> e2 = Collections.<String>emptyList(); // explicit type witness04When inference and your intent disagree
If you ask for an Integer from a list of strings, inference finds T must equal String (from the argument) and also fit inside Integer (from the target). It can't do both, and the message lists those constraints.
A type witness changes where the error appears: Main.<Integer>first(names) fixes T = Integer, so now the argument is wrong, and the message is the simpler List<String> cannot be converted to List<Integer>.
05Using T twice ties values together
<T> T pick(T a, T b) says both arguments and the result share one type. With pick("tea", "chai") that's String. With pick("tea", 42) the only T that fits both is a common supertype: an intersection of Serializable, Comparable and a few other interfaces that String and Integer both implement.
Assign that to Object and it compiles. Assign it to String and you get the error below, with the inferred intersection spelled out as INT#1.
06Generic static factories
A very common use is a static factory: a static generic method that creates objects, like List.of, Map.of, Optional.of and Comparator.comparing. Before the diamond existed (Java 7), factories were the main way to avoid repeating type arguments, and they're still preferred when a class wants named constructors.
Example: a Result<T> with Result.ok(value) and Result.fail(message). ok(42) infers Result<Integer>; fail("bad") infers its T from the target, because no argument mentions T.
static <T> Result<T> ok(T value) { return new Result<>(value, null); }
static <T> Result<T> fail(String error) { return new Result<>(null, error); }
Result<Integer> a = Result.ok(42); // T = Integer from the argument
Result<Integer> b = Result.fail("bad"); // T = Integer from the target07Generic instance methods inside generic classes
An instance method of Box<T> can declare an extra type parameter: <R> Box<R> map(Function<T, R> f). T comes from the box, R from the function you pass. Box<String>.map(String::length) gives a Box<Integer>.
Pick a new letter. Writing <T> Box<T> map(...) inside Box<T> shadows the class's T, so the method's T silently means something else. It compiles, and it's a classic source of confusing errors.
Try it yourself
- 1
Predict the inferred type
In the inference example, add
var v = pick(1, 2.5);and printv.getClass().getSimpleName(). Predict the printed class before running. Then tryint x = pick(1, 2.5);and read the error. - 2
Write your own factory
In the
Resultexample, addstatic <T> Result<T> ofNullable(T value)that returnsfail("missing")fornullandok(value)otherwise. Call it with"x"and withnull(assigned to aResult<String>). Predict both printouts. - 3
Shadow a type parameter on purpose
In the
Boxexample, rename<R>to<T>inmap(andBox<R>toBox<T>,Function<T, R>toFunction<T, T>). WhichTis which now? Compile and see which calls break.
Code & diagrams
Expected output
ASHA
14
[A3, A2, A1]
[*, *, *, *, *] size 5The object returned is still the real Integer 42. Inference only decides which static type the compiler lets you treat it as.
Expected output
pick strings: chai
pick mixed: 42 (Integer)
as Comparable: 42
empty: []
count: 0
witnessed: 0Expected output
21 -> Ok(21), age or 0: 21
-4 -> Fail(negative age), age or 0: 0
abc -> Fail(not a number: abc), age or 0: 0Function and lambdas arrive properly in Phase 10. Here, just notice how R is inferred from what each function returns.
Expected output
Box(generics) Box(8) Box(true) Box(GENERICS)
doubled length: 16Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Forget to declare the type parameter
Write static T first(List<T> list) without <T> before the return type.
Break #2
Ask for the wrong type from a generic method
With List<String> names, write Integer n = first(names);.
Break #3
Force a mixed pick into a String
With static <T> T pick(T a, T b), write String s = pick("tea", 42);.
Myth vs fact
Myth
Only generic classes can have generic methods.
Fact
Any class can. Collections, Arrays and Objects are ordinary classes full of static generic methods.
Myth
Type inference happens at run time.
Fact
It is purely a compiler step. The bytecode has no record of what T was inferred to be at a call, only the casts the compiler inserted.
Myth
You must always write the type argument, like Collections.<String>emptyList().
Fact
Since Java 8's improved inference you almost never need a witness. It's useful only to steer inference or to get a clearer error.
Myth
A method's <T> is the same as its class's T.
Fact
A method-level <T> declares a brand-new type variable that shadows the class's. Use a different letter to avoid the confusion.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Java 8 rewrote inference (JEP 101, "Generalized Target-Type Inference", part of JSR 335). Generic method calls became poly expressions whose type can depend on their context, including when nested as arguments to other generic calls. That is what lets
collect(Collectors.toList())and chains ofComparator.comparing(...).thenComparing(...)type-check, though very long chains sometimes still need a hint. - ▸
The JLS (section 18) defines inference as a constraint-solving problem: each call creates inference variables with equality, upper-bound and lower-bound constraints, which
javacresolves. The compiler messages (equality constraints,lower bounds,upper bounds) are printing exactly those constraint sets. - ▸
Prefer generic methods to raw
Objectutilities:static <T extends Comparable<? super T>> T max(Collection<? extends T> c)inCollectionsis the canonical example, combining a generic method, a bound (Topic 8.4) and wildcards (Topic 8.5). You'll be able to read every part of it by the end of the phase. - ▸
Constructors can be generic too:
<T> Main(T seed)declares a type parameter on the constructor itself, independent of the class's. It's legal but rare; static factories are clearer for the same job.
Remember this
- 1
You declare a generic method by putting a type parameter section between the modifiers and the return type:
public static <T> T first(List<T> list). That<T>says "this method has a type parameter called T". Without it, the compiler looks for a class namedTand reportscannot find symbol ... class T. - 2
Generic methods can live in any class, generic or not, and can be static or instance methods. That's how utility classes like
CollectionsandArrayswork:Collections.max,Arrays.asList,List.ofandObjects.requireNonNullare all generic methods in non-generic classes. A static method can't use its class'sT(Topic 8.2), so declaring its own is the only way for it to be generic. - 3
At each call the compiler performs type inference: it solves for
Tusing the argument types and, since Java 8, the target type (the type the result is assigned to or passed to).first(List.of("a"))infersT = String.List<String> e = Collections.emptyList();infersT = Stringpurely from the assignment target. - 4
You can also give the type argument yourself with a type witness:
Collections.<String>emptyList()orMain.<Integer>first(list). You rarely need it now, but it helps when inference picks something too general, and it makes error messages clearer. The witness needs a receiver in front of the dot (Main.orthis.): a bare<String>first(list)doesn't compile. - 5
When arguments disagree, inference looks for a common supertype.
pick("tea", 42)with<T> T pick(T a, T b)infers an intersection type, roughly "something that isSerializableandComparableand...", because that's whatStringandIntegerhave in common. Assigning the result toStringthen fails. Generic methods connect types: usingTin two places forces them to agree. - 6
A method's type parameter can shadow the class's: in
class Box<T> { <T> void put(T x) }, the method'sTis a different, unrelated type. That compiles but confuses everyone; use a different letter (<U>,<R>). Instance generic methods inside generic classes are common and useful:Box<T>can offer<R> Box<R> map(Function<T, R> f), which turns aBox<String>into aBox<Integer>. Streams (Phase 10) are built from exactly such methods.
Explain it without notes
How do you declare a generic method, and how is it different from a method in a generic class that uses the class's T?
Explain type inference. What information does the compiler use to decide T?
What is a type witness, and when would you use one?
Why are generic static factory methods so common in the JDK?
Practice
Write static <T> int countMatches(List<T> list, T target) that counts elements equal to target using equals. Test it with strings and with integers.
Write static <K, V> Map<V, K> invert(Map<K, V> map) that swaps keys and values, returning a TreeMap. Invert {a=1, b=2, c=3} and print it.
Write static <T> List<T> interleave(List<T> a, List<T> b) that alternates elements from both lists (then appends any leftovers). Test with [1, 3, 5, 7] and [2, 4].
Trade-offs
- ↔
Generic methods give type-safe reuse without making the whole class generic, but using one type parameter for several arguments couples them: sometimes you want
<A, B>instead of<T>, and sometimes a wildcard (Topic 8.5) is simpler than a type parameter. - ↔
Inference keeps call sites short, but errors in long inferred chains can be hard to read. Breaking a chain into local variables with explicit types is a valid way to get a clear message.
- ↔
Static factories are flexible (names, caching, subtypes) but less discoverable than constructors and don't work with
new-based frameworks. Most APIs offer factories for value-like types and constructors for plain classes.
Done when you can
Done when you can write a static generic method and place the
<T>correctly.Done when you can say which type the compiler infers at a call, from arguments or from the target.
Done when you can use a type witness and know it needs a receiver.
Done when you can read an inference error's equality and upper-bound constraints.
Done when you can write a generic static factory and a
map-style instance method with its own type parameter.