Generic Type Parameter and Generic Wildcard - HolmesJJ/OOP-FP GitHub Wiki

Generic Type Parameter T, E, K, V

  • T, E, K, and V are all Generic Type Parameters, there is no difference, just a symbol meaning by convention.
  • Defined by capital letters A, B, C, D, ... X, Y, Z are all Generic Type Parameters, T and A are the same, T is just the meaning of the name.
Symbol Description
T A specific java type
K, V Key, Value in the java
E Element
public class Generic<T> { 

    private final T key;

    public Generic(T key) { 
        this.key = key;
    }
}

and

public class Generic<A> { 

    private final A key;

    public Generic(A key) { 
        this.key = key;
    }
}

Replacing T with A makes no difference in the execution effect, but T stands for type by convention, so it is better to follow the convention, which increases the readability of the code.

If need to define multiple Generic Type Parameters, such as key (K) and value (V) Generic Type Parameters in Map:

public interface MyMap<K, V> {

    public K getKey();
    public V getValue();
}

public class MyMapImpl<K, V> implements MyMap<K, V> {

    private K key;
    private V value;

    public MyMapImpl(K key, V value) {
        this.key = key;
        this.value = value;
    }

    @Override
    public K getKey()   { return key; }

    @Override
    public V getValue() { return value; }
}

public static void myMapExample() {
    MyMap<String, Integer> mp1 = new MyMapImpl<String, Integer>("Even", 0);
    MyMap<String, String>  mp2 = new MyMapImpl<String, String>("Hello", "World");
    MyMap<Integer, Integer> mp3 = new MyMapImpl<Integer, Integer>(123, 456);

    System.out.println(mp1);
    System.out.println(mp2);
    System.out.println(mp3);
}

/* Output */
key: Even, value: 0
key: Hello, value: World
key: 123, value: 456

Generic Wildcard ?

  • ? is not a Generic Type Parameter
  • ? is not a Generic Type Parameter
  • ? is not a Generic Type Parameter
  • ? is similar with Number, String, Integer, etc. they are all actual types.
  • ? can be regarded as the parent of all types, like Object.
Symbol Description
? Uncertain java type

Bounded Wildcard and Unbounded Wildcard

Symbol Description
? Unbounded Wildcard, only the methods in the Object class can be used
<? extends T> Upper Bounded Wildcard
<? super T> Lower Bounded Wildcard

Why use Bounded Wildcard?

Example:

There is the Fruit class, and its extended class Apple class.

class Fruit {
}

class Apple extends Fruit {
}

Then there is a simple container: Plate class. A generic "thing" can be put on the plate. Then the plate can do two simple actions: "put" and "take", which are set() and get() methods.

class Plate<T> {
    private T item;

    public Plate(T item) {
        this.item = item;
    }

    public void set(T item) {
        this.item = item;
    }

    public T get() {
        return item;
    }
}

If we define a "Fruit Plate", logically the apple can be put on fruit plate. Unfortunately, the compiler does not allow this operation.

Plate<Fruit> plate = new Plate<Apple>(new Apple());

// Compilation error: "Plate with apples" cannot be converted to "Plate with fruits".

In fact, the logic of the compiler is:

  • Apple IS-A Fruit
  • Plate with apples NOT-IS-A Plate with fruits

Even if there is an inheritance relationship between the things (Apple and Fruit) in the container (Plate), there is no inheritance relationship between the containers (Plate with apples and Plate with fruits). That is because Generic is Invariant.

Therefore, we need to use Bounded Wildcards <? extends T> and <? super T> to make the relationship between "Plate with apples" and " and "Plate with fruits".

Upper Bounded Wildcard

The following code is "Upper Bounded Wildcard":

Plate<? extends Fruit>

Popular explanation:

  • => A plate that can put Fruit and everything that is extended from Fruit.
  • => A plate that can put any Fruits.
  • The biggest difference between Plate<? extends Fruit> and Plate<Apple> is: Plate<? extends Fruit> is the super class of Plate and Plate.
  • This means that we can assign the "Plate with apples" to the "Plate with fruits".
Plate<? extends Fruit> plate = new Plate<Apple>(new Apple());

If we extend Fruit and Apple, Food includes Fruit and Meat, Fruit includes Apple and Banana, Meat includes Pork and Beef, and Apple includes Green Apple and Red Apple.

// Level 1
class Food{}

// Level 2
class Fruit extends Food{}
class Meat extends Food{}

// Level 3
class Apple extends Fruit{}
class Banana extends Fruit{}
class Pork extends Meat{}
class Beef extends Meat{}

// Level 4
class RedApple extends Apple{}
class GreenApple extends Apple{}

In this system, the Upper Bounded Wildcard Plate<? extends Fruit> covers the blue area in the figure below. upper_bounded_wildcard

Lower Bounded Wildcard

The following code is "Lower Bounded Wildcard":

Plate<? super Fruit>

It expresses the opposite concept: A plate that can put Fruit and everything is the super class of Fruit. Plate<? super Fruit> is the super class of Plate<Fruit>, but not the super class of Plate.

In this system, the Lower Bounded Wildcard Plate<? super Fruit> covers the red area in the figure below. lower_bounded_wildcard

Side effects of Upper and Lower Bounded Wildcards

Bounded Wildcards make it easier to convert between different Java Generics. But don't forget that such conversion also has certain side effects. Some methods of the container may be invalid.

From the examples above, there are two methods in the Plate class, set() new thing to the Plate, and get() thing from the Plate.

class Plate<T> {
    private T item;

    public Plate(T item) {
        this.item = item;
    }

    public void set(T item) {
        this.item = item;
    }

    public T get() {
        return item;
    }
}

Upper Bounded Wildcard <? extends T> cannot store elements in, can only take elements out

  • set() method is invalid.
  • get() method works normally.
Plate<? extends Fruit> plate = new Plate<Apple>(new Apple());
    
// Cannot store elements in
plate.set(new Fruit());    // Error
plate.set(new Apple());    // Error

// Can only take elements out
// The elements that get from the Plate can only be stored in Fruit or its super class
Apple newFruit1 = plate.get();    // Error
Fruit newFruit2 = plate.get();
Object newFruit3 = plate.get();

For the code Plate<? extends Fruit>, the compiler only knows that the container Plate is Fruit or its extended class, but it does not know what type it is. It could be Fruit, Apple, Banana, RedApple, GreenApple, etc.

For the code new Plate<Apple>(new Apple()), although the assignment here is Apple, the Plate is not marked with "Apple". Instead, it is marked with a placeholder: CAP#1, to indicate that a Fruit or its extended class is obtained. The specific class is not clear.

Therefore, the compiler doesn't know whether Apple, Banana, RedApple, GreenApple, etc. can match this CAP#1, so they are not allowed to store in.

Lower Bounded Wildcard <? super T> can store elements in and take elements out, but the elements taken out can only be assigned to Object

  • set() method works normally.
  • get() method is partially invalid.
Plate<? super Fruit> plate = new Plate<Fruit>(new Fruit());

// Can store elements in
p.set(new Fruit());
p.set(new Apple());

// The elements taken out can only be assigned to Object 
Apple newFruit1 = plate.get();    // Error
Fruit newFruit2 = plate.get();    // Error
Object newFruit3 = plate.get();

Because the lower bound specifies the lower limit of the minimum granularity of the element, it is actually the container that relaxes its control over the type of element.

Since the element is Fruit or the super class of Fruit, the element can be stored in the container Plate as long as the granularity of the element is smaller than Fruit. But the elements taken out can only be assigned to Object, all the information of the type of the element is lost.

Producer Extends Consumer Super (PECS) principles

  • Take elements in frequently: Upper Bounded Wildcard <? extends T>.
  • Store elements out frequently: Lower Bounded Wildcard <? super T>.
⚠️ **GitHub.com Fallback** ⚠️