Calling Generic Methods
Consider the following class declaration:
public class ClassDecl {
static <E_1,…, E_k> void genericMethod(P_1 p_1,…, P_m p_m) { … }
// …
}
Note that in the method declaration above, a type P_i may or may not be from the list of type parameters E_1, …, E_k. We can call the method in various ways. One main difference from calling a non-generic method is that the actual type parameters can be specified before the method name in the call to a generic method. In the method calls shown below, <A_1,…, A_k> are the actual type parameters and (a_1,…, a_m) are the actual arguments. The specification <A_1,…, A_k> of the actual type parameters is known as a type witness, as it corroborates the types to use in the method call. If included, it must be specified in its entirety. If there is not type witness, then the compiler infers the actual type parameters.
The following method calls can occur in any static or non-static context where the class CallDecl is accessible:
CallDecl ref;
ref.<A_1,…, A_k>genericMethod(a_1,…, a_m);
CallDecl.<A_1,…, A_k>genericMethod(a_1,…, a_m);
The following method calls can only occur in a non-static context of the class CallDecl:
this.<A_1,…, A_k>genericMethod(a_1,…, a_m); // Non-static context
super.<A_1,…, A_k>genericMethod(a_1,…, a_m); // Non-static context
CallDecl.super.<A_1,…, A_k>genericMethod(a_1,…, a_m); // Non-static context
Another difference from calling non-generic methods is that, if the type witness is explicitly specified, the syntax of a generic static method call requires an explicit reference or the raw type. When the type witness is not explicitly specified, the syntax of a generic method call is similar to that of a non-generic method call.
<A_1,…, A_k>genericMethod(a_1,…, a_m); // Compile-time error!
genericMethod(a_1,…, a_m); // Ok.
Here are some examples of calls to the containsV2() method at (2) in the class Utilities in Example 11.9, where the type witness is specified. We can see from the method signature and the method call signature that the method can be applied to the arguments at (1), (2), and (3), but not at (4). At (5), we must specify a reference or the class name because a type witness with the actual type parameter is specified.
Integer[] intArray = {10, 20, 30};
boolean f1 = Utilities.<Integer>containsV2(20, intArray); // (1) true
// E is Integer.
// Method signature: containsV2(Integer, Integer[])
// Method call signature: containsV2(Integer, Integer[])
boolean f2 = Utilities.<Number>containsV2(30.5, intArray); // (2) false
// E is Number.
// Method signature: containsV2(Number, Number[])
// Method call signature: containsV2(Double, Integer[])
boolean f3 = Utilities.<Comparable<Integer>> containsV2(20, intArray); // (3) true
// E is Comparable<Integer>.
// Method signature: containsV2(Comparable<Integer>, Comparable<Integer>[])
// Method call signature: containsV2(Integer, Integer[])
boolean f4 = Utilities.<Integer>containsV2(30.5, intArray); // (4) Error!
// E is Integer.
// Method signature: containsV2(Integer, Integer[])
// Method call signature: containsV2(Double, Integer[])
// Requires explicit reference or raw type.
boolean f5 = <Integer>containsV2(20, intArray); // (5) Syntax error!
Here are some examples of method calls where the compiler infers the actual type parameters from the method call. At (6), both the key and the element type are Integer, the compiler infers that the actual type parameter is Integer. At (7), where the key type is Double and the element type is Integer, the compiler infers the actual type parameter to be Number—that is, the first common supertype of Double and Integer. At (8), the compiler infers the actual type parameter to be Serializable— that is, the first common supertype of String and Integer. In all the cases below, the method is applicable to the arguments.
boolean f6 = Utilities.containsV2(20, intArray); // (6) true
// E is inferred to be Integer.
// Method signature: containsV2(Integer, Integer[])
// Method call signature: containsV2(Integer, Integer[])
boolean f7 = Utilities.containsV2(30.5, intArray); // (7) false;
// E is inferred to be Number.
// Method signature: containsV2(Number, Number[])
// Method call signature: containsV2(Double, Integer[])
boolean f8 = Utilities.containsV2(“Hi”, intArray); // (8) false;
// E is inferred to be Serializable.
// Method signature: containsV2(Serializable, Serializable[])
// Method call signature: containsV2(String, Integer[])
At (8), if we had specified the actual type parameter explicitly to be Integer, the compiler would flag an error, as shown at (9), since the method signature is not applicable to the arguments:
boolean f9 = Utilities.<Integer>containsV2(“Hi”, intArray); // (9) Error!
// E is Integer.
// Method signature: containsV2(Integer, Integer[])
// Method call signature: containsV2(String, Integer[])
We can explicitly specify the key type to be a subtype of the element type by introducing a new formal parameter and a bound on the key type, as for the method containsV3() at (3) in Example 11.9:
static <K extends E, E> boolean containsV3(K key, E[] array) {
// …
}
The following calls at (10) and (11) illustrates inferring of actual type parameters from the method call when no type witness is specified. At (10), the compiler infers the K type parameter to be Double and the type parameter E to be Number—that is, the first common supertype of Double and the array element type Integer. The constraint is satisfied and the method signature is applicable to the arguments.
boolean f10 = Utilities.containsV3(30.5, intArray); // (10) false
// K is inferred to be Double. E is inferred to be Number.
// The constraint (K extends E) is satisfied.
// Method signature: containsV3(Double, Number[])
// Method call signature: containsV2(Double, Integer[])
boolean f11 = Utilities.containsV3(“Hi”, intArray); // (11) false
// K is inferred to be String. E is inferred to be Serializable.
// The constraint (K extends E) is satisfied.
// Method signature: containsV3(String, Serializable[])
// Method call signature: containsV2(String, Integer[])
At (11), the compiler infers the K type parameter to be String and the type parameter E to be Serializable—that is, the first common supertype of String and the array element type Integer. The constraint is satisfied and the method signature is applicable to the arguments, as both String and Integer are Serializable.
The examples below illustrate how constraints come into play in method calls. At (12), the constraint is satisfied and the method signature is applicable to the arguments. At (13), the constraint is not satisfied; therefore, the call is rejected. At (14), the constraint is satisfied, but the method signature is not applicable to the arguments. The call at (15) is rejected because the number of actual type parameters specified in the call is incorrect.
boolean f12 = Utilities.<Number, Number>containsV3(30.0, intArray); // (12) false
// K is Number. E is Number.
// The constraint (K extends E) is satisfied.
// Method signature: containsV3(Number, Number[])
// Method call signature: containsV3(Double, Integer[])
boolean f13 = Utilities.<Number, Integer>
containsV3(30.5, intArray); // (13) Error!
// K is Number. E is Integer.
// The constraint (K extends E) is not satisfied.
boolean f14 = Utilities.<Integer, Number>
containsV3(30.5, intArray); // (14) Error!
// K is Integer. E is Number.
// The constraint (K extends E) is satisfied.
// Method signature: containsV3(Integer, Number[])
// Method call signature: containsV3(Double, Integer[])
boolean f15 = Utilities.<Number>containsV3(30.5, intArray); // (15) Error!
// Incorrect no. of type parameters.
Typically, the dependencies among the parameters of a method and its return type are expressed by formal type parameters. Here are some examples:
public static <K,V> Map<V,List<K>> toMultiMap(Map<K,V> origMap) { … } // (16)
public static <N> Set<N> findVerticesOnPath(Map<N,Collection<N>> graph,
N startVertex) { … } // (17)
The method header at (16) expresses the dependency that the map returned by the method has the values of the original map as keys, and its values are lists of keys of the original map—that is, the method creates a multimap. In the method header at (17), the type parameter N specifies the element type of the set of vertices to be returned, the type of the keys in the map, the element type of the collections that are values of the map, and the type of the start vertex.
Leave a Reply