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

如果我有一个generics类Foo ,我不允许创建一个数组,如下所示:

 Bar[] bars = new Bar[]; 

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

但是,正如dimo414在回答这个问题(Java如何:通用arrays创建)中所建议的那样 ,我可以做到以下几点:

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

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

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

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

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

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

自从我在问题中提到我以来,我就会参与其中。

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

数组的实际运行时类型是Object[] 。 因此将它放入Bar[]类型的变量实际上是一个“谎言”,因为Object[]不是Bar[]的子类型(除非ObjectBar )。 但是,如果它留在类中,这个谎言是可以的,因为Bar在类中被擦除为Object 。 (在这个问题中, Bar的下限是Object 。在Bar的下限是其他的情况下,在此讨论中将所有出现的Object替换为绑定的内容。)但是,如果这个谎言以某种方式暴露给在外面(最简单的例子是将bars变量直接返回为Bar[]类型,然后它会引起问题。

要了解实际情况,使用和不使用generics来查看代码是有益的。 任何generics程序都可以重写为等效的非generics程序,只需删除generics并在正确的位置插入强制转换。 这种转换称为类型擦除

我们考虑一个简单的Foo ,包括获取和设置数组中特定元素的方法,以及获取整个数组的方法:

 class Foo { 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 foo = new Foo(); 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[]

一般而言,正在发生的事情和问题变得更加明显。 特别令人不安的是,在我们在generics中编写演员的类中不会发生演员失败 – 它发生在使用我们类的其他人的代码中。 而其他人的代码是完全安全和无辜的。 它也不会发生在我们在generics代码中进行演员的时候 – 它发生在稍后,有人在没有警告的情况下调用getArray()

如果我们没有这个getArray()方法,那么这个类就是安全的。 使用这种方法,它是不安全的。 什么特征使它不安全? 它返回bars Bar[] ,这取决于我们之前做的“谎言”。 由于谎言不正确,它会导致问题。 如果该方法已经将数组作为Object[]类型返回,那么它将是安全的,因为它不依赖于“谎言”。

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

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

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

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

  • Foo内部,我可以如图所示创建数组,并且可以正常使用它。 这是因为(如果我理解正确的话)编译器“擦除” Bar type并简单地将它变成Object到处。 所以基本上在Foo里面你只是处理一个Object[] ,这很好。
  • 但是如果你在Foo有这样的函数,它提供对数组的访问:

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

    如果你在其他地方使用它,你可能会遇到一些严重的问题。 这里有一些疯狂的例子(大部分实际上都有意义,但看起来很疯狂)你得到:

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

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

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

      也会导致相同的java.lang.ClassCastException

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

      作品…

    • 这是对我没有意义的一个:

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

      即使我没有在任何地方将它分配给String [],也会导致java.lang.ClassCastException

    • 甚至

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

      会导致相同的java.lang.ClassCastException

    • 只要

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

      作品…

    顺便说一句,这些都没有抛出任何编译时错误。

  • 现在,如果你有另一个generics类,但是:

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

    以下工作正常:

     String bar = new Baz().getFirstBar("Hello World"); 

大多数情况都是有道理的,一旦你意识到,在类型擦除之后, getBars(...)函数实际上返回一个Object[] ,独立于Bar 。 这就是为什么你不能(在运行时)将返回值赋给String[]而不生成exception,即使Bar被设置为String 。 然而,为什么这会阻止你在没有先将其转换回Object[]情况下将数据编入索引。 在Baz -class中工作正常的原因是Bar[]也会变成Object[] ,独立于Bar 。 因此,这相当于将数组转换为Object[] ,然后对其进行索引,然后将返回的条目转换回String

总的来说,在看到这个之后,我肯定相信使用这种方式创建数组是一个非常糟糕的主意,除非你没有将数组返回到generics类之外的任何地方。 出于我的目的,我将使用Collection<...>而不是数组。

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

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

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

一切正常,直到你想在那个数组中使用某些东西,就像Bar类型(或者你用它来初始化generics类的任何类型)那样,而不是它真正的Object 。 例如,有这样的方法:

  void init(T t) { T[] ts = (T[]) new Object[2]; ts[0] = t; System.out.println(ts[0]); } 

似乎适用于所有类型。 如果您将其更改为:

  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

如果您尝试有界generics类型,问题会更快出现:

  void init(T t) { T[] ts = (T[]) new Object[2]; ts[0] = t; System.out.println(ts[0]); ts[0].run(); } 

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

演员阵容:

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

是一个将在运行时发生的操作。 如果Bar[]的运行时类型不是Object[]那么这将生成ClassCastException

因此,如果您在Bar上放置边界,如那么它将失败。 这是因为Bar的运行时类型将是Something 。 如果Bar没有任何上限,那么它的类型将被擦除为Object ,编译器将生成所有相关的转换,以便将对象放入数组或从中读取。

如果您尝试将bars分配给运算符类型不是Object[]某些内容(例如String[] z = bars ),则操作将失败。 编译器会通过“未经检查的强制转换”警告向您发出有关此用例的警告。 所以即使它编译警告,以下内容也会失败:

 class Foo { Bar[] get() { return (Bar[])new Object[1]; } } void test() { Foo foo = new Foo<>(); String[] z = foo.get(); }