通过未经检查的类型转换在 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 ,这已经是所需的类型了。

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

如果bars-array 只包含Bar 类型的条目,那么在使用该数组或其条目时是否还会存在任何运行时问题?或者是唯一的危险,在运行时我可以在技术上将数组转换为其他东西(如String[]),然后我可以用Bar以外的类型的值填充它?

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

【问题讨论】:

【参考方案1】:

由于问题中提到了我,我会插话。

基本上,如果你不将这个数组变量暴露给类的外部,它不会造成任何问题。 (有点像,在维加斯发生的事情留在维加斯。)

数组的实际运行时类型是Object[]。所以将它放入Bar[] 类型的变量实际上是一个“谎言”,因为Object[] 不是Bar[] 的子类型(除非ObjectBar)。但是,如果这个谎言留在类中,它是可以的,因为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&lt;Bar&gt; 中,我可以创建如图所示的数组并正常使用它。这是因为(如果我理解正确的话)编译器“擦除”Bar-type 并简单地将其转换为 Object。所以基本上在Foo&lt;Bar&gt; 内部,你只是在处理Object[],这很好。

但如果你在Foo&lt;Bar&gt; 中有这样的函数,它提供了对数组的访问:

public Bar[] getBars(Bar bar) 
    Bar[] result = (Bar[]) new Object[1];
    result[0] = bar;
    return result;

如果您在其他地方使用它,您可能会遇到一些严重的问题。以下是一些疯狂的例子(大部分实际上是有道理的,但乍一看似乎很疯狂):

String[] bars = new Foo&lt;String&gt;().getBars("Hello World");

会导致java.lang.ClassCastException: [Ljava.lang.Object;无法转换为 [Ljava.lang.String;

for (String bar: new Foo&lt;String&gt;().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&lt;Bar&gt; 类中一切正常的原因是Bar[] 也将变成Object[],独立于Bar。因此,这相当于将数组转换为Object[],然后对其进行索引,然后将返回的条目转换回String

总的来说,在看到这个之后,我确信使用这种方式创建数组是一个非常糟糕的主意,除非你永远不会将数组返回到泛型类之外的任何地方。出于我的目的,我将使用 Collection&lt;...&gt; 而不是数组。

【讨论】:

旁白:有谁知道我如何在要点之后立即在代码块中启用语法突出显示?请随时编辑答案或让我知道。【参考方案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 上设置边界,就像在&lt;Bar extends Something&gt; 中一样,它将失败。这是因为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 中创建泛型数组的主要内容,如果未能解决你的问题,请参考以下文章

在Java中创建泛型类型的实例?

如何在颤振/飞镖中创建泛型类型的对象?

java创建泛型数组

未经检查的强制转换:尝试在同一方法中将 Int 或 String 强制转换为 T(泛型类型)

在 kotlin 中创建泛型类的新实例的正确方法是啥?

Java泛型