【问题标题】:Why does the JVM allow be to pass a B[] to a method that expects an A[]?为什么 JVM 允许将 B[] 传递给需要 A[] 的方法?
【发布时间】:2015-11-23 14:52:56
【问题描述】:

我有以下通用测试类:

public class BrokenGenerics<T> {

    private T[] genericTypeArray;

    public BrokenGenerics(T... initArray) {
        genericTypeArray = initArray;
    }

    public void setArray(T[] newArray) {
        genericTypeArray = newArray;
    }

    public T get(int idx) {
        return genericTypeArray[idx];
    }

    public Class getType() {
        return genericTypeArray.getClass().getComponentType();
    }



    public static boolean breakThis(BrokenGenerics any) {
        any.setArray(new B[]{new B(2)});
        return false;
    }

    public static void main(String[] args) {
        BrokenGenerics<A> aBreaker = new BrokenGenerics<A>(new A("1"));
        System.out.println(aBreaker.get(0));
        System.out.println(aBreaker.getType());
        breakThis(aBreaker);
        System.out.println(aBreaker.get(0));
        System.out.println(aBreaker.getType());
    }

    private static class A {

        public String val;

        public A(String init) {
            val = init;
        }

        @Override
        public String toString() {
            return "A value: " + val;
        }
    }

    private static class B {

        public int val;

        public B(int init) {
            val = init;
        }

        @Override
        public String toString() {
            return "B value: " + val;
        }
    }
}

当我运行它时,我得到了这个输出,并且没有错误

A value: 1
class BrokenGenerics$A
B value: 2
class BrokenGenerics$B

现在,我明白为什么会编译了;它在编译时无法知道 breakThis 正在传递一个错误类型的泛型。但是,一旦它运行any.setArray(new B[]{new B(2)}); 行,它不应该抛出一个ClassCastException(注意它不是!你自己试试吧!)因为我试图将B[] 传递给一个方法期望A[]然后,为什么它允许我get()B

【问题讨论】:

  • 我确定编译器会发出警告。它不能做太多其他事情,因为 T 在运行时是 Object
  • @Kayaman 编译器也不会发出任何警告。我的代码被绿了!在编译或运行时没有警告或错误。
  • @BenC.R.Leggiero 您不可能没有收到任何警告。至少,您应该收到public static boolean breakThis(BrokenGenerics any) { 的原始类型警告。
  • @Powerlord 你有没有把它放到你的IDE中并收到警告?也许我的 IDE 只是没有向我显示它们。
  • 好的。有道理。我现在也不相信这种双重性。如果您将不同类型的元素添加 在一起,您会得到ClassCastException,但在这种情况下,您将完全破坏数组。

标签: java generics


【解决方案1】:

Type Erasure 之后,T 将变成Object,因为您没有在T 上指定边界。因此,在运行时将任何类型的数组分配给genericTypeArray(现在是Object[] 类型)或调用函数setArray(...)(现在也接受Object[] 类型的参数)都没有问题。此外,您的 get(...) 方法只会返回一个 Object

当您以错误的类型期望访问数组中的元素时,麻烦就开始了,因为这可能会导致(隐式或显式)非法类型转换,例如将get(...) 返回的值分配给@987654336 类型的变量@。

如果您尝试对数组本身进行类型转换,您也可以获得运行时 ClassCastException,但是,根据我的经验,这种情况往往很少出现,尽管它可能很难找到或甚至了解它是否确实发生。您可以在下面找到一些示例。

所有泛型检查仅在编译时发生。如果您使用raw types,这些检查将无法严格执行,因此编译器可以做的最好的事情就是发出警告,让您知道您正在通过省略类型参数而放弃进行更有意义的检查的机会。

Eclipse 及其标准设置(可能还有带有正确标志的 java 编译器)为您的代码显示以下警告:

  • “类是原始类型”,您在其中定义 getType()(与您的问题有些无关)
  • “BrokenGenerics 是一种原始类型”,您在其中定义 breakThis(...)
  • “类型安全:方法setArray(Object[])属于原始类型 BrokenGenerics”,您在 breakThis(...) 中调用 setArray(...)

由于数组的非法类型转换导致 ClassCastException 的示例:

如果将数组暴露给外部世界,则可以在运行时获得 ClassCastExceptions(这通常是一件危险的事情,所以我尽量避免它)通过将以下内容添加到 BrokenGenerics&lt;T&gt;

public T[] getArray() {
    return genericTypeArray;
}

如果您随后将 main 方法更改为:

BrokenGenerics<A> aBreaker = new BrokenGenerics<A>(new A("1"));
A[] array = aBreaker.getArray();
System.out.println(array[0]);
System.out.println(aBreaker.getType());
breakThis(aBreaker);
array = aBreaker.getArray(); // ClassCastException here!
System.out.println(array[0]);
System.out.println(aBreaker.getType());

由于数组本身而不是其中一个元素的转换,您会在运行时在指定位置收到 ClassCastException。

如果您将变量 genericTypeArray 设置为 protected 并从使用固定类型参数子类化泛型类的代码中使用它,也会发生同样的事情:

private static class C extends BrokenGenerics<A> {

    public C(A... initArray) {
        super(initArray);
    }

    public void printFirst() {
        A[] result = genericTypeArray; // ClassCastException here!
        System.out.println(result[0]);
    }
}

要触发异常,请将以下内容添加到您的 main 方法中:

C cBreaker = new C(new A("1"));
cBreaker.printFirst();
breakThis(cBreaker);
cBreaker.printFirst();

想象一下这个案例出现在一个更大的项目中......你到底是怎么开始理解行代码可能会失败的?!? :) 特别是因为堆栈跟踪可能对试图找到实际上导致错误的 breakThis(...) 调用几乎没有帮助。

更深入的案例可以看some tests I did a little while back

【讨论】:

  • @BenC.R.Leggiero Java 数组并不是从它们所拥有的类型中抽象出来的,而是泛型是从它们所拥有的类型中抽象出来的。
  • @BenC.R.Leggiero 我每天都在学习有关泛型复杂细节的新知识。令人惊讶的是,擦除方法会产生多少奇怪的含义。这是我在 SO 上从 generics-ninjas 那里学到的东西,你可能也会感兴趣:stackoverflow.com/questions/17831896/…
  • 附带说明,作为一般规则,集合(CollectionListSetMap)与泛型相比比数组更适用。
  • @BenC.R.Leggiero 他们这样做了,事实上,通过使用原始类型访问集合,您可以像使用代码示例一样将它们炸毁。 :)
  • 数组具有的具体化可能......在泛型中很难解决。 Joshua Bloch 在 Effective Java: Second Edition, pg 119 中谈到了这一点。这是free chapter of the book posted online 的一部分。这恰好是关于泛型的章节。它还有一些其他关于泛型的有趣内容。
【解决方案2】:

它不应该抛出一个ClassCastException,因为我试图将B[] 传递给一个需要A[] 的方法吗?

没有。正如this post 解释的那样,您在

中调用setArray
public static boolean breakThis(BrokenGenerics any) {
    any.setArray(new B[]{new B(2)});
    return false;
}

在原始类型BrokenGenerics 的引用表达式上完成。当与原始类型交互时,所有对应的泛型参数都会被删除。所以setArray 实际上是在期待Object[]A B[] is a Object[].

为什么它允许我 get()B

假设你在问这个

System.out.println(aBreaker.get(0));

PrintStream#println(Object) 需要 Object,而不是 A。因此,编译器没有理由在此处插入强制转换。由于没有演员表,所以没有ClassCastException

如果你这样做了

A a = aBreaker.get(0);

或者有类似的方法

void println(A a) {}
...
println(aBreaker.get(0));

那么这些将导致ClassCastException。换句话说,编译器将在需要从泛型类型参数转换类型的任何地方插入强制转换 (checkcast)。 PrintStream#println 的情况并非如此。

同样,

System.out.println(aBreaker.getType());

甚至不涉及BrokenGenerics中声明的泛型参数

public Class getType() {...}

并且还返回一个原始类型的值Class。编译器没有理由将checkcast 添加到A

【讨论】:

  • 我想我的第二个问题用错了。我更感兴趣的是为什么return genericTypeArray[idx]; 行运行得很好。 get() 返回T,对吗?在运行时,难道它不知道当我打电话给get() 时,我想要一个A,但它给了我一个B?也就是说,我想我现在明白了,它只是抽象为 T (Object) 的超类型,这就是它起作用的原因。
猜你喜欢
  • 2010-11-04
  • 1970-01-01
  • 1970-01-01
  • 2020-04-15
  • 2021-06-07
  • 1970-01-01
  • 2021-08-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多