Lec 06 - Wildcards
Wildcards
Motivation: In Java, wildcards (?) are used in generics to create more flexible and reusable code by allowing parameterized types to operate on a range of types, rather than a single specific one.
Bonus: As we have seen in Lec 05 - Generics, Java Generics are invariant. But if we add wildcards (?) to Generics, it will have some variance relationship.
For example,
// Circle <: Shape
// A<? extends Circle> <: A<? extends Shape>Upper-Bounded Wildcards
An upper-bounded wildcard allows a generic type to accept any subtype of a specified class or interface T. This is useful when you want to read data from a generic structure and ensure that you're working with a specific base type or its subclasses.
For example,
public void copyFrom(Seq<? 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));
}
}Here, the Line 4 serves to read data from a generic structure. Here, what copyFrom does is to copy every element from another sequence called src, which is Seq<? extends T>, to the Seq<T> field in the current class.
Variance Relationship: Covariance
The upper-bounded wildcard has the following subtype relations:
If
S<:T, thenA<? extends S><:A<? extends T>(covariance)For any type
S,A<S><:A<? extends S>
Explanation
Let's use the knowledge we have seen in Lab 01.
subtype is nothing but a subset
For the Covariance Rule 1,
A<? extends S>represents the subset containing every type that is a subtype ofS(denote this subset as ).Similarly,
A<? extends T>represents the subset containing every type that is a subtype ofT(denote this subset as )Since
S<:T, we can intuitively see that the relationship between and is that .So, we can use "subtype is nothing but subset" again to deduce that
A<? extends S><:A<? extends T>.
For the Covariance Rule 2,
A<S>is a subset with a single element, which is typeS(deonte this subset as )Similarly,
A<? extends S>represents the subset containing every type that is a subtype ofS(denote this subset as ).We can intuitively see that the relationship between and is that .
So, we can use "subtype is nothing but subset" again to deduce that
A<S><:A<? extends S>.
Corollary
Substitutation principle: If
S<:T, thenA<S><:A<? extends T>.Transitivity: If
S<:TandT<:U, thenA<S><:A<? extends U>.
Lower-Bounded Wildcards
A lower-bounded wildcard allows a generic type to accept any supertype of a specified class T. This is particularly useful when you want to write data to a generic structure and ensure that the structure can accept objects of a specific type or its subclasses.
For example,
Here, Line 4 serves to write data to a generic structure. Here, what copyTo does is to copy every element in the Seq<T> field in the current class to the another sequence called dest, which is Seq <? super T>.
Variance Relationship: Contravariance
The lower-bounded wildcard has the following subtype relationship:
If
S<:T, thenA<? super T><:A<? super S>(contravariance)For any type
S,A<S><:A<? super S>
Explanation
Similarly as we have seen above, we can use the knowledge from Lab 01 again
subtype is nothing but a subset
For the Contrariance Rule 1,
A<? super S>represents the subset containing every type that is a supertype ofS(denote this subset as ).Similarly,
A<? super T>represents the subset containing every type that is a supertype ofT(denote this subset as )Since
S<:T, we can intuitively see that the relationship between and is that .So, we can use "subtype is nothing but subset" again to deduce that
A<? super T><:A<? super S>.
For the Contrariance Rule 2,
A<S>is a subset with a single element, which is typeS(deonte this subset as )Similarly,
A<? super S>represents the subset containing every type that is a supertype ofS(denote this subset as ).We can intuitively see that the relationship between and is that .
So, we can use "subtype is nothing but subset" again to deduce that
A<S><:A<? super S>.
Corollary
Transitivity: If
S<:T<:U, thenA<? super U><:A<? super T><:A<? super S>.
PECS Rule
"PECS" stands for "Producer Extends; Consumer Super". Basically this rule states that:
Producer (provides data): Use upper-bounded wildcards
? extends Tto read from it. SoTmust encompass (≥) the producer’s type.Consumer (accepts data): Use lower-bounded wildcards
? super Tto write to it. SoTmust fit inside (≤) the consumer’s type.
Unbounded Wildcards
An unbounded wildcard (?) means "I don’t know what the type is." It is used when we want to work with any type but don’t need to specify a relationship (subtype or supertype).
For example,
Here, the type of x can only be Object since it's the only safe choice. For, y it becomes even more restrictive, it must be null.
So, for a Seq<?>, we have the following principles,
We cannot add anything to
seq, exceptnull, because we don’t know the exact type.We can read from it, but the elements are treated as
Object.
Seq<?> is different from Seq<Object>, where the latter is not the supertype of any parameterized type Seq<T>, Seq<Object> is just a parameterized type of Seq<T> where T is Object.
Variance Relationship
A<?>is the supertype of every parameterized type ofA<T>, that isA<T><:A<?>.
Seq<?>, Seq<Object> and Seq
Seq<?>, Seq<Object> and SeqSeq<?>is a sequence of objects of some specific, but unknown type;Seq<Object>is a sequence ofObjectinstances, with type checking by the compiler;Seqis a sequence ofObjectinstances, without type checking.
Revisit Raw Type
The Problem with Raw Types and Generics
Java's generics (like List<String>) lose their type information during compilation due to "type erasure." This causes two main issues:
Type Checks: You can't reliably check specific generic types at runtime (e.g.,
instanceof List<String>won't work).Arrays: You can't directly create arrays of specific generic types (e.g.,
new List<String>[10]is invalid).
Old Solution - Raw Types
Previously, Java allowed you to:
Use raw types (without generics) for these cases:
instanceofchecks:a instanceof ArrayList(instead ofArrayList<String>)Array creation:
new ArrayList[10](instead ofArrayList<String>[10])
New Solution - Unbounded Wildcards (<?>)
<?>)Instead of raw types, we now use <?> (unknown type) to handle both scenarios better:
For instanceOf checks
The
<?>explicitly tells readers: "We're checking if it's an ArrayList of any type."This works because
<?>matches the erased type (just like raw types), but it's clearer and safer.
For Array creation
Comparable<?>is considered a "reifiable" type (its type info isn't lost during compilation).This allows safe array creation while still using generics.
Why This Matters
Clarity:
<?>clearly communicates "any type" instead of silently dropping generics (raw types).Safety: Discouraging raw types helps avoid accidental type errors in your code.
Modern Java: Newer Java versions encourage using wildcards (
<?>) over raw types.
Always use <?> instead of raw types when you need to check generic types with instanceof or create arrays of generic classes.
Revisit Type erasure
This is a continued discussion on Type Erasure process in Java
Since during the type erasure, all the generic type parameters information will be erased, the first step remains unchanged, that is the generic type will be erased to its rawtype.
Wildcard is not a type
?(which is the wildcard notation) cannot be used as type argument!
we cannot use ? when instantiating a generic type
For example, the following code doesn't work!
Instead, we should write as follows,
In this case, the compiler will do the type inference and conclude that the type argument will be Object.
we cannot use ? in generic type declaration
For example, the following is not allowed!
we can use ? to instantiate an array of generics
As we have seen Lec 06 - Wildcards, we can use unbounded wildcards to instantiate an array of generics. For example,
This means that we tell the compiler, I want an array of boxes, but I don't care what I put inside those boxes.
Type Inference
Type inference in Java is the compiler's ability to automatically determine (deduce) the type arguments for generic methods based on the context where they are used.
Rule to find the constraints
When doing Type Inference, form your constraints in the following ways
Target: __ <: __
This means "the return type of the method" <: "the type of the variable you are assigning to"
Argument: __ <: __
This means "the type of the argument" <: "the type of the parameter"
Bound: __ <: __
This means we need to consider "the bound of the generic type parameters"
For example, the following is our background,
If we use the following command, what will the T in max be inferred as?
Target:
T<:FruitArgument:
List<Fruit><:List<T>. Due to invaraince of generics,Tmust beFruitBound:
T<:Comparable<T>
By combing these three constraits, we can see that T will be inferred as Fruit.
Rule to solve the constraints
We now summarize the steps for type inference. First, we figure out all of the type constraints on our type parameters by using the rule above, and then we solve these constraints. If no type can satisfy all the constraints, we know that Java will fail to compile. If in resolving the type constraints for a given type parameter T we are left with:
Type1 <: T <: Type2, thenTis inferred asType1Type1 <: T, thenTis inferred asType1T <: Type2, thenTis inferred asType2
where Type1 and Type2 are arbitary types.
Last updated