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

时间:2013-07-24 10:53:27

标签: java arrays generics casting type-safety

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

Bar[] bars = new Bar[];

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

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

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

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

在回复 dimo414 的回答的评论中,有些人声称使用这种结构可能会在某些情况下引起问题而其他人说它没问题,因为对数组的唯一引用是{{1已经是所需类型的。

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

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

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

5 个答案:

答案 0 :(得分:20)

因为我在问题中提到过,所以我会插话。

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

数组的实际运行时类型为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()方法,那么这个类就是安全的。使用这种方法,它是不安全的。什么特征使它不安全?它返回bars作为类型Bar[],这取决于我们之前做出的“谎言”。由于谎言不正确,它会导致问题。如果该方法已将数组返回为类型Object[],那么它将是安全的,因为它不依赖于“谎言”。

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

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

答案 1 :(得分:3)

好的,我已经玩了这个构造了一点,它可能是一个真正的混乱。

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

  • Foo<Bar>内,我可以如图所示创建数组,并且可以正常使用它。这是因为(如果我理解正确的话)编译器会“删除”Bar - 类型,只需将其转换为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];
      

      工程...

    顺便说一下,

    none 会抛出任何编译时错误。

  • 现在,如果你有另一个泛型类,那么:

    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<...>而不是数组。

答案 2 :(得分:2)

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

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

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

答案 3 :(得分: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();
} 

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

答案 4 :(得分: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();
}