Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do I get a ClassCastException using generics in this case?

Tags:

java

generics

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

    }
}
like image 255
Oscar Avatar asked Dec 13 '22 07:12

Oscar


2 Answers

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.

like image 109
Stephen C Avatar answered Jan 22 '23 23:01

Stephen C


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.

like image 37
Eugene Avatar answered Jan 22 '23 21:01

Eugene