17

Well, I have read a lot of answers to this question, but I have a more specific one. Take the following snippet of code as an example.

public class GenericArray<E>{
    E[] s= new E[5];
}

After type erasure, it becomes

public class GenericArray{
    Object[] s= new Object[5];
}

This snippet of code seems to work well. Why does it cause a compile-time error?

In addition, I have known from other answers that the following codes work well for the same purpose.

public class GenericArray<E>{
    E[] s= (E[])new Object[5];
}

I've read some comments saying that the piece of code above is unsafe, but why is it unsafe? Could anyone provide me with a specific example where the above piece of code causes an error?

In addition, the following code is wrong as well. But why? It seems to work well after erasure, too.

public class GenericArray<E>{
        E s= new E();
    }
davmac
  • 18,558
  • 1
  • 32
  • 55
Zhu Li
  • 473
  • 5
  • 15
  • 3
    @Maroun May I know whether you have read my post? The answers of the question to which you redirect mine do not solve my question in fact. – Zhu Li May 06 '15 at 12:55
  • 1
    @MarounMaroun this question is more about "why" we the compiler complains about generic arrays. Your link is about "how" to create them. I guess this is not a duplicate. – Tom May 06 '15 at 13:52
  • 4
    I reopened the question, thanks for clarifying. – Maroun May 06 '15 at 13:53
  • For `E[] s= (E[])new Object[5];`, did you try `String[] arr = new GenericArray().s;`? – Radiodef May 07 '15 at 16:05
  • 1
    @Radiodef I have tried it and now I have understood why this is unsafe. Thanks. – Zhu Li May 08 '15 at 02:38

4 Answers4

7

Array declarations are required to have a reifiable type, and generics are not reifiable.

From the documentation: the only type you can place on an array is one that is reifiable, that is:

  • It refers to a non-generic class or interface type declaration.

  • It is a parameterized type in which all type arguments are unbounded wildcards (§4.5.1).

  • It is a raw type (§4.8).

  • It is a primitive type (§4.2).

  • It is an array type (§10.1) whose element type is reifiable.

  • It is a nested type where, for each type T separated by a ".", T itself is reifiable.

This means that the only legal declaration for a "generic" array would be something like List<?>[] elements = new ArrayList[10];. But that's definitely not a generic array, it's an array of List of unknown type.

The main reason that Java is complaining about the you performing the cast to E[] is because it's an unchecked cast. That is, you're going from a checked type explicitly to an unchecked one; in this case, a checked generic type E to an unchecked type Object. However, this is the only way to create an array that is generic, and is generally considered safe if you have to use arrays.

In general, the advice to avoid a scenario like that is to use generic collections where and when you can.

Community
  • 1
  • 1
Makoto
  • 96,408
  • 24
  • 164
  • 210
  • Do you mean that the potential problem of `public class GenericArray{ E[] s= (E[])new Object[5]; }` is just that it may cause runtime ClassCastException? – Zhu Li May 07 '15 at 08:09
  • It *may*, yes. You're telling the type checking built into the language that you know what you're doing and you're going to only insert elements of type `E` into that `Object[]`. This operation is generally safe for arrays, but Java will still raise an unchecked warning letting you know that it can't safely check the type of objects making their way into the array. – Makoto May 07 '15 at 14:42
  • I have understood it now. Cast check doesn't actually work here due to type erasure. That's why it is unsafe. Thanks. – Zhu Li May 08 '15 at 02:46
3

This snippet of code seems to work well. Why does it cause a compile-time error?

First, because it would violate type safety (i.e. it is unsafe - see below), and in general code that can be statically determined to do this is not allowed to compile.

Remember that, due to type erasure, the type E is not known at run-time. The expression new E[10] could at best create an array of the erased type, in this case Object, rendering your original statement:

E[] s= new E[5];

Equivalent to:

E[] s= new Object[5];    

Which is certainly not legal. For instance:

String[] s = new Object[10];

... is not compilable, for basically the same reason.

You argued that after erasure, the statement would be legal, implying that you think this means that the original statement should also be considered legal. However this is not right, as can be shown with another simple example:

ArrayList<String> l = new ArrayList<Object>();

The erasure of the above would be ArrayList l = new ArrayList();, which is legal, while the original is clearly not.

Coming at it from a more philosophical angle, type erasure is not supposed to change the semantics of the code, but it would do so in this case - the array created would be an array of Object rather than an array of E (whatever E might be). Storing a non-E object reference in it would then be possible, whereas if the array were really an E[], it should instead generate an ArrayStoreException.

why is it unsafe?

(Bearing in mind we are now talking about the case where E[] s= new E[5]; has been replaced with E[] s = (E[]) new Object[5];)

It is unsafe (which in this instance is short for type unsafe) because it creates at run-time a situation in which a variable (s) holds a reference to an object instance which is not a sub-type of the variable's declared type (Object[] is not a subtype of E[], unless E==Object).

Could anyone provide me with a specific example where the above piece of code causes an error?

The essential problem is that it is possible to put non-E objects into an array that you create by performing a cast (as in (E[]) new Object[5]). For example, say there is a method foo which takes an Object[] parameter, defined as:

void foo(Object [] oa) {
    oa[0] = new Object();
}

Then take the following code:

String [] sa = new String[5];
foo(sa);
String s = sa[0]; // If this line was reached, s would
                  // definitely refer to a String (though
                  // with the given definition of foo, this
                  // line won't be reached...)

The array definitely contains String objects even after the call to foo. On the other hand:

E[] ea = (E[]) new Object[5];
foo(ea);
E e = ea[0];  // e may now refer to a non-E object!

The foo method might have inserted a non-E object into the array. So even though the third line looks safe, the first (unsafe) line has violated the constraints that guarantee that safety.

A full example:

class Foo<E>
{
    void foo(Object [] oa) {
        oa[0] = new Object();
    }

    public E get() {
        E[] ea = (E[]) new Object[5];
        foo(ea);
        return ea[0];  // returns the wrong type
    }
}

class Other
{
    public void callMe() {
        Foo<String> f = new Foo<>();
        String s = f.get();   // ClassCastException on *this* line
    }
}

The code generates a ClassCastException when run, and it is not safe. Code without unsafe operations such as casts, on the other hand, cannot produce this type of error.

In addition, the following code is wrong as well. But why? It seems to work well after erasure, too.

The code in question:

public class GenericArray<E>{
    E s= new E();
}

After erasure, this would be:

Object s = new Object();

While this line itself would be fine, to treat the lines as being the same would introduce the semantic change and safety issue that I have described above, which is why the compiler won't accept it. As an example of why it could cause a problem:

public <E> E getAnE() {
    return new E();
}

... because after type erasure, 'new E()' would become 'new Object()' and returning a non-E object from the method clearly violates its type constraints (it is supposed to return an E) and is therefore unsafe. If the above method were to compile, and you called it with:

String s = <String>getAnE();

... then you would get a type error at runtime, since you would be attempting to assign an Object to a String variable.

Further notes / clarification:

  • Unsafe (which is short for "type unsafe") means that it could potentially cause a run-time type error in code that would otherwise be sound. (It actually means more than this, but this definition is enough for purposes of this answer).
  • it's possible to cause a ClassCastException or ArrayStoreException or other exceptions with "safe" code, but these exceptions only occur at well defined points. That is, you can normally only get a ClassCastException when you perform a cast, an operation that inherently carries this risk. Similarly, you can only get an ArrayStoreException when you store a value into an array.
  • the compiler doesn't verify that such an error will actually occur before it complains that an operation is unsafe. It just knows that that certain operations are potentially able to cause problems, and warns about these cases.
  • that you can't create a new instance of (or an array of) a type parameter is both a language feature designed to preserve safety and probably also to reflect the implementation restrictions posed by the use of type erasure. That is, new E() might be expected to produce an instance of the actual type parameter, when in fact it could only produce an instance of the erased type. To allow it to compile would be unsafe and potentially confusing. In general you can use E in place of an actual type with no ill effect, but that is not the case for instantiation.
davmac
  • 18,558
  • 1
  • 32
  • 55
  • We can alway make a reference variable of type E refer to a non-E object if we intend to. For example: `String x; Object y=new Object(); x=(String)y; ` This is not considered as a compilation error, though it will cause runtime exception. But why is `E[] ea = new E[5]` not allowed in compilation since potential error will be found during runtime? – Zhu Li May 07 '15 at 06:36
  • Moreover, instantiation of an instance of parameter type is not allowed in Java, either. But why? I guess that the answer to this question is also the answer to my original question. – Zhu Li May 07 '15 at 06:40
  • @ZhuLi your example uses a cast. As I state above, code without casts is supposed to be safe, and that's why `E[] ea = new E[5]` is not allowed. Yes, the error would be found at runtime, but the error would be a ClassCastException and would occur on a line not containing a cast and probably in another (completely "innocent") class. Instantiation of the exact parameter type is not possible due to type erasure (see link given in my other comment above). – davmac May 07 '15 at 08:25
  • I think type erasure replace all Es with Object, so ` E[] ea = new E[5]` becomes `Object[] ea=new Object[5]` And the latter seems OK. As to cast, it is frequently used implicitly when generic type is used.("Insert type casts if necessary to preserve type safety." from the website you provide) Do you think that the implicit casts make usage of generic type unsafe too? – Zhu Li May 07 '15 at 08:34
  • @ZhuLi "... And the latter seems OK" - but my answer already explains why it's not ok. I have amended the answer to show a complete code example. And yes, the cast _can_ be (and often is) used in a way that is not problematic, but it has the _potential_ to cause an issue (as shown in my answer) and in particular it breaks the "golden rule" - that all code without casts is safe. Hence, it is unsafe. – davmac May 07 '15 at 08:38
  • `public class Foo { void foo(Object [] oa) { oa[0] = new Object(); } public String get() { String[] ea = new String[5]; foo(ea); return ea[0]; // returns the wrong type } public static void main(String[] args){ new Other().callMe(); } } class Other { public void callMe() { Foo f = new Foo(); String s = f.get(); } }` I have modified it in this way and it also causes runtime exception. What I mean is that the key leading to exception here is not `E[] ea = (E[]) new Object[5];` – Zhu Li May 07 '15 at 09:29
  • What I mean is that the key leading to exception here is not `E[] ea = (E[]) new Object[5];` but the tricky method `foo(Object[]);` There is a difference though. The runtime exception becomes ArrayStoreException. – Zhu Li May 07 '15 at 09:31
  • @ZhuLi, the fact that you remove `E[] ea = (E[]) new Object[5]` makes your revised example irrelevant to the original question. Although the line does not itself cause an exception, it allows for a later type error (ClassCastException) to occur even without the presence of a cast (as I show). That is what "unsafe" means (or to put it another way, "unsafe" code allows potentially having an object of the wrong type stored in a variable). Your revised example does not have this problem (although it certainly produces a different kind of error at runtime). – davmac May 07 '15 at 10:00
  • Well, what I want to show is that the tricky method `foo()` causes runtime exception regardless of whether `E[] ea = (E[]) new Object[5]` is used. So your example actually tells us to avoid using that kind of method, not a so-called generic array. If your example implies that we should avoid using `E[] ea = (E[]) new Object[5]`, it also implies that we, to some extent, should not use `String[] ea = new String[5];` that could not prevent error from occurring, either. – Zhu Li May 07 '15 at 10:18
  • @ZhuLi please read my full amended answer. It is of course the case that you will get an `ArrayStoreException` if you attempt to store the wrong kind of object into an array. But you will get the exception when you try to do the store, not at some later point in code (perhaps even in another class) that is otherwise sound. That is the difference. That is what makes one _unsafe_ and the other not. I have fully answered your original question. You seem to think "unsafe" means "could possibly cause an exception" but that is not the case, it means more than that. – davmac May 07 '15 at 10:51
  • So _unsafe_ here means that, we cannot locate runtime exception directly at the exact place where it happens and when the codes become more complicated it will be difficult for us to find the error? – Zhu Li May 07 '15 at 11:07
  • @ZhuLi "unsafe" is short for "type unsafe". Strictly speaking, It just means that type safety has been violated. In Java, the result can be a type-based runtime exception occurring at a later point, and yes, it might be difficult to find the root cause of the error, but that is not what makes it "unsafe" - It's "unsafe" because you violated type safety by making a variable hold a reference to an object that is not a subtype of the declared type of the variable. You need to stop trying to impose your preconceived notion of what "unsafe" means. – davmac May 07 '15 at 12:09
  • Now I guess I have got it. Your amended answer, especially **That is, new E() might be expected to produce an instance of the actual type parameter, when in fact it could only produce an instance of the erased type.** solves all my problems. Thank you so much for all your patience and time you spent dicussing with me. – Zhu Li May 07 '15 at 12:20
2

A compiler can use a variable of type Object to do anything a variable of type Cat can do. The compiler may have to add a typecast, but such typecast will either throw an exception or yield a reference to an instance of Cat. Because of this, the generated code for a SomeCollection<T> doesn't have to actually use any variables of type T; the compiler can replace T with Object and cast things like function return values to T where necessary.

A compiler cannot use an Object[], however, to do everything a Cat[] can do. If a SomeCollection[] had an array of type T[], it would not be able to create an instance of that array type without knowing the type of T. It could create an instance of Object[] and store references to instances of T in it without knowing the type of T, but any attempt to cast such an array to T[] would be guaranteed to fail unless T happened to be Object.

supercat
  • 69,493
  • 7
  • 143
  • 184
  • But Object[] can be cast to T[]. – Zhu Li May 07 '15 at 05:25
  • @ZhuLi: If a variable of type `Object[]` identifies an *instance* of type `T[]`, then it may be successfully cast. If a variable of type `Object` identifies an instance of `Object[]`, however, the cast will fail at runtime. – supercat May 07 '15 at 13:16
  • @ZhuLi: The Java Runtime could have been simplified if it had eliminated all array types except `primitiveArray` and `referenceArray` as well as all primitives shorter than `int`, and had the compiler generate suitable instructions for loading/storing things to/from an array based upon its compile-time type (so a variable of type `Cat[]` would hold a reference to a `referenceArray` object, and any time an element was read the system would confirm that it was really a `Cat` and throw an exception otherwise). Had it done so, then generic arrays would be no problem. As it is, though... – supercat May 07 '15 at 13:44
  • ...arrays have more specific types [a fact which allows data to be read from arrays without type-checking, but necessitates type-checking when writing]. In any case, generics can mostly deal with the situation by using arrays of type `Object`. – supercat May 07 '15 at 13:47
  • I guess have understood the problem better.Thanks. – Zhu Li May 08 '15 at 03:09
0

Let's say generic arrays are allowed in Java. Now, take a look at following code,

Object[] myStrs = new Object[2];
myStrs[0] = 100;  // This is fine
myStrs[1] = "hi"; // Ambiguity! Hence Error.

If user is allowed to create generic Array, then user can do as I've shown in above code and it will confuse compiler. It defeats the purpose of arrays (Arrays can handle only same/similar/homogeneous type of elements, remember?). You can always use array of class/struct if you want heterogeneous array.

More info here.

Radiodef
  • 35,285
  • 14
  • 78
  • 114
Abhishek
  • 6,114
  • 12
  • 46
  • 78
  • 4
    Your example doesn't make sense to me. Where do you see a _generic_ array in your example? And since the compiler is able to check the correct usage of for example a collection with a generic type, then he is also able to do the same for a generic array. – Tom May 06 '15 at 15:47
  • As `Object` is super class of all the classes in JAVA, I gave `Object` as datatype of array, which indirectly imply generic array. You can see `Object[] s= new Object[5];` statement in question about it. To addon, I wanted to give simple example to show why it is not possible. :) – Abhishek May 06 '15 at 16:15
  • 3
    *" I gave Object as datatype of array, which indirectly imply generic array"* what? And generic array accepts only its generic type and subtypes of it and `Object` accepts everything. They have not much in common. And the `Object[] s= new Object[5];` you mean in the question is _after_ the type erasure and _after_ the compiler checked that there is no such stuff like in your answer. Your example still doesn't make any sense. – Tom May 06 '15 at 16:43