What makes the use of bounded wildcard parameterized types possible in an array in java?

87 Views Asked by At

What makes it possible to add a Pair<String, String> when I'm only extending the class Number ? Does't this violate the expectation that an array should only hold elements of the same type ?

public class test {

    public static void main(String[] args) {
        
        Pair<? extends Number, ? extends Number>[] arr = new Pair[2];
        
        m1(arr);
        
        arr[1] = new Pair<Integer, Integer>(1, 1);
        
        System.out.println(arr[0].x);
        System.out.println(arr.getClass().getComponentType());
        
    }
    
    static void m1(Object[] arr) {
        arr[0] = new Pair<String, String>("test","test");
    }
    
    
}

class Pair<T, E> {
    
    public T x;
    public E y;
    
    public Pair(T x, E y) {
        this.x = x;
        this.y = y;
    }
    
}
2

There are 2 best solutions below

2
JayC667 On

We have the following situation: inheritance in Java is on multiple levels, not just on 2 levels.

  • class Object
  • class Number (extends Object)
  • class Integer extends Number
  • class Double extends Number

Relations:

  • Number, Integer, Double are Objects
  • Integer, Double are Numbers
  • Integer, Double are Objects

So an Integer IS a Number, just with some additional functionality. Same for Double. If you tell the array (define, declare) to hold Numbers, by writing either List<Number> list or List(? extends Number) list (so Number or any subclass thereof), then you can add any Number.

If you created an array of Numbers like this: arr[0] = new Double(2.0); (initialize and assign) then the new value is a Double, but Double is a Number, so the new value also is still a Number. So this is totally valid.

In your case, you create an array of Pairs of Numbers, making the whole thing a little more convoluted, but the basic principle remains the same: The "declared" type is still Number for all of them, but the assigned value is Double or Integer (which are still also Numbers). You could still sort this out later by instanceOf checks to see what different subtypes the values are, if need be.

2
Rafael Odon On

This is one example of the array covariance plus type erasure in Java.

Array covariance

Arrays in Java are covariant: if an array can handle Integer (Integer[]), and Integer is an Object, then this array can also handle Object, so it can be referenced as Object[]. Since String is also an Object, things get tricky! (read further here and here).

Type Erasure

The correctness of the generics types is checked at compile-time, and then forgotten/erased at runtime. A Pair<Double, Double> is just a Pair at runtime. That's why if you can trick the compiler, as you did introducing the m1() method, then you are ok at runtime. (read further here).

Your code explained

Your arr variable is described as Pair<? extends Number, ? extends Number>[]. But remember that variables are just a name that references some object. The actual object behind it is just an array of Pair (Pair[]).

When you call m1(), another variable is used to reference this very same array, but this time it's a more general variable that handle any array of objects (Object[]).

You see that the actual array created in the main method is compatible with both types of the variables used to reference it throughout the code.

To overcome the array covariance, you could choose to use an ArrayList (or any other Collection you wish), but then, as you see, you would still have the generic type erasure problem on the Pair objects. like this:

public static void main(String[] args) {

    List<Pair<? extends Number, ? extends Number>> arr = 
        new ArrayList<Pair<? extends Number, ? extends Number>>(); 
        
    m1(arr);
        
    arr.add(new Pair<Integer, Integer>(1, 1));
    ...               
}
    
static void m1(List arr) {
    arr.add(new Pair<String, String>("test","test"));
}

What a maze, uh?!