Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why isn't java typesafe when inferring array types?

I was playing around with generics and found that, to my surprise, the following code compiles:

class A {}
class B extends A {}

class Generic<T> {
    private T instance;
    public Generic(T instance) {
        this.instance = instance;
    }
    public T get(){ return instance; }
}

public class Main {
    public static void main(String[] args) {
        fArray(new B[1], new Generic<A>(new A())); // <-- No error here
    }

    public static <T> void fArray(T[] a, Generic<? extends T> b) {
        a[0] = b.get();
    }
}

I would expect T to be inferred to B. A does not extend B. So why doesn't the compiler complain about it?

T seems to be inferred to Object, since I can pass a Generic<Object> as well.

Moreover, when actually running the code, it throws an ArrayStoreException on the a[0] = b.get(); line.

I'm not using any raw generic types. I feel like this exception could have been avoided with a compile time error, or at least a warning, if T was actually inferred to be B.


When further testing with the List<...> equivalent:

public static void main(String[] args) {
    fList(new ArrayList<B>(), new Generic<A>(new A())); // <-- Error, as expected
}

public static <T> void fList(List<T> a, Generic<? extends T> b) {
    a.add(b.get());
}

This does produce and error:

The method fList(List<T>, Generic<? extends T>) in the type Main is not applicable for the arguments (ArrayList<B>, Generic<A>)

As does the more generic case:

public static <T> void fList(List<? extends T> a, Generic<? extends T> b) {
    a.add(b.get()); // <-- Error here
}

The compiler correctly recognizes that the first ? might be further down the inheritance hierarchy than the second ?.

e.g. If the first ? were B and the second ? were A then this isn't type safe.


So why doesn't the first example produce a similar compiler error? Is it just an oversight? Or is there a technical limitation?

The only way I could produce an error is by explicitly providing a type:

Main.<B>fArray(new B[1], new Generic<A>(new A())); // <-- Not applicable

I didn't really find anything through my own research, except for this article from 2005 (before generics), which talks about the dangers of array covariance.

Array covariance seems to hint towards an explanation, but I can't think of one.


Current jdk is 1.8.0.0_91

like image 997
Jorn Vernee Avatar asked May 04 '16 10:05

Jorn Vernee


2 Answers

Consider this example:

class A {}
class B extends A {}

class Generic<T> {
    private T instance;
    public Generic(T instance) {
        this.instance = instance;
    }
    public T get(){ return instance; }
}

public class Main {
    public static void main(String[] args) {
        fArray(new B[1], new Generic<A>(new A())); // <-- No error here
    }

    public static <T> void fArray(T[] a, Generic<? extends T> b) {
        List<T> list = new ArrayList<>();
        list.add(a[0]);
        list.add(b.get());
        System.out.println(list);
    }
}

As you can see, the signatures used to infer the type parameters are identical, the only thing that's different is that fArray() only reads array elements instead of writing them, making the T -> A inference perfectly justifiable at runtime.

And there is no way for the compiler to tell what your array reference will be used for in the implementation of the method.

like image 146
biziclop Avatar answered Sep 20 '22 14:09

biziclop


I would expect T to be inferred to B. A does not extend B. So why doesn't the compiler complain about it?

T is not inferred to be B, it is inferred to be A. Since B extends A, B[] is a subtype of A[], and therefore the method call is correct.

Contrary to generics, the element type of an array is available at runtime (they are reified). So when you try to do

a[0] = b.get();

the runtime environment knows that a is actually a B array and cannot hold an A.

The problem here is that Java is extending dynamically. Arrays are there since the first version of Java, while generics were added only in Java 1.5. In general, Oracle tries to make new Java versions backward compatible, and therefore errors made in earlier versions (the array covariance, for example) are not corrected in newer versions.

like image 37
Hoopje Avatar answered Sep 18 '22 14:09

Hoopje