As noted in Item 28, parameterized types are invariant. In other words,
for any two distinct types
Type1
and Type2
, List<Type1>
is neither a subtype nor a supertype of List<Type2>
. Although it is counter-intuitive that
List<String>
is not a subtype of List<Object>
, it really does make sense. You can put any object into a List<Object>
, but you can put only strings into a List<String>
. Since a List<String>
can’t do everything a List<Object>
can, it isn’t a subtype (by the Liskov substitution principal, Item 10).public class Stack<E> {
public Stack();
public void push(E e);Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ... ;
numberStack.pushAll(integers);
public E pop();
public boolean isEmpty();
}
// pushAll method without wildcard type - deficient!
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
StackTest.java:7: error: incompatible types: Iterable<Integer>
cannot be converted to Iterable<Number>
numberStack.pushAll(integers);
cannot be converted to Iterable<Number>
numberStack.pushAll(integers);
Luckily, there’s a way out. The language provides a special kind of parameterized type call a bounded wildcard type to deal with situations like this. The type of the input parameter to
pushAll
should not be “Iterable
of E
” but “Iterable
of some subtype of E
,” and there is a wildcard type that means precisely that: Iterable<? extends E>
. (The use of the keyword extends
is slightly misleading: recall from Item 29 that subtype is defined so that every type is a subtype of itself, even though it does not extend itself.) Let’s modify pushAll
to use this type:
// Wildcard type for a parameter that serves as an E producer
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
// popAll method without wildcard type - deficient!
public void popAll(Collection<E> dst) {while (!isEmpty())
dst.add(pop());
}
// Wildcard type for parameter that serves as an E consumer
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
For maximum flexibility, use wildcard types on input parameters that represent producers or consumers.
PECS stands for producer-
extends
, consumer-super
.
The PECS mnemonic captures the fundamental principle that guides the use of wild-card types. Naftalin and Wadler call it the Get and Put Principle [Naftalin07, 2.4].
// Wildcard type for parameter that serves as an T producer
public Chooser(Collection<? extends T> choices)
public Chooser(Collection<? extends T> choices)
public static <E> Set<E> union(Set<E> s1, Set<E> s2)
public static <E> Set<E> union(Set<? extends E> s1,
Set<? extends E> s2)
Set<? extends E> s2)
Properly used, wildcard types are nearly invisible to the users of a class. They cause methods to accept the parameters they should accept and reject those they should reject. If the user of a class has to think about wildcard types, there is probably something wrong with its API.
Set<Integer> integers = Set.of(1, 3, 5);
Set<Double> doubles = Set.of(2.0, 4.0, 6.0);
Set<Number> numbers = union(integers, doubles);
Set<Double> doubles = Set.of(2.0, 4.0, 6.0);
Set<Number> numbers = union(integers, doubles);
Union.java:14: error: incompatible types
Set<Number> numbers = union(integers, doubles);
^
required: Set<Number>
found: Set<INT#1>
where INT#1,INT#2 are intersection types:
INT#1 extends Number,Comparable<? extends INT#2>
INT#2 extends Number,Comparable<?>
Set<Number> numbers = union(integers, doubles);
^
required: Set<Number>
found: Set<INT#1>
where INT#1,INT#2 are intersection types:
INT#1 extends Number,Comparable<? extends INT#2>
INT#2 extends Number,Comparable<?>
Luckily there is a way to deal with this sort of error. If the compiler doesn’t infer the correct type, you can always tell it what type to use with an explicit type argument [JLS, 15.12]. Even prior to the introduction of target typing in Java 8, this isn’t something that you had to do often, which is good because explicit type arguments aren’t very pretty. With the addition of an explicit type argument, as shown here, the code fragment compiles cleanly in versions prior to Java 8:
// Explicit type parameter - required prior to Java 8
Set<Number> numbers = Union.<Number>union(integers, doubles);
Set<Number> numbers = Union.<Number>union(integers, doubles);
public static <T extends Comparable<T>> T max(List<T> list)
public static <T extends Comparable<? super T>> T max(
List<? extends T> list)
To get the revised declaration from the original, we applied the PECS heuristic twice. The straightforward application is to the parameter
list
. It produces T
instances, so we change the type from List<T>
to List<? extends T>
. The tricky application is to the type parameter T
. This is the first time we’ve seen a wildcard applied to a type parameter. Originally, T
was specified to extend Comparable<T>
, but a comparable of T
consumes T
instances (and produces integers indicating order relations). Therefore, the parameterized type Comparable<T>
is replaced by the bounded wildcard type Comparable<? super T>
. Comparables are always consumers, so you should generally use Comparable<? super T>
in preference to Comparable<T>
. The same is true of comparators; therefore, you should generally use Comparator<? super T>
in preference to Comparator<T>
.
List<ScheduledFuture<?>> scheduledFutures = ... ;
// Two possible declarations for the swap method
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
Which of these two declarations is preferable, and why? In a public API, the second is better because it’s simpler. You pass in a list—any list—and the method swaps the indexed elements. There is no type parameter to worry about. As a rule, if a type parameter appears only once in a method declaration, replace it with a wildcard. If it’s an unbounded type parameter, replace it with an unbounded wildcard; if it’s a bounded type parameter, replace it with a bounded wildcard.
There’s one problem with the second declaration for
swap
. The straightforward implementation won’t compile:
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
list.set(i, list.set(j, list.get(i)));
}
Swap.java:5: error: incompatible types: Object cannot be
converted to CAP#1
converted to CAP#1
ist.set(i, list.set(j, list.get(i)));
^
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?public static void swap(List<?> list, int i, int j) {
^
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
// Private helper method for wildcard capture
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
}
// Private helper method for wildcard capture
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
In summary, using wildcard types in your APIs, while tricky, makes the APIs far more flexible. If you write a library that will be widely used, the proper use of wildcard types should be considered mandatory. Remember the basic rule: producer-
extends
, consumer-super
(PECS). Also remember that all comparables and comparators are consumers.
No comments:
Post a Comment