Command Palette

Search for a command to run...

PHASE 8Intermediate Java 5+ ~33 min· topic 3 of 6

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 "Serializable and Comparable". 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's T.
Static factory
A static method that creates and returns an object, like List.of(...) or Optional.of(...), used instead of calling new directly.

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.

Main.javawhole filejava
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
}
terminal
$ javac Main.java # with 'static T first(List<T> list)', no <T>
── expected output ──
Main.java:4: error: cannot find symbol
static T first(List<T> list) {
^
symbol: class T
location: class Main
Main.java:4: error: cannot find symbol
static T first(List<T> list) {
^
symbol: class T
location: class Main
2 errors

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.

Inference from the argumentsdiagram
Rendering diagram…

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

Main.javawhole filejava
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 witness

04When 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>.

terminal
$ javac Main.java
── expected output ──
Main.java:10: error: incompatible types: inference variable T has incompatible bounds
Integer n = first(names);
^
equality constraints: String
upper bounds: Integer,Object
where T is a type-variable:
T extends Object declared in method <T>first(List<T>)
1 error

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.

terminal
$ javac Main.java
── expected output ──
Main.java:5: error: incompatible types: inferred type does not conform to upper bound(s)
String s = pick("tea", 42);
^
inferred: INT#1
upper bound(s): String,Object
where INT#1,INT#2 are intersection types:
INT#1 extends Object,Serializable,Comparable<? extends INT#2>,Constable,ConstantDesc
INT#2 extends Object,Serializable,Comparable<?>,Constable,ConstantDesc
1 error

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.

Main.javawhole filejava
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 target

07Generic 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. 1

    Predict the inferred type

    In the inference example, add var v = pick(1, 2.5); and print v.getClass().getSimpleName(). Predict the printed class before running. Then try int x = pick(1, 2.5); and read the error.

  2. 2

    Write your own factory

    In the Result example, add static <T> Result<T> ofNullable(T value) that returns fail("missing") for null and ok(value) otherwise. Call it with "x" and with null (assigned to a Result<String>). Predict both printouts.

  3. 3

    Shadow a type parameter on purpose

    In the Box example, rename <R> to <T> in map (and Box<R> to Box<T>, Function<T, R> to Function<T, T>). Which T is which now? Compile and see which calls break.

Code & diagrams

Generic utility methods in a non-generic class New tab
Sign in to run this example in your browser.

Expected output

ASHA
14
[A3, A2, A1]
[*, *, *, *, *] size 5
Inference: from arguments, from the target, and by witness New tab

The object returned is still the real Integer 42. Inference only decides which static type the compiler lets you treat it as.

Sign in to run this example in your browser.

Expected output

pick strings: chai
pick mixed: 42 (Integer)
as Comparable: 42
empty: []
count: 0
witnessed: 0
Static factory methods with a generic Result type New tab
Sign in to run this example in your browser.

Expected 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: 0
A generic instance method that changes the type: map New tab

Function and lambdas arrive properly in Phase 10. Here, just notice how R is inferred from what each function returns.

Sign in to run this example in your browser.

Expected output

Box(generics) Box(8) Box(true) Box(GENERICS)
doubled length: 16

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

terminal
$ javac Main.java
── what you'll see ──
Main.java:4: error: cannot find symbol
static T first(List<T> list) {
^
symbol: class T
location: class Main
Main.java:4: error: cannot find symbol
static T first(List<T> list) {
^
symbol: class T
location: class Main
2 errors

Break #2

Ask for the wrong type from a generic method

With List<String> names, write Integer n = first(names);.

terminal
$ javac Main.java
── what you'll see ──
Main.java:10: error: incompatible types: inference variable T has incompatible bounds
Integer n = first(names);
^
equality constraints: String
upper bounds: Integer,Object
where T is a type-variable:
T extends Object declared in method <T>first(List<T>)
1 error

Break #3

Force a mixed pick into a String

With static <T> T pick(T a, T b), write String s = pick("tea", 42);.

terminal
$ javac Main.java
── what you'll see ──
Main.java:5: error: incompatible types: inferred type does not conform to upper bound(s)
String s = pick("tea", 42);
^
inferred: INT#1
upper bound(s): String,Object
where INT#1,INT#2 are intersection types:
INT#1 extends Object,Serializable,Comparable<? extends INT#2>,Constable,ConstantDesc
INT#2 extends Object,Serializable,Comparable<?>,Constable,ConstantDesc
1 error

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 of Comparator.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 javac resolves. The compiler messages (equality constraints, lower bounds, upper bounds) are printing exactly those constraint sets.

  • ▸

    Prefer generic methods to raw Object utilities: static <T extends Comparable<? super T>> T max(Collection<? extends T> c) in Collections is 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. 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 named T and reports cannot find symbol ... class T.

  2. 2

    Generic methods can live in any class, generic or not, and can be static or instance methods. That's how utility classes like Collections and Arrays work: Collections.max, Arrays.asList, List.of and Objects.requireNonNull are all generic methods in non-generic classes. A static method can't use its class's T (Topic 8.2), so declaring its own is the only way for it to be generic.

  3. 3

    At each call the compiler performs type inference: it solves for T using the argument types and, since Java 8, the target type (the type the result is assigned to or passed to). first(List.of("a")) infers T = String. List<String> e = Collections.emptyList(); infers T = String purely from the assignment target.

  4. 4

    You can also give the type argument yourself with a type witness: Collections.<String>emptyList() or Main.<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. or this.): a bare <String>first(list) doesn't compile.

  5. 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 is Serializable and Comparable and...", because that's what String and Integer have in common. Assigning the result to String then fails. Generic methods connect types: using T in two places forces them to agree.

  6. 6

    A method's type parameter can shadow the class's: in class Box<T> { <T> void put(T x) }, the method's T is 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 a Box<String> into a Box<Integer>. Streams (Phase 10) are built from exactly such methods.

Explain it without notes

01

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?

02

Explain type inference. What information does the compiler use to decide T?

03

What is a type witness, and when would you use one?

04

Why are generic static factory methods so common in the JDK?

Practice

01

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.

02

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.

03

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.