通过未经检查的类型转换在 Java 中创建泛型数组

2022-09-02 09:46:19

如果我有一个泛型类,我不允许创建如下数组:Foo<Bar>

Bar[] bars = new Bar[];

(这将导致错误“无法创建 Bar 的泛型数组”)。

但是,正如dimo414在回答这个问题(Java如何:通用数组创建)时所建议的那样,我可以执行以下操作:

Bar[] bars = (Bar[]) new Object[];

(这将“仅”生成一个警告:“类型安全:未选中从 Object[] 到 Bar[]的强制转换”)。

在回应dimo414答案的评论中,有些人声称在某些情况下使用此构造会导致问题,而另一些人则说这很好,因为对数组的唯一引用是,它已经是所需的类型。bars

我有点困惑,在哪些情况下这是可以的,在哪些情况下它可能会让我陷入困境。例如,newacctAaron McDaid的评论似乎直接相互矛盾。不幸的是,原始问题中的评论流只是以未回答的“为什么这'不再正确'?”结束,所以我决定为它提出一个新问题:

如果 -array 只包含类型的条目,在使用数组或其条目时是否仍存在任何运行时问题?或者是唯一的危险,在运行时,我可以在技术上将数组转换为其他内容(如),这将允许我用非类型的值填充它?barsBarString[]Bar

我知道我可以使用,但我对上面的类型转换结构特别感兴趣,因为,例如,在GWT中,-选项不可用。Array.newInstance(...)newInstance(...)


答案 1

既然问题中提到了我,我就插话。

基本上,如果你不向类外部公开这个数组变量,它不会引起任何问题(有点像,在拉斯维加斯发生的事情留在拉斯维加斯。

数组的实际运行时类型是 。因此,将其放入类型的变量中实际上是一个“谎言”,因为它不是(除非是)的子类型。但是,如果这个谎言留在类内,则此谎言是可以的,因为该谎言被擦除到类内部(的下限在这个问题中。如果 的下限是其他内容,请将此讨论中出现的所有内容替换为该边界。但是,如果这个谎言以某种方式暴露在外部(最简单的例子是将变量直接返回为类型,那么它将导致问题。Object[]Bar[]Object[]Bar[]ObjectBarBarObjectBarObjectBarObjectbarsBar[]

要了解到底发生了什么,查看带有和不具有泛型的代码是有启发性的。任何泛型程序都可以重写为等效的非泛型程序,只需删除泛型并在正确的位置插入强制转换即可。这种转换称为类型擦除

我们考虑一个简单的实现,其中包含用于获取和设置数组中特定元素的方法,以及用于获取整个数组的方法:Foo<Bar>

class Foo<Bar> {
    Bar[] bars = (Bar[])new Object[5];
    public Bar get(int i) {
        return bars[i];
    }
    public void set(int i, Bar x) {
        bars[i] = x;
    }
    public Bar[] getArray() {
        return bars;
    }
}

// in some method somewhere:
Foo<String> foo = new Foo<String>();
foo.set(2, "hello");
String other = foo.get(3);
String[] allStrings = foo.getArray();

类型擦除后,这将变为:

class Foo {
    Object[] bars = new Object[5];
    public Object get(int i) {
        return bars[i];
    }
    public void set(int i, Object x) {
        bars[i] = x;
    }
    public Object[] getArray() {
        return bars;
    }
}

// in some method somewhere:
Foo foo = new Foo();
foo.set(2, "hello");
String other = (String)foo.get(3);
String[] allStrings = (String[])foo.getArray();

所以班级内部不再有演员了。但是,调用代码中存在强制转换 - 当获取一个元素并获取整个数组时。为了得到一个元素而进行的强制转换不应该失败,因为我们唯一可以放入数组的东西是,所以我们唯一能得到的东西也是。但是,在获取整个数组时,强制转换将失败,因为数组具有 实际的运行时类型 。BarBarObject[]

非一般地写,正在发生的事情和问题变得更加明显。特别令人不安的是,强制转换失败不会发生在我们用泛型编写强制转换的类中 - 它发生在使用我们类的其他人的代码中。而其他人的代码是完全安全和无辜的。在我们进行泛型代码转换的时候,它也不会发生 - 它发生在以后,当有人在没有警告的情况下调用时。getArray()

如果我们没有此方法,那么这个类将是安全的。使用此方法,它是不安全的。什么特性使它不安全?它返回为类型,这取决于我们之前所做的“谎言”。由于谎言不是真的,它会引起问题。如果该方法将数组作为类型返回,那么它将是安全的,因为它不依赖于“lie”。getArray()barsBar[]Object[]

人们会告诉你不要做这样的演员阵容,因为它会导致演员例外,如上所示,在意想不到的地方,而不是在未经检查的演员阵容的原始位置。编译器不会警告您不安全的内容(因为从它的角度来看,给定您告诉它的类型,它是安全的)。因此,这取决于程序员对这个陷阱要勤奋,而不是以不安全的方式使用它。getArray()

但是,我认为这在实践中并不是一个大问题。任何设计良好的 API 都不会向外部公开内部实例变量。(即使有一种方法可以将内容作为数组返回,它也不会直接返回内部变量;它会复制它,以防止外部代码直接修改数组。所以无论如何都不会像现在这样实现任何方法。getArray()


答案 2

与列表相反,Java 的数组类型是重新定义的,这意味着 的运行时类型与 不同。因此,当您编写Object[]String[]

Bar[] bars = (Bar[]) new Object[];

您已经创建了一个运行时类型的数组,并将其“强制拼接”到 。我在引号内说“cast”,因为这不是一个真正的检查-cast操作:它只是一个编译时指令,它允许您将一个分配给类型的变量。当然,这为各种运行时类型错误打开了大门。它是否真的会产生错误完全取决于你的编程能力和注意力。因此,如果您觉得可以接受它,那么这样做是可以的;如果您不这样做,或者此代码是具有许多开发人员的大型项目的一部分,那么这是一件危险的事情。Object[]Bar[]Object[]Bar[]