简体   繁体   English

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

[英]Creating generic array in Java via unchecked type-cast

If I have a generic class Foo<Bar> , I am not allowed to create an array as follows: 如果我有一个泛型类Foo<Bar> ,我不允许创建一个数组,如下所示:

Bar[] bars = new Bar[];

(This will cause the error "Cannot create a generic array of Bar"). (这将导致错误“无法创建Bar的通用数组”)。

But, as suggested by dimo414 in an answer to this question (Java how to: Generic Array creation) , I can do the following: 但是,正如dimo414在回答这个问题(Java如何:通用数组创建)中所建议的那样 ,我可以做到以下几点:

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

(This will "only" generate a warning: "Type safety: Unchecked cast from Object[] to Bar[]"). (这将“仅”生成警告:“类型安全:从Object []到Bar []”的未选中的强制转换。

In the comments responding to dimo414 's answer, some people claim that using this construct can cause problems in certain situations and others say it's fine, as the only reference to the array is bars , which is of the desired type already. 在回应dimo414的回答的评论中,有些人声称使用这种结构可能会在某些情况下引起问题而其他人说它很好,因为对数组的唯一引用是bars ,它已经是所需的类型。

I'm a little confused in which cases this is OK and in which cases it can run me into trouble. 我有点困惑,在哪些情况下这是可以的,在哪些情况下,它可以让我陷入困境。 The comments by newacct and Aaron McDaid , for example, seem to directly contradict each other. 例如, newacctAaron McDaid的评论似乎直接相互矛盾。 Unfortunately the comment stream in the original question simply ends with the unanswered "Why is this 'no longer correct'?", so I decided to make a new question for it: 不幸的是,原始问题中的评论流只是以未答复的“为什么这个'不再正确'?”结束,所以我决定为它提出一个新问题:

If the bars -array only ever contains entries of type Bar , could there still be any run-time issues when using the array or its entries? 如果bars只包含Bar类型的条目,那么在使用数组或其条目时是否仍然存在任何运行时问题? Or is the only danger, that at run-time I could technically cast the array to something else (like String[] ), which would then allow me to fill it with values of a type other than Bar ? 或者是唯一的危险,在运行时我可以在技术上将数组转换为其他东西(如String[] ),然后允许我用除Bar之外的类型的值填充它?

I know I can use Array.newInstance(...) instead, but I am specifically interested in the type-casting construct above, since, for example, in GWT the newInstance(...) -option isn't available. 我知道我可以使用Array.newInstance(...) ,但我对上面的类型转换构造特别感兴趣,因为,例如,在GWT中newInstance(...) option不可用。

Since I was mentioned in the question, I will chime in. 自从我在问题中被提及以来,我将会加入。

Basically, it will not cause any problems if you don't expose this array variable to the outside of the class. 基本上,如果不将此数组变量暴露给类的外部,它不会导致任何问题。 (kinda like, What happens in Vegas stays in Vegas.) (有点像,拉斯维加斯会发生什么事情留在拉斯维加斯。)

The actual runtime type of the array is Object[] . 数组的实际运行时类型是Object[] So putting it into a variable of type Bar[] is effectively a "lie", since Object[] is not a subtype of Bar[] (unless Object is Bar ). 因此将它放入Bar[]类型的变量实际上是一个“谎言”,因为Object[]不是Bar[]的子类型(除非ObjectBar )。 However, this lie is okay if it stays inside the class, since Bar is erased to Object inside the class. 但是,如果它留在类中,这个谎言是可以的,因为Bar在类中被擦除为Object (The lower bound of Bar is Object in this question. In a case where the lower bound of Bar is something else, replace all occurrences of Object in this discussion with whatever that bound is.) However, if this lie gets somehow exposed to the outside (the simplest example is returning the bars variable directly as type Bar[] , then it will cause problems. (在这个问题中, Bar的下限是Object 。在Bar的下限是其他的情况下,在此讨论中将所有出现的Object替换为绑定的内容。)但是,如果这个谎言以某种方式暴露给在外面(最简单的例子是将bars变量直接返回为Bar[]类型,然后它会导致问题。

To understand what is really going on, it is instructive to look at the code with and without generics. 要了解实际情况,使用和不使用泛型来查看代码是有益的。 Any generics program can be re-written into an equivalent non-generics program, simply by removing generics and inserting casts in the right place. 任何泛型程序都可以重写为等效的非泛型程序,只需删除泛型并在正确的位置插入强制转换。 This transformation is called type erasure . 这种转换称为类型擦除

We consider a simple implementation of Foo<Bar> , with methods for getting and setting particular elements in the array, as well as a method for getting the whole array: 我们考虑一个简单的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();

After type erasure, this becomes: 在类型擦除后,这变为:

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();

So there are no casts inside the class anymore. 因此,课程中不再有演员。 However, there are casts in the calling code -- when getting one element, and getting the entire array. 但是,调用代码中存在强制转换 - 获取一个元素并获取整个数组。 The cast to get one element should not fail, because the only things we can put into the array are Bar , so the only things we can get out are also Bar . 获取一个元素的强制转换不应该失败,因为我们可以放入数组的唯一东西是Bar ,所以我们唯一可以得到的东西也是Bar However, the cast when getting the entire array, that will fail, since the array has actual runtime type Object[] . 但是,获取整个数组时的强制转换会失败,因为数组具有实际的运行时类型Object[]

Written non-generically, what is happening and the problem become much more apparent. 非一般性地写,正在发生的事情和问题变得更加明显。 What is especially troubling is that the cast failure does not happen in the class where we wrote the cast in generics -- it happens in someone else's code that uses our class. 特别令人不安的是,在我们用泛型编写演员的类中不会发生演员失败 - 它发生在使用我们类的其他人的代码中。 And that other person's code is completely safe and innocent. 而其他人的代码是完全安全和无辜的。 It also does not happen at the time where we did our cast in the generics code -- it happens later, when someone calls getArray() , without warning. 它也不会发生在我们在泛型代码中进行演员的时候 - 当有人在没有警告的情况下调用getArray()时会发生这种情况。

If we didn't have this getArray() method, then this class would be safe. 如果我们没有这个getArray()方法,那么这个类就是安全的。 With this method, it is unsafe. 使用这种方法,它是不安全的。 What characteristic makes it unsafe? 什么特征使它不安全? It returns bars as type Bar[] , which depends on the "lie" we made earlier. 它返回bars Bar[] ,这取决于我们之前做的“谎言”。 Since the lie is not true, it causes problems. 由于谎言不正确,它会导致问题。 If the method had instead returned the array as type Object[] , then it would be safe, since it does not depend on the "lie". 如果该方法已经将数组作为Object[]类型返回,那么它将是安全的,因为它不依赖于“谎言”。

People will tell you to not do such a cast like this, because it causes cast exceptions in unexpected places as seen above, not in the original place where the unchecked cast was. 人们会告诉你不要做这样的演员,因为它会导致如上所示在意想不到的地方出现强制转换异常,而不是在未经检查的演员所在的原始地方。 The compiler will not warn you that getArray() is unsafe (because from its point of view, given the types you told it, it is safe). 编译器不会警告你getArray()是不安全的(因为从它的角度来看,鉴于你告诉它的类型,它是安全的)。 Thus it depends on the programmer to be diligent about this pitfall and not to use it in an unsafe way. 因此,程序员需要努力解决这个陷阱,而不是以不安全的方式使用它。

However, I would argue that this is not a big concern in practice. 但是,我认为这在实践中并不是一个大问题。 Any well-designed API will never expose internal instance variables to the outside. 任何设计良好的API都不会将内部实例变量暴露给外部。 (Even if there is a method to return the contents as an array, it would not return the internal variable directly; it would copy it, to prevent outside code from modifying the array directly.) So no method will be implemented like getArray() is anyway. (即使有一种方法将内容作为数组返回,它也不会直接返回内部变量;它会复制它,以防止外部代码直接修改数组。)因此不会像getArray()那样实现任何方法无论如何。

As opposed to lists, Java's array types are reified , which means that the runtime type of Object[] is distinct from String[] . 相对于列表,Java的阵列类型物化 ,这意味着的运行时类型 Object[]是从不同String[] Therefore, when you write 因此,当你写

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

you have created an array of runtime type Object[] and have "cast" it to Bar[] . 你已经创建了一个运行时类型Object[]的数组,并将它“转换”为Bar[] I say "cast" within quotes because this is not a real checked-cast operation: it is just a compile-time directive which allows you to assign an Object[] into a variable of type Bar[] . 我在引号中说“强制转换”因为这不是一个真正的check-cast操作:它只是一个编译时指令,它允许你将一个Object[]分配给一个Bar[]类型的变量。 Naturally, this opens the door to all kinds of runtime type errors. 当然,这为各种运行时类型错误打开了大门。 Whether it will actually create the errors is entirely up to your programming prowess and attentiveness. 它是否真的会产生错误完全取决于你的编程能力和专注力。 Therefore, if you feel up to it, then it is OK to do it; 因此,如果你能够做到,那就可以做到; if you don't or this code is a part of a larger project with many developers, then it is a dangerous thing to do. 如果你不这样做或者这个代码是许多开发人员的大项目的一部分,那么这是一件危险的事情。

Ok, I've played with this construct for a bit and it can be a REAL mess. 好吧,我已经使用了这个构造了一点,它可能是一个真正的混乱。

I think the answer to my question is: Everything works fine as long as you always handle the array as generic. 我认为我的问题的答案是:只要您始终将数组作为通用处理,一切正常。 But as soon as you try to treat it in a non-generic way, there's trouble. 但是一旦你尝试以非通用方式对待它,就会遇到麻烦。 Let me give a couple of examples: 让我举几个例子:

  • Inside Foo<Bar> , I can create the array as shown and work with it just fine. Foo<Bar>内部,我可以如图所示创建数组,并且可以正常使用它。 This is because (if I understand correctly) the compiler "erases" the Bar -type and simply turns it into Object everywhere. 这是因为(如果我理解正确的话)编译器“擦除” Bar type并简单地将它变成Object到处。 So essentially inside Foo<Bar> you are just handling an Object[] , which is fine. 所以基本上在Foo<Bar>里面你只是处理一个Object[] ,这很好。
  • But if you have a function like this inside Foo<Bar> , which provides access to the array: 但是如果你在Foo<Bar>有这样的函数,它提供对数组的访问:

     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; 将导致java.lang.ClassCastException:[Ljava.lang.Object; cannot be cast to [Ljava.lang.String; 无法转换为[Ljava.lang.String;

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

      will also cause the same java.lang.ClassCastException 也会导致相同的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. 即使我没有在任何地方将它分配给String [],也会导致java.lang.ClassCastException

    • Even 甚至

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

      will cause the same java.lang.ClassCastException ! 会导致相同的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"); 

Most of this makes sense, once you realize that, after type-erasure, the getBars(...) -function actually returns an Object[] , independent of Bar . 大多数情况都是有道理的,一旦你意识到,在类型擦除之后, getBars(...)函数实际上返回一个Object[] ,独立于Bar This is why you can't (at runtime) assign the return value to a String[] without generating an exception, even if Bar was set as String . 这就是为什么你不能(在运行时)将返回值赋给String[]而不生成异常,即使Bar被设置为String Why this prevents you to index into the array without first casting it back to an Object[] beats me, though. 为什么这会阻止你在没有先将它强制转换回Object[]情况下将数据编入索引。 The reason why things work fine in the Baz<Bar> -class is that the Bar[] there will also be turned into an Object[] , independent of Bar . Baz<Bar>类中,事情正常工作的原因是Bar[]也会变成Object[] ,独立于Bar Thus, this would be equivalent to casting the array to Object[] , then indexing it, and then casting the returned entry back to String . 因此,这相当于将数组转换为Object[] ,然后对其进行索引,然后将返回的条目转换回String

Overall, after seeing this, I'm definitely convinced that using this way to create arrays is a really bad idea, unless you don't ever return the array to anywhere outside your generic class. 总的来说,在看到这个之后,我肯定相信使用这种方式创建数组是一个非常糟糕的主意,除非你没有将数组返回到泛型类之外的任何地方。 For my purposes, I'll be using a Collection<...> instead of an array. 出于我的目的,我将使用Collection<...>而不是数组。

Everything works okay until you want to use something in that array as it would of type Bar (or whatever type you initialize the generic class with) and not as an Object which it really is. 一切正常,直到你想要使用该数组中的某些东西,就像Bar类型(或者你初始化泛型类的任何类型)而不是它真正的Object For example having the method: 例如,有这样的方法:

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

seems to work okay for all types. 似乎适用于所有类型。 If you change it to: 如果您将其更改为:

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

and call it with 并称之为

init("asdf");

it still works fine; 它仍然可以正常工作; but when you want to really use the real T[] array (which should be String[] in the above example): 但是当你想真正使用真正的T []数组(在上面的例子中应该是String [])时:

String[] strings = init("asfd");

then you have a problem, since Object[] and String[] are two distinct classes and what you have is Object[] so a ClassCastException is thrown. 那么你有一个问题,因为Object[]String[]是两个不同的类,你拥有的是Object[]所以抛出了ClassCastException

The problem arises more quickly if you try a bounded generic type: 如果您尝试有界泛型类型,问题会更快出现:

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

As a good practice, try avoiding using generics and arrays since they do not mix very well together. 作为一种好的做法,尝试避免使用泛型和数组,因为它们不能很好地混合在一起。

The cast: 演员:

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

is an operation that will happen at runtime. 是一个将在运行时发生的操作。 If the runtime type of Bar[] is anything other than Object[] then this will generate a ClassCastException . 如果Bar[]的运行时类型不是Object[]那么这将生成ClassCastException

Therefore, if you place bounds on Bar as in <Bar extends Something> it will fail. 因此,如果您在Bar上放置边界,如<Bar extends Something>那么它将失败。 This is because the runtime type of Bar will be Something . 这是因为Bar的运行时类型将是Something If Bar doesn't have any upper bounds then it's type will be erased to Object and the compiler will generate all the relevant casts for placing objects into the array or for reading from it. 如果Bar没有任何上限,那么它的类型将被删除为Object ,编译器将生成所有相关的转换,以便将对象放入数组或从中读取。

If you try to assign bars to something with a runtime type that isn't Object[] (eg String[] z = bars ) then the operation will fail. 如果您尝试将bars分配给运算符类型不是Object[]某些内容(例如String[] z = bars ),则操作将失败。 The compiler warns you about this usecase with the "unchecked cast" warning. 编译器通过“未经检查的强制转换”警告警告您此用例。 So the following will fail even though it compiles with a warning: 所以即使它编译警告,以下内容也会失败:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM