通过未经检查的类型转换在 Java 中创建泛型数组
Posted
技术标签:
【中文标题】通过未经检查的类型转换在 Java 中创建泛型数组【英文标题】:Creating generic array in Java via unchecked type-cast 【发布时间】:2013-07-24 10:53:27 【问题描述】:如果我有一个泛型类Foo<Bar>
,则不允许创建如下数组:
Bar[] bars = new Bar[];
(这将导致错误“无法创建 Bar 的通用数组”)。
但是,正如 dimo414 在回答 this question (Java how to: Generic Array creation) 时所建议的,我可以执行以下操作:
Bar[] bars = (Bar[]) new Object[];
(这将“仅”生成警告:“类型安全:未经检查的从 Object[] 转换为 Bar[]”)。
在响应 dimo414 答案的 cmets 中,一些人声称使用此构造在某些情况下会导致问题,而另一些人则说这很好,因为对数组的唯一引用是 bars
,这已经是所需的类型了。
我有点困惑,在哪些情况下这是可以的,在哪些情况下可能会给我带来麻烦。例如,newacct 和 Aaron McDaid 的 cmets 似乎直接相互矛盾。不幸的是,原始问题中的评论流只是以未回答的“为什么这'不再正确'?”结束,所以我决定为它提出一个新问题:
如果bars
-array 只包含Bar
类型的条目,那么在使用该数组或其条目时是否还会存在任何运行时问题?或者是唯一的危险,在运行时我可以在技术上将数组转换为其他东西(如String[]
),然后我可以用Bar
以外的类型的值填充它?
我知道我可以改用Array.newInstance(...)
,但我对上面的类型转换结构特别感兴趣,因为例如,在 GWT 中,newInstance(...)
-选项不可用。
【问题讨论】:
【参考方案1】:由于问题中提到了我,我会插话。
基本上,如果你不将这个数组变量暴露给类的外部,它不会造成任何问题。 (有点像,在维加斯发生的事情留在维加斯。)
数组的实际运行时类型是Object[]
。所以将它放入Bar[]
类型的变量实际上是一个“谎言”,因为Object[]
不是Bar[]
的子类型(除非Object
是Bar
)。但是,如果这个谎言留在类中,它是可以的,因为Bar
在类中被擦除为Object
。 (在这个问题中,Bar
的下限是 Object
。如果Bar
的下限是别的东西,那么在这个讨论中将所有出现的Object
替换为任何该边界。)但是,如果这个谎言以某种方式暴露在外面(最简单的例子是直接将bars
变量作为Bar[]
类型返回,那么它会导致问题。
要了解实际发生的情况,查看带有和不带有泛型的代码会很有启发性。任何泛型程序都可以重写为等效的非泛型程序,只需删除泛型并在正确的位置插入强制转换即可。这种转换称为类型擦除。
我们考虑一个简单的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();
所以类内不再有演员表。但是,调用代码中有强制转换——当获取一个元素并获取整个数组时。获取一个元素的转换应该不会失败,因为我们可以放入数组的唯一内容是Bar
,所以我们唯一可以取出的内容也是Bar
。但是,获取整个数组时的强制转换会失败,因为数组具有实际的运行时类型Object[]
。
以非通用方式编写,正在发生的事情和问题变得更加明显。特别令人不安的是,转换失败不会发生在我们用泛型编写转换的类中——它发生在使用我们类的其他人的代码中。而那个人的代码是完全安全和无辜的。在我们对泛型代码进行强制转换时也不会发生这种情况——它会在以后有人调用getArray()
时发生,而没有警告。
如果我们没有这个getArray()
方法,那么这个类将是安全的。用这种方法,是不安全的。什么特点使它不安全?它以Bar[]
类型返回bars
,这取决于我们之前制作的“谎言”。由于谎言不是真的,它会引起问题。如果该方法将数组返回为 Object[]
类型,那么它将是安全的,因为它不依赖于“谎言”。
人们会告诉你不要进行这样的转换,因为它会在如上所示的意外位置导致转换异常,而不是在未经检查的转换所在的原始位置。编译器不会警告您 getArray()
不安全(因为从它的角度来看,鉴于您告诉它的类型,它是安全的)。因此,程序员必须认真对待这个陷阱,不要以不安全的方式使用它。
但是,我认为这在实践中并不是一个大问题。任何设计良好的 API 都不会将内部实例变量暴露给外部。 (即使有方法将内容作为数组返回,它也不会直接返回内部变量;它会复制它,以防止外部代码直接修改数组。)所以不会像getArray()
那样实现任何方法无论如何。
【讨论】:
嘿! :) 惊人的!真正思考类型擦除后整个 Foo 类的样子以及类型转换发生的确切位置是有帮助的。例如,我一直在(错误)假设(Bar)-casts 发生在 Foo 类内部而不是它被调用的地方。但这样肯定更有意义。不过,对我来说仍然有点奇怪的是,Object bar = foo.getArray()[0]
将失败,而for (Object bar: foo.getArray())
工作正常......但我想这只是由两个语句在哪里以及如何投射数组引起的......谢谢! +1,v'【参考方案2】:
与列表相反,Java 的数组类型是reified,这意味着Object[]
的运行时类型 不同于String[]
。因此,当你写
Bar[] bars = (Bar[]) new Object[];
您创建了一个运行时类型为Object[]
的数组并将其“转换”为Bar[]
。我在引号内说“cast”是因为这不是一个真正的检查强制转换操作:它只是一个编译时指令,它允许您将Object[]
分配给Bar[]
类型的变量。自然,这为各种运行时类型错误打开了大门。它是否真的会产生错误完全取决于您的编程能力和注意力。因此,如果您觉得可以,那么可以这样做;如果您不这样做,或者此代码是包含许多开发人员的大型项目的一部分,那么这样做是很危险的。
【讨论】:
【参考方案3】:好的,我已经用这个结构玩了一会儿,它可能真的是一团糟。
我认为我的问题的答案是:只要您始终将数组作为泛型处理,一切正常。但是,一旦您尝试以非通用方式对其进行处理,就会遇到麻烦。让我举几个例子:
在Foo<Bar>
中,我可以创建如图所示的数组并正常使用它。这是因为(如果我理解正确的话)编译器“擦除”Bar
-type 并简单地将其转换为 Object
。所以基本上在Foo<Bar>
内部,你只是在处理Object[]
,这很好。
但如果你在Foo<Bar>
中有这样的函数,它提供了对数组的访问:
public Bar[] getBars(Bar bar)
Bar[] result = (Bar[]) new Object[1];
result[0] = bar;
return result;
如果您在其他地方使用它,您可能会遇到一些严重的问题。以下是一些疯狂的例子(大部分实际上是有道理的,但乍一看似乎很疯狂):
String[] bars = new Foo<String>().getBars("Hello World");
会导致java.lang.ClassCastException: [Ljava.lang.Object;无法转换为 [Ljava.lang.String;
for (String bar: new Foo<String>().getBars("Hello World"))
也会导致同样的java.lang.ClassCastException
但是
for (Object bar: new Foo<String>().getBars("Hello World"))
System.out.println((String) bar);
工作...
这对我来说没有意义:
String bar = new Foo<String>().getBars("Hello World")[0];
也会导致 java.lang.ClassCastException,即使我没有将它分配给任何地方的 String[]。
偶数
Object bar = new Foo<String>().getBars("Hello World")[0];
会导致同样的java.lang.ClassCastException!
只有
Object[] temp = new Foo<String>().getBars("Hello World");
String bar = (String) temp[0];
工作...
顺便说一下,并且没有会引发任何编译时错误。
现在,如果你有另一个泛型类:
class Baz<Bar>
Bar getFirstBar(Bar bar)
Bar[] bars = new Foo<Bar>().getBars(bar);
return bars[0];
以下工作正常:
String bar = new Baz<String>().getFirstBar("Hello World");
这大部分是有道理的,一旦你意识到,在类型擦除之后,getBars(...)
-函数实际上返回一个Object[]
,独立于Bar
。这就是为什么您不能(在运行时)将返回值分配给 String[]
而不会产生异常,即使 Bar
被设置为 String
。但是,为什么这会阻止您在不先将其转换回Object[]
的情况下对数组进行索引,这让我感到震惊。在Baz<Bar>
类中一切正常的原因是Bar[]
也将变成Object[]
,独立于Bar
。因此,这相当于将数组转换为Object[]
,然后对其进行索引,然后将返回的条目转换回String
。
总的来说,在看到这个之后,我确信使用这种方式创建数组是一个非常糟糕的主意,除非你永远不会将数组返回到泛型类之外的任何地方。出于我的目的,我将使用 Collection<...>
而不是数组。
【讨论】:
旁白:有谁知道我如何在要点之后立即在代码块中启用语法突出显示?请随时编辑答案或让我知道。【参考方案4】:一切正常,直到你想在该数组中使用 Bar
类型的东西(或你初始化泛型类的任何类型),而不是真正的 Object
。比如有方法:
<T> void init(T t)
T[] ts = (T[]) new Object[2];
ts[0] = t;
System.out.println(ts[0]);
似乎适用于所有类型。如果您将其更改为:
<T> T[] init(T t)
T[] ts = (T[]) new Object[2];
ts[0] = t;
System.out.println(ts[0]);
return ts;
并调用它
init("asdf");
它仍然可以正常工作;但是当你想真正使用真正的 T[] 数组时(在上面的例子中应该是 String[]):
String[] strings = init("asfd");
那么你就有问题了,因为Object[]
和String[]
是两个不同的类,而你拥有的是Object[]
,所以会抛出ClassCastException
。
如果您尝试使用有界泛型类型,问题会更快出现:
<T extends Runnable> void init(T t)
T[] ts = (T[]) new Object[2];
ts[0] = t;
System.out.println(ts[0]);
ts[0].run();
作为一个好的做法,尽量避免使用泛型和数组,因为它们不能很好地混合在一起。
【讨论】:
【参考方案5】:演员:
Bar[] bars = (Bar[]) new Object[];
是一个将在运行时发生的操作。如果Bar[]
的运行时类型不是Object[]
,那么这将生成ClassCastException
。
因此,如果您在Bar
上设置边界,就像在<Bar extends Something>
中一样,它将失败。这是因为Bar
的运行时类型将是Something
。如果Bar
没有任何上限,那么它的类型将被擦除为Object
,编译器将生成所有相关的转换,以便将对象放入数组或从中读取。
如果您尝试将bars
分配给运行时类型不是Object[]
(例如String[] z = bars
)的对象,则操作将失败。编译器会通过“未经检查的强制转换”警告向您发出有关此用例的警告。因此,即使编译时出现警告,以下内容也会失败:
class Foo<Bar>
Bar[] get()
return (Bar[])new Object[1];
void test()
Foo<String> foo = new Foo<>();
String[] z = foo.get();
【讨论】:
以上是关于通过未经检查的类型转换在 Java 中创建泛型数组的主要内容,如果未能解决你的问题,请参考以下文章