Reputation: 65
This is the code: A simple ceneric class and trying to assign an integer to aa[0]
.
public class GenericTest<T> {
T [] aa = (T[]) new Object[2];
T bb;
public GenericTest(T x, T y) {
aa[0] = x; aa[1] = y;
System.out.println(aa[0] + " " + aa[1]); //OK
}
static public void main(String[] args) {
GenericTest<Integer> ll = new GenericTest<>(1,2);
ll.bb = 1; // OK
ll.aa[0] = 6; // ClassCastException from Object to Integer
}
}
Upvotes: 4
Views: 1542
Reputation: 65
This code works (printing the class inside the constructor....) and I don't know why
public class GenericTest<AnyType> {
public AnyType [] aa = (AnyType []) new Object[2];
public AnyType bb;
public GenericTest(AnyType x, AnyType y) {
aa[0]=x; aa[1]=y; System.out.println(aa.getClass());
System.out.println( aa[0]+" "+aa[1]); //OK
}
static public void main(String[] args) {
GenericTest<Integer> ll = new GenericTest<>(1,2);
ll.aa[0]= 6; // ClassCastException from Object to Integer
}
}
Upvotes: 0
Reputation: 140534
This line:
ll.aa[0] = 6;
fails because the compiler has inserted some casts. Because it knows that ll
is a GenericTest<Integer>
, it expected aa
to be an Integer[]
.
The compiler inserts a cast to this type before you can do anything with it: it is evaluated identically to:
((Integer[]) ll.aa)[0] = Integer.valueOf(6);
The problem is that ll.a
isn't an Integer[]
, it's an Object[]
, so this cast fails.
These casts are inserted even when you're doing something that could be applied to "any array", or even "any object": for example, ll.a.toString()
would have casts inserted, even though toString
is available on all subclasses of Object
, i.e. all objects.
T [] aa = (T[]) new Object[2];
As already pointed out by StephenC, this is an unchecked cast. Checked casts are instructions inserted into the bytecode, which statically check that the object is of a particular type. But, because T
is a type variable, there is no "particular type" that the compiler can choose to insert a checkcast
instruction on that assignment.
The correct way to deal with this is either:
Use a List<T>
instead of a T[]
as the field type. Generics and arrays don't play nicely together anyway, so you're better off sticking with generics.
You can use a fixed-length array, e.g. Arrays.asList(null, null)
, to mimick an array, insofar as you can only set the elements, not clear/add/remove etc.
Inject a T[]
(or a Supplier<T[]>
, or an IntFunction<T[]>
etc) into the constructor of the GenericTest
, in order that you push the burden of ensuring the correct type onto the caller:
public GenericTest(T x, T y, IntFunction<T[]> arrayFn) {
// ...
aa = arrayFn.apply(2); // Creates an array of the right length.
}
// Then...
GenericTest<Integer> ll = new GenericTest(1, 2, Integer[]::new);
ll.aa[0] = 6;
This will then be type-correct, because aa
was created using Integer[]::new
, i.e. it's an Integer[]
. The casts are still inserted, they just pass the type check.
Upvotes: 4
Reputation: 719616
In fact, the exception message is this:
java.lang.ClassCastException:
[Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
It is saying that it can't cast an Object[]
to an Integer[]
.
The root cause of is the initializer in:
T [] aa = (T[]) new Object[2];
That typecast is an unsafe typecast. And indeed the compiler tells you that something is wrong:
$ javac GenericTest.java
Note: GenericTest.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
Anyhow ... what is happening is that when you then do this:
ll.aa[0] = 6;
the JVM is trying to cast the ll.aa
to an Integer[]
... because that is what the static typing says that it should be. But it isn't an Integer[]
. It is an Object[]
. Since Object[]
is not assignment compatible with an Integer[]
that gives you a class cast exception.
(Why is it doing a hidden type cast? Well this is how the JVM ensures runtime type safety in the face of possible unsafe casts and the like!)
How to fix it?
Avoid using T[]
. Use List<T>
instead.
Unfortunately, if you have to use T[]
there is no easy fix. Basically arrays of a generic type parameter are difficult to create. You end up having to pass the Class
object for the parameter's actual class as an extra parameter. Something like this:
import java.lang.reflect.Array;
public class GenericTest<T> {
T [] aa;
T bb;
public GenericTest(Class<T> cls, T x, T y) {sy
aa = (T[]) Array.newInstance(cls, 2);
aa[0] = x; aa[1] = y;
System.out.println(aa[0] + " " + aa[1]); //OK
}
static public void main(String[] args) {
GenericTest<Integer> ll = new GenericTest<>(Integer.class, 1, 2);
ll.bb = 1; // OK
ll.aa[0] = 6; // ClassCastException from Object to Integer
}
}
There is still a warning about an unsafe typecast ... but in this case it is safe to suppress the warning.
For Java 8 onwards, there is another solution which involves passing a reference to the array constructor for Integer[]
; see Andy Turner's answer. This is cleaner than using reflection and calling Array.newInstance
, but you still have to pass an extra parameter to the constructor.
Upvotes: 6
Reputation: 103893
T [] aa = (T[]) new Object[2];
Because that line is broken. Your compiler warned you when you tried to compile it. As a general rule, if you get a compiler warning you do not understand, then do something else - java compiler warnings are best treated as: "Your code is completely broken and this is not going to work, but for the benefit of e.g. trying to start this application up because you want to test a completely different part of it, we'll just slap this broken stuff in a class file so you can move on, for now. Do not under any circumstances publish this to production until you fully understand this warning first."
In java, generics is a 100% compile-time show: Generics are compiler-checked documentation. At runtime, generics are mostly erased, and the few places where they remain, they are 'comments' as far as the runtime is concerned. The verifier and the runtime system never does any checks, ever. If javac allows it, the generics are now done being useful.
This in sharp contrast to arrays: Arrays are their own object and they DO know their 'component type'. Also, the compiler doesn't do all that much checking on them, but the runtime DOES. If you use some classfile hackery to put an Integer
in a List<String>
, the runtime will let you. However, you cannot use any amount of hackery to put an Integer in a String[]
. It just won't work - you end up with an ArrayStoreException
instead.
Because arrays actually know their type, at runtime, Integer[]
and Object[]
are different types (whereas at runtime, a List
is just a List
- the runtime doesn't know what generics are).
T
is Integer, and that variable is of type T[]
, therefore, it is Integer[]
, therefore, any interactions with aa
get you an implicit cast to Integer[]
. This is 100% analogous to this code:
List<String> list = new ArrayList<String>();
List raw = list; // legal, but gets you a warning.
raw.add(5);
System.out.println(list.get(0));
You can compile it. If you run it, the last line throws ClassCastException which is weird, there is no cast there at all! Ah, but there is. Because the compiler rewrote that for you, into this code (and remember, the runtime does not know generics, which is why it is rewritten like this):
List list = new ArrayList();
List raw = list;
raw.add(Integer.valueOf(5));
String $v = (String) list.get(0);
System.out.println($v);
You'd think: Wait, why cast it? There is a System.out.println(Object)
variant? But, that's just the rules. Java will immediately cast that because it can, after all, list
is a List<String>
which means its get(int idx)
method can be assumed to always return strings, so java will cast it as soon as possible even if not needed.
The exact same thing happens in your code: It sees T[]
, where T is bound to Integer
, so, aa
is cast to Integer[]
even if it wasn't needed. This cast fails at runtime because an object created as new Object[2]
is clearly not an Integer[]
. After all, I can invoke:
Integer[] x = ...;
System.out.println(x.getClass().getComponentType());
and this code is guaranteed to print java.lang.Integer
. If I can write code so that java.lang.Object
is printed, that would be broken, and yet that is exactly what would happen.
Do not, ever, write T[]
. It's as simple as that.
Arrays are low-level constructs you shouldn't be using, especially if the component type is non-primitive. If you MUST use them, then they should be relegated to internal implementation details, hidden behind a nicer API.
Do the casting in those wrappers.
This is precisely how ArrayList works. ArrayList has an array inside that holds your list elements (hence the name ArrayList). That is defined as Object[] storage = new Object[...];
. Not T[] storage = (T[]) new Object[..]
. Then, the get method of ArrayList does the casting:
public T get(int idx) {
// check if idx is within 0-size()...
return (T) storage[idx];
}
This cast does nothing (javac emits a warning that it has no idea what T might be and the runtime doesn't either, so nobody can check this), but this will all work at runtime (obviously: ArrayList works fine).
You need to do the same thing, and hide that array, if you must use it. Or better yet, don't do any of this stuff and use List<T>
instead. Arrays of non-primitive types are a blight: If you use them, you better have an excellent reason for it, and hide it as much as possible.
Upvotes: 1
Reputation: 121048
This is what happens when you use generics. Because generics are erased at runtime, compiler still needs to somehow be safe (after erasure) that things work correctly. Let's simplify this:
GenericTest<Integer> ll = new GenericTest<>(1,2);
ll.bb = 1; // OK
System.out.println(ll.aa.getClass());
The last line is going to be translated to:
28: getfield #7 // Field aa:[Ljava/lang/Object;
31: checkcast #42 // class "[Ljava/lang/Integer;"
notice the checkcast
. Since your T
was resolved as Integer
, means that the array must be Integer[]
too; when in reality it is Object []
. Compiler is trying to warn you btw when you do :
T [] aa = (T []) new Object[2];
because this is unsafe. In general, generic arrays are a major headache in java, imo.
Upvotes: 3