【问题标题】:Creating generic array in Java via unchecked type-cast通过未经检查的类型转换在 Java 中创建泛型数组
【发布时间】: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(...)-选项不可用。

【问题讨论】:

    标签: java arrays generics casting type-safety


    【解决方案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();
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-02-03
          • 2015-06-20
          • 2019-09-13
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多