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.

Why is it called upper-bounded?

This is because the wildcard ? is bounded above by T, which means that ? <: T. (a.k.a T is an upper-bound of wildcard ?)

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.

Each of the element from src should be of type ? extends T, which is obviously a subtype of T.

Variance Relationship: Covariance

The upper-bounded wildcard has the following subtype relations:

  1. If S <: T, then A<? extends S> <: A<? extends T> (covariance)

  2. 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,

  1. A<? extends S> represents the subset containing every type that is a subtype of S (denote this subset as XX).

  2. Similarly, A<? extends T> represents the subset containing every type that is a subtype of T (denote this subset as YY)

  3. Since S <: T, we can intuitively see that the relationship between XX and YY is that XYX\subset Y.

  4. So, we can use "subtype is nothing but subset" again to deduce that A<? extends S> <: A<? extends T>.

For the Covariance Rule 2,

  1. A<S> is a subset with a single element, which is type S (deonte this subset as XX)

  2. Similarly, A<? extends S> represents the subset containing every type that is a subtype of S (denote this subset as YY).

  3. We can intuitively see that the relationship between XX and YY is that XYX\subset Y.

  4. So, we can use "subtype is nothing but subset" again to deduce that A<S> <: A<? extends S>.

Corollary

  1. Substitutation principle: If S <: T, then A<S> <: A<? extends T>.

  2. Transitivity: If S <: T and T <: U, then A<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.

Why is it called lower-bounded?

This is because the wildcard ? is bounded below by T, which means that T <: ?. (a.k.a T is a lower-bound of wildcard ?)

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>.

Each element from dest should be of type ? super T, which is obviously a supertype of T.

Variance Relationship: Contravariance

The lower-bounded wildcard has the following subtype relationship:

  1. If S <: T, then A<? super T> <: A<? super S> (contravariance)

  2. 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,

  1. A<? super S> represents the subset containing every type that is a supertype of S (denote this subset as XX).

  2. Similarly, A<? super T> represents the subset containing every type that is a supertype of T (denote this subset as YY)

  3. Since S <: T, we can intuitively see that the relationship between XX and YY is that YXY\subset X.

  4. So, we can use "subtype is nothing but subset" again to deduce that A<? super T> <: A<? super S>.

For the Contrariance Rule 2,

  1. A<S> is a subset with a single element, which is type S (deonte this subset as XX)

  2. Similarly, A<? super S> represents the subset containing every type that is a supertype of S (denote this subset as YY).

  3. We can intuitively see that the relationship between XX and YY is that XYX\subset Y.

  4. So, we can use "subtype is nothing but subset" again to deduce that A<S> <: A<? super S>.

Corollary

  1. Transitivity: If S <: T <: U, then A<? 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 T to read from it. So T must encompass () the producer’s type.

  • Consumer (accepts data): Use lower-bounded wildcards ? super T to write to it. So T must fit inside () the consumer’s type.

PECS is usually used on method parameter. So, one easy way for you to think is that:

  1. Take the method parameter as your "studyObject"

  2. look at the studyObject.method()

  3. If .method() is something like get(), read(), then your studyObject is a producer, add lower-bounded wildcard to your method parameter.

  4. If .method() is something like set(), write(), then your studyObject is a consumer, add upper-bounded wildcard to your method parameter.

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, except null, because we don’t know the exact type.

  • We can read from it, but the elements are treated as Object.

Variance Relationship

  1. A<?> is the supertype of every parameterized type of A<T>, that is A<T> <: A<?>.

Seq<?>, Seq<Object> and Seq

  • Seq<?> is a sequence of objects of some specific, but unknown type;

  • Seq<Object> is a sequence of Object instances, with type checking by the compiler;

  • Seq is a sequence of Object instances, 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:

    • instanceof checks: a instanceof ArrayList (instead of ArrayList<String>)

    • Array creation: new ArrayList[10] (instead of ArrayList<String>[10])

New Solution - Unbounded Wildcards (<?>)

Instead of raw types, we now use <?> (unknown type) to handle both scenarios better:

1

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.

2

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.

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

  1. ? (which is the wildcard notation) cannot be used as type argument!

1

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.

2

we cannot use ? in generic type declaration

For example, the following is not allowed!

3

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.

Type Inference happens usually because when you are parameterizing the generic methods or generic types, you didn't provide the type arguments. Thus, it needs to deduce them using some rules.

Rule to find the constraints

When doing Type Inference, form your constraints in the following ways

1

Target: __ <: __

This means "the return type of the method" <: "the type of the variable you are assigning to"

2

Argument: __ <: __

This means "the type of the argument" <: "the type of the parameter"

3

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?

  1. Target: T <: Fruit

  2. Argument: List<Fruit> <: List<T>. Due to invaraince of generics, T must be Fruit

  3. Bound: 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, then T is inferred as Type1

  • Type1 <: T, then T is inferred as Type1

  • T <: Type2, then T is inferred as Type2

where Type1 and Type2 are arbitary types.

Last updated