Unit 23: Wildcards
After going through this unit, students should:
- be aware of the meaning of wildcard
?
and bounded wildcards - know how to use wildcards to write methods that are more flexible in accepting a range of types
- know that upper-bounded wildcard is covariant and lower-bounded wildcard is contravariant
- know the PECS principle and how to apply it:
contains
with Array<T>
Now that we have our Array<T>
class, let's modify our generic contains
method and replace the type of the argument T[]
with Array<T>
.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Similar to the version that takes in T[]
, using generics allows us to constrain the type of the elements of the array and the object to search for to be the same. This allows the following code to type-check correctly:
1 2 3 4 5 6 |
|
But trying to search for a circle in an array of string would lead to a type error:
1 |
|
Consider now having an array of shapes.
1 2 3 4 5 6 7 |
|
As expected, we can pass Shape
as the argument for T
, and search for a Shape
in an instance of Array<Shape>
. Similarly, we can pass Circle
as the argument for T
and search for a Circle
in an instance of Array<Circle>
.
We could also look for a Circle
instance from Array<Shape>
if we pass Shape
as the argument for T
.
1 |
|
Note that we can pass in a Circle
instance as a Shape
, since Circle
<: Shape
.
Recall that generics are invariant in Java, i.e, there is no subtyping relationship between Array<Shape>
and Array<Circle>
. Array<Circle>
is not a subtype of Array<Shape>
. Otherwise, it would violate the Liskov Substitution Principle, we can put a square into an Array<Shape>
instance, but we can't put a square into an Array<Circle>
instance.
So, we can't call:
1 |
|
The following would result in compilation errors as well:
1 2 |
|
Thus, with our current implementation, we can't look for a shape (which may be a circle) in an array of circles, even though this is something reasonable that a programmer might want to do. This constraint is due to the invariance of generics -- while we avoided the possibility of run-time errors by avoiding covariance arrays, our methods have become less general.
Let's see how we can fix this with bounded type parameters first. We can introduce another type parameter, say S
, to remove the constraints that the type of the array must be the same as the type of the object to search for. I.e., we change from
1 |
|
to:
1 |
|
But we don't want to completely decouple T
and S
, as we want T
to be a subtype of S
. We can thus make T
a bounded type parameter, and write:
1 |
|
Now, we can search for a shape in an array of circles.
1 |
|
Copying to and from Array<T>
Let's consider another example. Let's add two methods copyFrom
and copyTo
, to Array<T>
so that we can copy to and from one array to another.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
With this implementation, we can copy, say, an Array<Circle>
to another Array<Circle>
, an Array<Shape>
to another Array<Shape>
, but not an Array<Circle>
into an Array<Shape>
, even though each circle is a shape!
1 2 3 4 5 |
|
Upper-Bounded Wildcards
Let's consider the method copyFrom
. We should be able to copy from an array of shapes, an array of circles, an array of squares, etc, into an array of shapes. In other words, we should be able to copy from an array of any subtype of shapes into an array of shapes. Is there such a type in Java?
The type that we are looking for is Array<? extends Shape>
. This generic type uses the wildcard ?
. Just like a wild card in card games, it is a substitute for any type. A wildcard can be bounded. Here, this wildcard is upper-bounded by Shape
, i.e., it can be substituted with either Shape
or any subtype of Shape
.
The upper-bounded wildcard is an example of covariance. The upper-bounded wildcard has the following subtyping relations:
- If
S
<:T
, thenA<? extends S>
<:A<? extends T>
(covariance) - For any type
S
,A<S>
<:A<? extends S>
For instance, we have:
Array<Circle>
<:Array<? extends Circle>
- Since
Circle
<:Shape
,Array<? extends Circle>
<:Array<? extends Shape>
- Since subtyping is transitive, we have
Array<Circle>
<:Array<? extends Shape>
Because Array<Circle>
<: Array<? extends Shape>
, if we change the type of the parameter to copyFrom
to Array<? extends T>
,
1 2 3 4 5 6 |
|
We can now call:
1 |
|
without error.
Lower-Bounded Wildcards
Let's now try to allow copying of an Array<Circle>
to Array<Shape>
.
1 |
|
by doing the same thing:
1 2 3 4 5 6 |
|
The code above would not compile. We will get the following somewhat cryptic message when we compile with the -Xdiags:verbose
flag:
1 2 3 4 5 6 7 8 9 10 11 |
|
Let's try not to understand what the error message means first, and think about what could go wrong if the compiler allows:
1 |
|
Here, we are trying to put an instance with compile-time type T
into an array that contains elements with the compile-time type of T
or subtype of T
.
The copyTo
method of Array<Shape>
would allow an Array<Circle>
as an argument, and we would end up putting instance with compile-time type Shape
into Array<Circle>
. If all the shapes are circles, we are fine, but there might be other shapes (rectangles, squares) in this
instance of Array<Shape>
, and we can't fit them into Array<Circle>
! Thus, the line
1 |
|
is not type-safe and could lead to ClassCastException
during run-time.
Where can we copy our shapes into? We can only copy them safely into an Array<Shape>
, Array<Object>
, Array<GetAreable>
, for instance. In other words, into arrays containing Shape
or supertype of Shape
.
We need a wildcard lower-bounded by Shape
, and Java's syntax for this is ? super Shape
. Using this new notation, we can replace the type for dest
with:
1 2 3 4 5 6 |
|
The code would now type-check and compile.
The lower-bounded wildcard is an example of contravariance. We have the following subtyping relations:
- If
S
<:T
, thenA<? super T>
<:A<? super S>
(contravariance) - For any type
S
,A<S>
<:A<? super S>
For instance, we have:
Array<Shape>
<:Array<? super Shape>
- Since
Circle
<:Shape
,Array<? super Shape>
<:Array<? super Circle>
- Since subtyping is transitive, we have
Array<Shape>
<:Array<? super Circle>
The line of code below now compiles:
1 |
|
Our new `Array 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// version 0.5 (with flexible copy using wildcards)
class Array<T> {
private T[] array;
Array(int size) {
// The only way we can put an object into the array is through
// the method set() and we only put an object of type T inside.
// So it is safe to cast `Object[]` to `T[]`.
@SuppressWarnings("unchecked")
T[] a = (T[]) new Object[size];
this.array = a;
}
public void set(int index, T item) {
this.array[index] = item;
}
public T get(int index) {
return this.array[index];
}
public void copyFrom(Array<? extends T> src) {
int len = Math.min(this.array.length, src.array.length);
for (int i = 0; i < len; i++) {
this.set(i, src.get(i));
}
}
public void copyTo(Array<? super T> dest) {
int len = Math.min(this.array.length, dest.array.length);
for (int i = 0; i < len; i++) {
dest.set(i, this.get(i));
}
}
}
PECS
Now we will introduce the rule that governs when we should use the upper-bounded wildcard ? extends T
and a lower-bounded wildcard ? super T
. It depends on the role of the variable. If the variable is a producer that returns a variable of type T
, it should be declared with the wildcard ? extends T
. Otherwise, if it is a consumer that accepts a variable of type T
, it should be declared with the wildcard ? super T
.
As an example, the variable src
in copyFrom
above acts as a producer. It produces a variable of type T
. The type parameter for src
must be either T
or a subtype of T
to ensure type safety. So the type for src
is Array<? extends T>
.
On the other hand, the variable dest
in copyTo
above acts as a consumer. It consumes a variable of type T
. The type parameter of dest
must be either T
or supertype of T
for it to be type-safe. As such, the type for dest
is Array<? super T>
.
This rule can be remembered with the mnemonic PECS, or "Producer Extends; Consumer Super".
Unbounded Wildcards
It is also possible to have unbounded wildcards, such as Array<?>
. Array<?>
is the supertype of all generic Array<T>
.
A method that takes in generic type with unbounded wildcard would be pretty restrictive, however. Consider this:
1 2 3 4 5 6 |
|
What should the type of the returned element x
be? Since Array<?>
is the supertype of all possible Array<T>
, the method foo
can receive an instance of Array<Circle>
, Array<String>
, etc. as an argument. The only safe choice for the type of x
is Object
.
The type for y
is every more restrictive. Since there are many possibilities of what type of array it is receiving, we can only put null
into array
!
Back to contains
Now, let's simplify our contains
methods with the help of wildcards. Recall that to add flexibility into the method parameter and allow us to search for a shape in an array of circles, we have modified our method into the following:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Can we make this simpler using wildcards? Since we want to search for an object of type S
in an array of its subtype, we can remove the second parameter type T
and change the type of array to Array<? extends S>
:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
We can double-check that array
is a producer (it produces curr
on Line 5) and this follows the PECS rules.
Now, we can search for a shape in an array of circles.
1 |
|