11.4 Wildcards
In this section, we discuss how using wildcards can increase the expressive power of generic types. But first we examine one major difference between array types and parameterized types. The generic class Node<E> used in this subsection is defined in Example 11.2, p. 568.
The Subtype Covariance Problem with Parameterized Types
The following three declarations create three nodes of Integer, Double, and Number type, respectively.
Node<Integer> intNode = new Node<>(2020,null); // (1)
Node<Double> doubleNode = new Node<>(3.14,null); // (2)
Node<Number> numNode = new Node<>(2021, null); // (3)
In the declaration at (3), the signature of the constructor call is Node(Integer, null). The formal type parameter E of the generic class Node<E> is bound to the actual type parameter Number—that is, the signature of the constructor is Node(Number, Node<Number>). Since the type Integer is a subtype of the type Number, and null can be assigned to any reference, the constructor call succeeds.
In the method calls at (4) and (5) below, the method signature in both cases is setData(Number). The method calls again succeed, since the actual parameters are of types Double and Integer, which are subtypes of Number:
numNode.setData(10.5); // (4)
numNode.setData(2022); // (5)
However, the following calls do not succeed:
numNode.setNext(intNode); // (6) Compile-time error!
numNode = new Node<Number>(2030, doubleNode); // (7) Compile-time error!
The actual type parameter at (6) is determined to be Number. The generic class Node<E> is thus parameterized with the class Number. The compiler complains that the method setNext(Node<Number>) in the parameterized class Node<Number> is not applicable for the actual argument (Node<Integer>) at (6)—that is, the method signature setNext(Node<Number>) is not compatible with the method call signature setNext(Node<Integer>). The compiler also complains at (7): The constructor signature Node(Number, Node<Number>) is not applicable for the arguments (int, Node<Double>). The problem is with the second argument at (7). We cannot pass an argument of type Node<Integer> or Node<Double> where a parameter of type Node<Number> is expected. The following assignments will also not compile:
numNode = intNode; // (8) Compile-time error!
numNode = doubleNode; // (9) Compile-time error!
The reason for the compile-time errors is that Node<Integer> and Node<Double> are not subtypes of Node<Number>, although Integer and Double are subtypes of Number. In the case of arrays, the array types Integer[] and Double[] are subtypes of the array type Number[]. The subtyping relationship between the individual types carries over to corresponding array types. This type relationship is called subtype covariance (see Figure 11.2). This relationship holds for arrays because the element type is available at runtime, and can be checked. If the subtype covariance were allowed for parameterized type, it could lead to problems at runtime, as the element type would not be known and cannot be checked, since it has been erased by the compiler.
numNode = intNode; // If this assignment was allowed,
numNode.setData(25.5); // the data could be corrupted,
Integer iRef = intNode.getData(); // resulting in a ClassCastException!
Therefore, the subtype covariance relationship does not hold for parameterized types that are instantiations of the same generic type with different actual type parameters, regardless of any subtyping relationship between the actual type parameters. The actual type parameters are concrete types (e.g., Integer, Number), and therefore, the parameterized types are called concrete parameterized types. Such parameterized types are totally unrelated. As an example from the Java Collections Framework, the parameterized type Map<Integer, String> is not a subtype of the parameterized type Map<Number, String>.
Figure 11.2 No Subtype Covariance for Parameterized Types
Leave a Reply