20

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

Bar[] bars = new Bar[];

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

但是,正如dimo414在回答这个问题 (Java how to: Generic Array creation)中所建议的那样,我可以执行以下操作:

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

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

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

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

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

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

4

5 回答 5

24

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

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

数组的实际运行时类型是Object[]. 所以把它放入一个类型的变量Bar[]实际上是一个“谎言”,因为Object[]它不是Bar[](除非ObjectBar)的子类型。但是,如果它留在课堂内,这个谎言是可以的,因为Bar它会被擦除到Object课堂内。(这个问题的下限BarObject。如果下限Bar是别的东西,请用Object这个界限替换所有出现的讨论。)但是,如果这个谎言以某种方式暴露在外面(最简单的示例将bars变量直接作为 type返回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以 type的形式返回Bar[],这取决于我们之前所做的“谎言”。由于谎言不是真的,它会引起问题。如果该方法将数组返回为 type Object[],那么它将是安全的,因为它不依赖于“谎言”。

人们会告诉你不要进行这样的强制转换,因为它会在如上所示的意想不到的地方导致强制转换异常,而不是在未经检查的强制转换所在的原始位置。编译器不会警告你这getArray()是不安全的(因为从它的角度来看,给定你告诉它的类型,它是安全的)。因此,程序员必须对这个陷阱保持警惕,不要以不安全的方式使用它。

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

于 2013-07-25T04:50:50.583 回答
3

与列表相反,Java 的数组类型是reified 这意味着运行时类型Object[]不同于String[]. 因此,当你写

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

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

于 2013-07-24T11:12:01.870 回答
3

好的,我已经用这个结构玩了一段时间,它可能真的是一团糟。

我认为我的问题的答案是:只要您始终将数组作为通用处理,一切都可以正常工作。但是,一旦您尝试以非通用方式对其进行处理,就会遇到麻烦。让我举几个例子:

  • 在里面Foo<Bar>,我可以创建如图所示的数组并很好地使用它。这是因为(如果我理解正确的话)编译器会“擦除” Bar-type 并简单地将其变成Object任何地方。所以本质上Foo<Bar>你只是在处理一个Object[],这很好。
  • 但是如果你有一个这样的函数 inside 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(...)-function 实际上返回一个Object[],独立于Bar. 这就是为什么您不能(在运行时)将返回值分配给 aString[]而不会产生异常,即使Bar设置为String. 但是,为什么这会阻止您在不先将其转换回数组的情况下对数组进行索引,这让Object[]我很吃惊。在 - 类中工作正常的原因Baz<Bar>Bar[]there 也会变成一个Object[],独立于Bar。因此,这相当于将数组转换为Object[],然后对其进行索引,然后将返回的条目转换回String

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

于 2013-07-24T11:51:18.980 回答
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();
} 

作为一个好的做法,尽量避免使用泛型和数组,因为它们不能很好地混合在一起。

于 2013-07-24T11:34:45.197 回答
0

剧组:

  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();
}
于 2013-07-24T11:53:25.637 回答