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

12

我在尝试泛型的时候发现,令我惊讶的是,以下代码可以编译通过:

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被推断为B,但A没有扩展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年的文章(在泛型之前)这篇文章,讲述了数组协变的危险。

数组协变似乎暗示了一个解释,但我想不出来。


当前jdk版本为1.8.0.0_91。


我在想你是否遇到了Java中泛型类型擦除的问题?这只是一个想法,我现在没时间仔细看。 - Ukko
2个回答

5
考虑下面这个示例:

假设有以下代码:

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推断变得完全合理。而编译器无法确定方法实现中将使用何种数组引用。

@JornVernee,是的,我有些过度了,但是你能够得到这个概念。当T -> A满足所有条件时,编译器选择'T-> B'没有任何理由。 - biziclop
是的,这很不错。但重点是你想通过说“?”必须始终扩展T(即数组类型)来添加额外的类型安全性。就像你做A a = new A(); A a1 = a;而不是Object a = new A(); A a2 = (A) a;有时第二种方法也可以正常运行(进行强制转换),但为了确保我们使用第一种方式,因为它具有类型安全性。 - Jorn Vernee
@JornVernee 好的,现在怎么样? :) - biziclop

2
我希望T被推断为B。 A并未扩展B。那么编译器为什么不对此发出投诉呢?
T没有被推断为B,而是被推断为A。由于B扩展了A,因此B[]是A[]的子类型,因此方法调用是正确的。
与泛型相反,数组的元素类型在运行时可用(它们是实体化的)。因此,当你尝试进行以下操作时:
a[0] = b.get();

运行环境知道a实际上是一个B数组,不能容纳A
问题在于Java具有动态扩展性。数组从Java的第一个版本就存在,而泛型仅在Java 1.5中添加。通常,Oracle试图使新的Java版本向后兼容,因此早期版本中出现的错误(例如数组协变)在较新的版本中不会被更正。

T没有被推断为Object,因为这样你就无法将Generic<A>传递给你的方法,因为Generic<A>不是Generic<Object>的子类型。当然,如果你实际上传递了一个Generic<Object>而不是一个Generic<A>,那么T会被推断为Object - Hoopje
如果 TObject,那么 Generic<A> 满足 Generic<? extends T>。这一点可以通过调用 Main.<Object>fArray(new B[1], new Generic<A>(new A())); 来进一步证明,该调用可以编译通过而没有错误。 - Jorn Vernee
是的,你说得对。我错过了? extends。抱歉。关键是,T不能被推断为B,因为A不是B的子类。所以我不明白你期望T被推断为B的想法。 - Hoopje
当然有解决方案。但是就像我所演示的那样,它会破坏类型安全,因此不应该被允许。 - Jorn Vernee
是的,它破坏了类型安全。正如我在我的答案中所说的那样,这是因为较新的Java版本尝试不破坏旧代码。在Java 1.0时代,将数组协变似乎是一个好主意,因为你可以编写“通用”方法,如void f(Object[] arr),并仍然使用任何数组类型调用它们。现在我们知道得更多了,但我们必须接受它。 - Hoopje
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接