Covariant Subtyping for Arrays

Java's type system includes the following general rules:

If an expression E is of type T1, and T1 is a subtype of T2, then E is of type T2.

If an expression E is of type T1, and a variable x is of type T2, and T1 is a subtype of T2, then the assignment x=E is well-typed (that is, legal).

If T1 is a subtype of T2, then T1[] is a subtype of T2[].

The last of those rules is the covariant subtyping rule for Java's array type constructor. (It's covariant rather than contravariant because the subtyping relationship between T1[] and T2[] runs in the same direction as the subtyping relationship between T1 and T2.)

Covariant subtyping for the array type constructor improves reuse and makes programming easier because you can pass arguments of type T1[] to a method m that accepts arguments of type T2[] instead of having to write a second version of m that accepts arguments of type T1[].

Unfortunately, covariant subtyping of the array type constructor is unsound when arrays are mutable. Suppose T1 is a subclass of T2 and defines a method g that isn't defined by class T2. Then the following Java code will pass the type checker and compile just fine, but throws an exception at run time:

          void f (T2[] a) {
              T1 x = new T2();
              for (int i = 0; i < a.length; i = i + 1)
                  a[i] = x;
          }
      
          void troublesome () {
              T1[] a = new T1[1000];
              f(a);
              System.out.println("Something is about to go wrong!");
              a[0].g();
          }
    

To detect that run-time error and throw a clean exception, implementations of Java must implement a mechanism that checks every assignment that stores into an array of reference values.

If you modify the example above to use ArrayLists instead of arrays, the Java compiler will reject the program. The parametric polymorphism of ArrayList is not covariant.

For debugging: Click here to validate.