为什么在推断数组类型时 java 不是类型安全的?

Why isn't java typesafe when inferring array types?

我正在研究泛型,令我惊讶的是,以下代码可以编译:

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();
    }
}

我希望 T 可以推断为 BA 不扩展 B。那么为什么编译器不抱怨呢?

T 似乎被推断为 Object,因为我也可以传递 Generic<Object>

此外,当实际 运行 代码 时,它会在 a[0] = b.get(); 行抛出一个 ArrayStoreException

我没有使用任何原始泛型类型。我觉得如果 T 实际上被推断为 B.

,则可以通过编译时错误或至少警告来避免此异常

当使用 List<...> 等价物进一步测试时:

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());
}

这确实会产生错误:

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

更一般的情况也是如此:

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

编译器正确地识别出第一个 ? 可能比第二个 ? 在继承层次结构中更靠下。

例如如果第一个 ?B 而第二个 ?A 那么这不是类型安全的。


那么为什么第一个示例没有产生类似的编译器错误?这只是一个疏忽吗?还是有技术限制?

产生错误的唯一方法是显式提供类型:

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

除了 2005 年(泛型之前)的 this article,我通过自己的研究并没有真正找到任何东西,它谈到了数组协变的危险。

数组协方差似乎暗示了一种解释,但我想不出一个。


当前 jdk 是 1.8.0。0_91

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

T不是推断为B,而是推断为A。由于 B 扩展了 AB[]A[] 的子类型,因此方法调用是正确的。

与泛型相反,数组的元素类型在运行时可用(具体化)。所以当你尝试做

a[0] = b.get();

运行时环境知道 a 实际上是一个 B 数组,不能容纳 A.

这里的问题是 Java 正在动态扩展。数组自 Java 的第一个版本就存在,而泛型仅在 Java 1.5 中添加。通常,Oracle 会尝试使新的 Java 版本向后兼容,因此较早版本中出现的错误(例如数组协方差)不会在较新版本中得到纠正。

考虑这个例子:

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);
    }
}

如你所见,用于推断类型参数的签名是相同的,唯一不同的是 fArray() 只读取数组元素而不是写入它们,从而使 T -> A 推断在运行时完全合理。

并且编译器无法判断您的数组引用在方法的实现中将用于什么。