Generic Methods
A type parameter is a placeholder written in angle brackets after a class name, and it works much like a formal parameter in a subroutine.
A Counting Problem That Repeats
Suppose you need to count how many times a value appears in an array. The logic is the same whether the array holds strings, colors, or numbers: walk through every element, compare it with the value you are looking for, and keep a running count. If you write a separate method for each element type, the same loop is repeated again and again. A generic method solves this problem by describing the logic once, using a placeholder type called a type parameter, so that the same method body can be applied to objects of any type.
This article looks at how such a method is used, not only how it is written. The central question is how Java decides which type stands in for the placeholder each time the method is called, and where that decision can go wrong. We will start with the placeholder itself, then follow the compiler through a real call.
Why One Method Can Serve Many Types
A generic method has one or more type parameters. A type parameter is a placeholder, written in angle brackets, that stands in for a real type. One method body can therefore serve many object types, because the body is written in terms of the placeholder rather than in terms of any particular type.
The idea is closely related to how a subroutine uses its formal parameters. A formal parameter is a stand-in that receives a real value each time the subroutine is called. A type parameter works in the same way, except that it receives a real type name instead of a value. The method is written once, and each call supplies the concrete type that fills the placeholder for that call.
What do you think happens?
A generic method works with any type, so will it accept a primitive int array such as int[] as its array argument?
Reveal answer
No. Generic programming in Java applies only to objects, so a generic method cannot be used with a primitive int[] array.
The phrase works with any type has a boundary. Primitive types such as int, double, and char are not objects, so they fall outside what a generic method can accept directly. Keep this answer in mind; we return to it later with autoboxing.
Reading a Generic Class Header
Generic ideas appear first in classes. The header of a generic class is where the placeholder is introduced. The general form is a class name followed by the type parameter in angle brackets. In the class Queue<ItemType>, the token ItemType is the type parameter. It is declared in the angle brackets directly after the class name, and it is used afterward wherever the class needs an element type.
Read a generic header from left to right. The angle-bracket part is the only place where the placeholder is declared. Everything inside the braces refers back to that name. When you read a method in the body, treat ItemType the same way you would treat String or int: it is a type name, even though its real value is not yet known.
- ItemType declared in the angle brackets after the class name
- ItemType as the actual type parameter of the internal
LinkedList - ItemType as the formal parameter type of enqueue
- ItemType as the return type of dequeue
Look at the four jobs the placeholder performs in this class. It is the return type of dequeue. It is the type of the formal parameter item in enqueue. It is the actual type parameter given to the LinkedList that stores the items, so the internal list is declared as a list of the placeholder type. The fourth job is less obvious: the class is written only in terms of the placeholder, so it never commits to any particular element type.
Substituting a Real Type into Queue
The placeholder is replaced by an actual type name when the class is used to declare a variable or create an object. The class definition itself is never rewritten. Instead, each use of the class supplies a type name, and the class behaves as if that name had been written in every place the placeholder appears. This is why one class definition gives you a whole family of parameterized types. Queue<String>, Queue<Integer>, and Queue<Color> are all valid uses of the same Queue class.
class Queue<ItemType>
LinkedList<ItemType> items
enqueue(ItemType item)
ItemType dequeue()ItemType is a placeholder with no real type yet
class Queue<String>
LinkedList<String> items
enqueue(String item)
String dequeue()Every use of ItemType now refers to String
first holds "Ada"- Queue<String> fills the placeholder with String at the declaration
- enqueue expects a String because its parameter type is now String
- dequeue returns a String, so no cast is needed
In this example, the declaration fills the placeholder with String. Wherever the class body said ItemType, the code now deals with String. The return type of dequeue becomes String, which is why the result can be assigned to a String variable without any cast. Nothing in the Queue class itself was rewritten to make this happen. The same substitution works for Queue<Integer> or Queue<Color>, each time with a different real type in the same single spot.
Queue is used in exactly the same way as built-in generic classes such as LinkedList and HashSet. Once you have written a generic class, another programmer uses it with the same angle-bracket notation they already know from those library classes.
How the Compiler Deduces T
A generic method can have one or more type parameters. In the method countOccurrences, the placeholder is T. When you call the method, you never write which type replaces T. The compiler works that out for you. It looks at the types of the actual arguments in the call and deduces the type that T must stand for. The programmer does not name the type in the call.
- <T> before the return type declares the method's type parameter
- T[] and T are uses of the placeholder, just as ItemType was in the class
- The loop body is identical for every element type
Consider the call countOccurrences(wordList, word). The variable wordList has type String[], so the compiler can tell that the type replacing T is String. The first argument supplies the information, and the second argument must agree with it. The reasoning is a sequence of decisions, shown in the flow below.
Deducing T from a String array
String[] wordList = {"the", "cat", "the"}; String word = "the"; int n = countOccurrences(wordList, word);
- 1Read the first argument
wordListhas type String[], which is an array whose elements are String. - 2Deduce T
Because the parameter type is T[], the element type of the array tells the compiler that T is String.
- 3Check the second argument
word has type String, which matches T, so the call is accepted.
- 4Run the body
The loop compares each element with "the" and counts two matches.
n is 2
The same method body serves other types too. If palette is a Color[] and color is a Color, the compiler deduces T as Color from those arguments, and the call counts occurrences of color in palette. Nothing about the method changed. Only the argument types differed, and that difference is what the compiler reads.
Autoboxing and the Primitive Array Boundary
Return now to the boundary from earlier. Generic programming in Java applies only to objects. Imagine a call made with an array of primitive int values, with the second argument being the literal 17. The generic method expects object values, and an int[] does not supply them. The call is rejected, because the placeholder cannot be matched to a primitive element type.
count is 2- numbers is an Integer[], so the compiler deduces T as Integer
- The literal 17 is a primitive int, but it is converted to an Integer object at the call
- The result is the number of elements equal to 17
The call countOccurrences(numbers, 17) uses autoboxing. The numbers variable is an Integer[], so the generic method expects object values. The literal 17 is a primitive int, not an object. Java automatically converts the 17 to a value of type Integer, so the call can proceed. Autoboxing converts the value at the point of the call. It does not change the array's element type, which remains Integer.
Autoboxing applies to a single value passed where an object is expected. It does not turn an int[] into an Integer[]. An int[] has a primitive element type, so it still cannot match a type parameter, even though a single int argument such as 17 can be boxed.
Mistakes Beginners Make with Type Parameters
Common mistake Assuming a generic method accepts primitive arrays
What it looks likeint[] numbers = {17, 4, 17}; countOccurrences(numbers, 17);Why it failsGeneric programming in Java applies only to objects, and an int[] has primitive elements, so it cannot supply the object type that T needs.
Do this insteadUse Integer[] for the array. The literal 17 is then autoboxed at the call.
Common mistake Writing the type in a generic method call
What it looks likecountOccurrences<String>(wordList, word);Why it failsA generic method deduces T from the arguments. The explicit type belongs to generic classes, such as Queue<String>.
Do this insteadCall countOccurrences(wordList, word) and let the compiler deduce T from the argument types.
Common mistake Believing the name of the type parameter changes behavior
Why it failsThe name is an arbitrary label. Renaming ItemType to T has no effect on the meaning of the class.
Do this insteadPick a name that helps readers, and change it consistently everywhere the placeholder appears.
Common mistake Forgetting that a placeholder must be used consistently
What it looks likeA class header declares ItemType, but a method body refers to Item.
Why it failsInside the class, each name refers to one declared type parameter. A name that was never declared does not refer to the placeholder.
Do this insteadWhen tracing a mismatch, check that every use matches the name in the angle brackets of the header.
Tracing a Type-Parameter Mismatch
When a generic class or method behaves unexpectedly, trace the placeholder. Start at the header, find the token declared in the angle brackets, and then follow each use of that token through the body. A mismatch appears at the first point where a real argument or value does not fit the type that the placeholder was bound to. Finding that single point is the core debugging skill behind this topic.
Given countOccurrences(palette, color) where palette is a Color[] and color is a String, identify where the type-parameter mismatch appears. Which argument does the compiler use to deduce T, and which argument disagrees with it?
Hints
- The first argument fixes T, because its element type is Color.
- Compare the type of the second argument with the deduced T.
Key Takeaways from Generic Methods
- 1A type parameter is a placeholder in angle brackets that stands in for a real type, much as a formal parameter stands in for a value.
- 2In a generic class, the placeholder appears in the header and is used as return types, parameter types, and type arguments of other generic classes such as
LinkedList. - 3A generic class is used by naming a real type, as in Queue<String>, and the placeholder is replaced everywhere it appears. A generic method is not given a type in the call; the compiler deduces T from the argument types.
- 4Generic programming applies only to objects, so primitive arrays such as int[] cannot be used directly, while autoboxing converts a single primitive value such as 17 into an Integer at the call.
At a glance
- A type parameter is a placeholder written in angle brackets that is replaced by an actual type name at each point of use.
- A generic class such as Queue<ItemType> uses its placeholder as return types, parameter types, and type arguments of other generic classes, and the names chosen for that placeholder are arbitrary labels.
- A generic method such as
countOccurrencesis written once and applied to many object types, with the compiler deducing T from the types of the actual arguments. - Generic programming applies only to objects: an int[] cannot be used directly, while autoboxing converts a single value such as 17 into an Integer at the call.