A generic method declares its own type parameter, independent of any enclosing class. A plain, non-generic class can host a method that works with any type, and a generic class can host a method whose type parameter has nothing to do with the class's own. This lesson covers the syntax, where the <T> goes, how the compiler infers the type at the call site, and the common shapes that show up in everyday e-commerce code.
A class-level type parameter, like the T in Box<T>, lives on the instance. Every method inside Box<T> sees the same T, and that T is fixed when you build the box. Use this approach when a single object should hold one type of thing.
A lot of useful operations don't fit that shape. Consider a helper that picks the first non-null of two arguments. The two arguments should have the same type, the return should be that same type, but there's no object to "hold" the type. Each call is independent. The type parameter belongs on the method, not on the class.
The simplest version:
One method, two completely different element types in two calls. The class NullFallback is not generic; it has no <T> after its name. The <T> belongs to firstNonNull alone. Each call gets its own T, inferred from the arguments.
The placement of <T> is what makes this a generic method. It sits between the modifiers (public static) and the return type (T). The compiler reads that as: from here until the end of the method's signature, the name T is a stand-in for a type the caller will fix.
If the <T> is missing, the compiler doesn't know what T means in the parameters or return type:
What's wrong with this code?
The compiler reports cannot find symbol: class T because T was never declared. Without the <T> declaration before the return type, T is just an unknown name, not a type parameter.
Fix:
The declaration <T> introduces the name; the rest of the signature can then use it as a type. Read it like this: "for any type T, this method takes two Ts and returns a T."
The four parts of a generic method signature sit in a fixed order:
Modifiers come first, then the type parameter list <T>, then the return type, then the method name and parameter list. The order matters: public static T <T> firstNonNull(...) doesn't compile, because the compiler would try to read T as the return type before it had been declared.
A method can declare more than one type parameter. They go inside the same angle brackets, separated by commas. A method that pairs a key and a value into a Map.Entry:
Two independent type parameters, K and V, fixed separately at each call. The first call binds K to String and V to Integer. The second binds both to String. The method body never has to know the concrete types; it hands them to Map.entry, which is itself a generic method on the Map interface.
The host class doesn't need to be generic for its methods to be. Utility classes are the most common example: a holder for static helpers that operate on whatever types the caller passes.
A small utility class for cart operations:
CartUtils is a plain class with no type parameter of its own. Each method carries its own <T> and works on whatever element type the caller passes. firstN is called once with List<String> and could also be called with List<Order>; same code, different T. addIfNotPresent is called twice with List<Integer> and Integer; the compiler ensures the item type matches the list's element type at each call.
This is the most common shape of a generic method. Most static helpers in java.util.Collections and java.util.Arrays follow this pattern: a non-generic class hosting many independent generic methods.
firstN allocates a new list sized to limit up front, which avoids the resizing growth that an empty ArrayList would go through during the loop. Without the capacity hint, a few small reallocations happen on the way to the final size.
A generic class can host a method whose type parameter is different from the class's. Take care here: the class's T and the method's T are not the same T, even if you write the same letter.
A small container that supports converting its contents through a caller-supplied mapping function demonstrates this. (The class is generic in T; the method introduces a new type parameter R for the result.)
The class fixes T to String when you build nameBox. The map method introduces a fresh R, which is bound to Integer at the call site because String::length returns an int (autoboxed to Integer). The method returns a new Box<R>, which is Box<Integer> in this call.
Reusing the class's name T for the method makes the compiler treat the method's T as shadowing the class's T, which is almost never the intent. Each name is a label, but reusing a label invites confusion.
What's wrong with this code?
The class declares T and the method declares another, unrelated T inside the same scope. Inside map, the name T refers to the method's type parameter, not the class's. Reading value as a T of the class no longer works, because value's static type is the outer (shadowed) T. Some compilers raise a warning; either way, it hurts readability.
Fix: Use a different letter for the method's type parameter, like R for "result". That signals that the two parameters are independent.
Conventional letters help: T for "type" on the class, R for "result", K and V for "key" and "value", E for "element". They're not enforced by the language; they're a contract for the next reader of the code.
In every example so far, the call to a generic method has been short: firstNonNull(null, "WELCOME10"), not NullFallback.<String>firstNonNull(null, "WELCOME10"). The compiler infers the type parameter from the arguments, so most of the time the type doesn't need to be written explicitly.
Inference looks at three sources:
For firstNonNull(null, "WELCOME10"), the first argument is null (which doesn't constrain T), but the second is a String, so the compiler infers T = String. For pair("Notebook", 25), the first argument fixes K = String and the second fixes V = Integer.
The target type kicks in when arguments alone aren't enough. A generic factory for empty lists:
emptyList has no arguments, so the compiler can't look at parameters to infer T. It looks at the target type instead. On the line List<String> wishlist = emptyList();, the target is List<String>, which fixes T = String. On the next line, the target is List<Integer>, so T = Integer. Two calls to the same generic method produce two differently typed empty lists.
When the compiler can't infer the type and there's no target type to fall back on, the diamond goes empty and T defaults to Object. That's almost never the intended behaviour.
What's wrong with this code?
Inside println(Object), the target type for emptyList() is Object, which the compiler accepts. T becomes Object, and the call returns a List<Object>. No compile error, but probably not the intended outcome; assigning the result to a List<String> would fail.
Fix: Assign to a typed variable first, or use the explicit-type syntax to pin T at the call site.
Or:
The second form is the explicit type witness. The angle brackets sit between the dot and the method name. It's rare in practice (the compiler usually handles inference well), but recognizing it is useful.
Inference for the most common case:
The compiler combines what it sees in the arguments with what it knows about the target type, intersects the constraints, and picks the narrowest T that satisfies both. When the inputs disagree, it walks up the type hierarchy until they meet. firstNonNull("WELCOME10", 25) infers T = Object (or more precisely, an upper bound of Object & Serializable & Comparable<?>) because String and Integer have no closer common parent in Java's type system.
T is inferred as something like Object & Serializable & Comparable<?>, which is why the call site has to assign the result to an Object. The runtime value is still a String; it's the static type that widens.
Inference handles almost every call. Three situations still need an explicit witness:
The call has no arguments and no target type. Calling EmptyLists.emptyList() directly inside a varargs argument list, or passing it to a method that accepts Object, gives the compiler no constraints to work with.
A `T` wider than the arguments suggest is needed. Passing a String while the inferred type should be CharSequence requires the witness to force it.
Method chaining where inference can't see far enough. Older Java versions had trouble inferring through chained calls; the witness short-circuits the lookup.
Three calls, one without a witness and two with. The second call forces T = CharSequence even though the arguments are Strings, which the compiler accepts because String implements CharSequence. The third call uses the witness because passing the result straight to println(Object) would otherwise erase the element type.
The witness goes between the receiver (or the class name, for static calls) and the method name, separated by a dot. For static method calls on the current class, the form is WitnessExamples.<T>method(...) or, on an instance, this.<T>method(...). The dot is part of the syntax; without it the compiler reads < as a less-than operator.
Most generic methods are static. Instance methods on a generic class usually rely on the class's T, so they don't need their own. The methods that need their own are usually utilities that don't depend on instance state.
A pattern that comes up often in e-commerce code is picking something at random:
Output (varies between runs):
The method works on any element type, because the body never touches anything specific to String (or Integer, or whatever the caller passes). The compiler's type-checking guarantees that the returned T matches the list's T, so the caller never needs a cast.
Put one of these together with a Map:
Two type parameters, fixed independently from map's declared types. The compiler checks that key matches map's key type and value matches map's value type. The actual java.util.Map interface ships a richer putIfAbsent that also returns the existing value, but the shape is the same: a generic method whose type parameters mirror the map's.
containsKey followed by put walks the hash table twice. Map.putIfAbsent (the built-in instance method) is one lookup. The version above is fine for teaching the syntax, but production code should use the built-in.
Putting several of these patterns into one class shows how generic methods compose in real code. The class is non-generic; each method declares its own type parameter (or parameters):
Output (pick varies between runs):
Five generic methods, five different shapes:
firstNonNull takes two values of the same type and returns one.firstN takes a list of one type and returns a list of that same type.pickRandom takes a list and returns one element.pair takes two values of two different types and returns a structured combination.invert takes a map and swaps its key and value types.Each one declares the type parameters it needs in its own header. The class doesn't carry any of them, because it doesn't need to. This is the pattern that fits utility code: data-free classes with generic methods that operate on whatever the caller has.
The two tools solve different problems:
| Question | Generic class | Generic method |
|---|---|---|
| Where is the type parameter declared? | After the class name | Before the return type |
| How long does the type stay fixed? | Lifetime of the instance | Single method call |
| Does every method on the host see the same type? | Yes | Only this one method |
| Best fit for | Containers and data structures | Utilities and helpers |
Example from java.util | ArrayList<E>, HashMap<K,V> | Collections.<T>emptyList(), Map.entry(k, v) |
A class is generic when an instance holds something whose type the caller chose. A method is generic when an operation works on arguments whose types the caller chose, but no instance is needed to carry that type forward.
Both can coexist in one class. ArrayList<E> has a class-level E, but the static generic method Collections.<T>emptyList() (declared on Collections, not on ArrayList) follows the generic method pattern. Inside ArrayList, methods like toArray(T[] a) declare their own T, separate from the class's E.
List<E> has E = String, and toArray is <T> T[] toArray(T[] a). The <T> is declared on the method, fixed to String by the array argument. The compiler enforces that T is compatible with E (in a real library this is enforced at runtime by checking the array's component type), so an Integer[] cannot accidentally be passed to a List<String> and produce a typed Integer[] back. That signature shows why a method inside a generic class might still want its own type parameter: the array type isn't necessarily the same as the element type, but it has to be a supertype of it.
10 quizzes