Wildcard Parameterized Types as Formal Parameters
We now examine the implications of using wildcard parameterized types to declare formal parameters of a method.
We want to add a method in the class MyStack<E> (Example 11.10, p. 598) for moving the elements of a source stack to the current stack. Here are three attempts at implementing such a method for the class MyStack<E>:
public void moveFromV1(MyStack<E> srcStack) { // (1)
while (!srcStack.isEmpty())
this.push(srcStack.pop());
}
public void moveFromV2(MyStack<? extends E> srcStack) { // (2)
while (!srcStack.isEmpty())
this.push(srcStack.pop());
}
public void moveFromV3(MyStack<? super E> srcStack) { // (3)
while (!srcStack.isEmpty())
this.push(srcStack.pop()); // Compile-time error!
}
Given the following three stacks:
MyStack<Number> numStack = new MyStack<>(); // Stack of Number
numStack.push(5.5); numStack.push(10.5); numStack.push(20.5);
MyStack<Integer> intStack1 = new MyStack<>(); // Stack of Integer
intStack1.push(5); intStack1.push(10); intStack1.push(20);
MyStack<Integer> intStack2 = new MyStack<>(); // Stack of Integer
intStack2.push(15); intStack2.push(25); intStack2.push(35);
We can only move elements between stacks of the same concrete type with the method at (1). The compile-time error below is due to the fact that MyStack<Integer> is not a subtype of MyStack<Number>.
intStack1.moveFromV1(intStack2); // Ok.
numStack.moveFromV1(intStack2); // Compile-time error!
We can also move elements from a stack of type MyStack<? extends E> to the current stack, using the method at (2). This is possible because a reference of a type MyStack<? extends E> can refer to a stack with objects of type E or its subclass, and the get operation (i.e., the pop() method) is permissible, returning an object which has an actual type bounded by the upper bound E. The returned object can always be put into a stack of type E or its supertype.
intStack1.moveFromV2(intStack2); // Pop from intStack2. Push on intStack1.
numStack.moveFromV2(intStack2); // Pop from intStack2. Push on numStack.
The method at (3) will only allow Objects to be popped from a stack of type MyStack<? super E>, which could only be pushed onto a stack of type Object. Since E cannot be determined at compile time, the push() operation on the current stack is not permitted. Of the first two methods, the method at (2) is more flexible in permitting the widest range of calls for copying from the source stack to the current stack.
Similarly, we can add a method in the class MyStack<E> for moving the elements of the current stack to a destination stack:
public void moveToV1(MyStack<E> dstStack) { // (4)
while (!this.isEmpty())
dstStack.push(this.pop());
}
public void moveToV2(MyStack<? extends E> dstStack) { // (5)
while (!this.isEmpty())
dstStack.push(this.pop()); // Compile-time error!
}
public void moveToV3(MyStack<? super E> dstStack) { // (6)
while (!this.isEmpty())
dstStack.push(this.pop());
}
In the method at (5), the reference of type MyStack<? extends E> does not allow any set operations (in this case, the push() method) on the destination stack. The method at (6) provides the most flexible solution, as a reference of type MyStack<? super E> permits set operations for objects of type E or its subtypes:
intStack1.moveToV1(intStack2); // Pop from intStack1. Push on intStack2.
intStack1.moveToV1(numStack); // Compile-time error!
intStack1.moveToV3(intStack2); // Pop from intStack1. Push on intStack2.
intStack1.moveToV3(numStack); // Pop from intStack1. Push on numStack.
Evidently, the method at (6) is more flexible in permitting the widest range of calls for copying from the current stack to the destination stack.
Based on the discussion above, we can write a generic method for moving elements from a source stack to a destination stack. The following method signature is preferable, where objects of type E or its subtypes can be popped from the source stack and pushed onto a destination stack of type E or its supertype:
public static <E> void move(MyStack<? extends E> srcStack, // (7)
MyStack<? super E> dstStack) {
while (!srcStack.isEmpty())
dstStack.push(srcStack.pop());
}
// Client code
MyStack.move(intStack1, intStack2);
MyStack.move(intStack1, numStack);
MyStack.move(numStack, intStack2); // Compile-time error!
It is a common idiom to use wildcards as shown above in the method at (7), as the upper bounded wildcard (? extends Type) can be used to get objects from a data structure, and the lower bounded wildcard (? super Type) can be used to set objects in a data structure. Using wildcards in the method signature can increase the utility of a method, especially when explicit type parameters are specified in the method call.
Leave a Reply