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

20

如果我有一个泛型类Foo<Bar>,我不能按照以下方式创建数组:

Bar[] bars = new Bar[];

这将导致错误"Cannot create a generic array of Bar"。

但是,如此问题(Java how to: Generic Array creation)dimo414所建议的,我可以采取以下措施:

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

(这将“仅仅”产生一个警告:“类型安全:从Object[]到Bar[]的未经检查的转换”。)

在回答dimo414的答案时的评论中,一些人声称在某些情况下使用这个结构可能会导致问题,而另一些人则表示这是可以接受的,因为数组的唯一引用是已经具有所需类型的bars。

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

如果bars数组只包含类型为Bar的条目,那么在使用该数组或其条目时是否仍然可能存在运行时问题?或者唯一的危险是,在运行时我可以在技术上将数组强制转换为其他类型(如String []),这将允许我填充其他类型的值而不是Bar?

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

5个回答

25

由于我在问题中被提到,我会发表一下我的看法。

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

该数组的实际运行时类型是Object[]。因此,将其放入Bar[]类型的变量中实际上是一种“谎言”,因为除非ObjectBar,否则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()方法,那么这个类就是安全的。有了这个方法,它就是不安全的。是什么特征使其不安全?它将 bars 作为类型 Bar [] 返回,这取决于我们之前所说的“谎言”。由于这个谎言不是真的,所以会引起问题。如果该方法改为以 Object [] 类型返回数组,则它将是安全的,因为它不依赖于这个“谎言”。
人们会告诉你不要像这样进行转换,因为它会在意想不到的地方引起转换异常,就像上面所看到的那样,并不是在未经检查的转换发生的原始位置。编译器不会警告您 getArray()不安全(因为从它的角度来看,根据您告诉它的类型,它是安全的)。因此,这取决于程序员勤奋地避免这种陷阱并不以不安全的方式使用它。
然而,我认为实际中并不是一个大问题。任何设计良好的API都不会将内部实例变量暴露给外部。 (即使有一种方法可以将内容作为数组返回,它也不会直接返回内部变量;它会对其进行复制,以防止外部代码直接修改数组。)因此,任何方法都不会像 getArray()一样实现。

嘿! :) 太棒了!实际上思考类型擦除后整个Foo类的样子以及类型转换发生的确切位置是很有帮助的。例如,我曾经错误地认为(Bar)-casts发生在Foo类内部而不是调用它的地方。但这样做确实更有意义。然而,仍然有一点奇怪的是,Object bar = foo.getArray()[0]会失败,而for (Object bar: foo.getArray())却可以正常工作...但我想这只是由于两个语句转换数组的位置和方式不同造成的...谢谢!+1,v´ - Markus A.

4
与列表不同,Java的数组类型是具体化的,这意味着Object[]的运行时类型与String[]不同。因此,当您编写以下代码时:
Bar[] bars = (Bar[]) new Object[];

您创建了一个运行时类型为Object[]的数组,并将其“转换”为Bar[]。我在引号中说“转换”,因为这不是真正的检查转换操作:它只是一条编译时指令,允许您将Object[]分配给类型为Bar[]的变量。自然而然地,这打开了各种运行时类型错误的大门。它是否会创建错误完全取决于您的编程能力和专注力。因此,如果您感觉自己有能力,那么可以这样做;如果您没有能力或者这段代码是一个有许多开发人员的更大的项目的一部分,那么这是一件危险的事情。


3

好的,我已经对这个结构进行了一些测试,它可能会非常混乱。

我认为我的问题的答案是:只要你始终将数组视为通用形式处理,一切都可以正常工作。但是,一旦你试图以非通用的方式处理它,就会出现麻烦。让我举几个例子:

  • Inside Foo<Bar>, I can create the array as shown and work with it just fine. This is because (if I understand correctly) the compiler "erases" the Bar-type and simply turns it into Object everywhere. So essentially inside Foo<Bar> you are just handling an Object[], which is fine.
  • But if you have a function like this inside Foo<Bar>, which provides access to the array:

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

    you can run into some serious issues, if you use it somewhere else. Here are some examples of the craziness (most of it actually makes sense, but it seems crazy at first sight) you get:

    • String[] bars = new Foo<String>().getBars("Hello World");

      will cause java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;

    • for (String bar: new Foo<String>().getBars("Hello World"))

      will also cause the same java.lang.ClassCastException

    • but

      for (Object bar: new Foo<String>().getBars("Hello World"))
          System.out.println((String) bar);
      

      works...

    • Here's one that doesn't make sense to me:

      String bar = new Foo<String>().getBars("Hello World")[0];
      

      will cause the java.lang.ClassCastException, too, even though I'm not assigning it to a String[] anywhere.

    • Even

      Object bar = new Foo<String>().getBars("Hello World")[0];
      

      will cause the same java.lang.ClassCastException!

    • Only

      Object[] temp = new Foo<String>().getBars("Hello World");
      String bar = (String) temp[0];
      

      works...

    and none of these throw any compile-time errors, by the way.

  • Now, if you have another generic class, though:

    class Baz<Bar> {
        Bar getFirstBar(Bar bar) {
            Bar[] bars = new Foo<Bar>().getBars(bar);
            return bars[0];
        }
    }
    

    The following works just fine:

    String bar = new Baz<String>().getFirstBar("Hello World");
    
一旦您意识到类型擦除后,getBars(...)函数实际上返回一个独立于Bar对象的Object[],大多数情况都会让人感觉合理。因此,即使将Bar设置为String,在运行时也无法将返回值分配给String[]而不生成异常。至于为什么这会防止您在未先将其转换回Object[]的情况下索引数组,就令我困惑了。在Baz<Bar>类中的原因是,Bar[]也会被转换为独立于Bar对象的Object[]。因此,这相当于将数组强制转换为Object[],然后对其进行索引,然后将返回的条目强制转换回String
总体而言,在看到这个之后,我绝对相信使用这种方式创建数组是一种非常糟糕的想法,除非您永远不会将数组返回到泛型类以外的任何地方。出于我的目的,我将使用Collection<...>代替数组。

附注:有人知道我如何在紧随项目符号后的代码块中启用语法突出显示吗?请随意编辑答案或告诉我。 - Markus A.

0

一切都正常工作,直到您想使用数组中的某个内容,就像它是类型为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();
} 

作为最佳实践,请尽量避免同时使用泛型和数组,因为它们不是很兼容。

0

演员阵容:

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

Bar[] 是在运行时发生的操作。如果 Bar[] 的运行时类型与 Object[] 不同,则会生成 ClassCastException

因此,如果像 <Bar extends Something> 这样对 Bar 进行限制,它将失败。这是因为 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();
}

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