10

I'm using java generics and varargs.

If I use the following code, I'll get a ClassCastException, even though I'm not using casts at all.

Stranger yet, if I run this on Android (dalvik) no stack trace is included with the exception, and if I change the interface to abstract class, the exception variable e is empty.

The code:

public class GenericsTest {
    public class Task<T> {
        public void doStuff(T param, Callback<T> callback) {
            // This gets called, param is String "importantStuff"

            // Working workaround:
            //T[] arr = (T[]) Array.newInstance(param.getClass(), 1);
            //arr[0] = param;
            //callback.stuffDone(arr);

            // WARNING: Type safety: A generic array of T is created for a varargs parameter
            callback.stuffDone(param);
        }
    }

    public interface Callback<T> {
        // WARNING: Type safety: Potential heap pollution via varargs parameter params
        public void stuffDone(T... params);
    }

    public void run() {
        Task<String> task = new Task<String>();
        try {
            task.doStuff("importantStuff", new Callback<String>() {
                public void stuffDone(String... params) {
                    // This never gets called
                    System.out.println(params);
                }});
        } catch (ClassCastException e) {
            // e contains "java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;"
            System.out.println(e.toString());
        }
    }

    public static void main(String[] args) {
        new GenericsTest().run();
    }
}

If you run this, you'll get an ClassCastException that Object cannot be cast to String with stack trace pointing to invalid line number. Is this a bug in Java? I've tested it in Java 7 and Android API 8. I did workaround for it (commented out in the doStuff-method), but it seems silly to have to do it this way. If I remove varargs (T...), everything works OK, but my actual implementation kinda needs it.

Stacktrace from exception is:

java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
    at GenericsTest$1.stuffDone(GenericsTest.java:1)
    at GenericsTest$Task.doStuff(GenericsTest.java:14)
    at GenericsTest.run(GenericsTest.java:26)
    at GenericsTest.main(GenericsTest.java:39)
murgo
  • 344
  • 3
  • 11
  • 1
    Is there a chance you could provide a copy of the stacktrace. My suspicion is it is from the implicit cast that happens due to type erasure. – Matt Jan 30 '12 at 00:09
  • Added stack trace to the question. – murgo Jan 30 '12 at 00:30

3 Answers3

14

This is expected behaviour. When you use generics in Java, the actual types of the objects are not included in the compiled bytecode (this is known as type erasure). All types become Object and casts are inserted into the compiled code to simulate typed behaviour.

Additionally, varargs become arrays, and when a generic varargs method is called, Java creates an array of type Object[] with the method parameters before calling it.

Thus, your line callback.stuffDone(param); compiles as callback.stuffDone(new Object[] { param });. However, your implementation of the callback requires an array of type String[]. The Java compiler has inserted an invisible cast in your code to enforce this typing, and because Object[] cannot be cast to String[], you get an exception. The bogus line number you see is presumably because the cast doesn't appear anywhere in your code.

One workaround for this is to completely remove the generics from your Callback interface and class, replacing all types with Object.

grahamparks
  • 16,130
  • 5
  • 49
  • 43
  • Ok, figured it would be that. It's just weird that it gets compiled to new Object[] { param }, not new String[] { param } which would work. The compiler has all the information to do the better cast (it knows to cast it to String[] anyways). That and the broken line number in stack trace made me think it was Java bug. Removed the varargs, kept generics at my actual program. – murgo Jan 30 '12 at 01:19
  • @murgo: "The compiler has all the information..." No it doesn't. The array is created when you call the varargs function, i.e. in `doStuff`, when you call `callback.stuffDone(param);`. At that place, it does not know, either at compile time or runtime, what T would be. – newacct Jan 30 '12 at 02:04
1

grahamparks answer is correct. The mysterious typecast is normal behaviour. They are inserted by the compiler to ensure that the application is runtime typesafe in the face of possible incorrect use of generics.

If you are playing by the rules, this typecast will always succeed. It is failing because you have ignored / suppressed the warnings about unsafe use of generics. This is not a wise thing to do ... especially if you don't understand exactly understand what they mean, and whether they can be safely ignored.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • There were warnings, that are shown in the question. Still, figured that something was fishy, because I thought I'd be using the generics in a "working way". This should really be a compilation error rather than warning. – murgo Jan 30 '12 at 01:22
  • 1
    @murgo - They are warnings because there some cases where it is safe to ignore them. Indeed, there are cases where the best solution involves ignore / suppress the warning. – Stephen C Jan 30 '12 at 05:04
1

That's indeed due to type erasure, but the critical part here is varargs. They are, as already noted, implemented as table. So compiler is actually creating an Object[] to pack your params and hence later invalid cast. But there is a hack around it: if you're nice enough to pass a table as vararg, compiler will recognize it, not re-pack it and because you saved him some work it will let you run your code :-)

Try to run after following modifications:

public void doStuff(T[] param, Callback callback) {

and

task.doStuff(new String[]{"importantStuff"}, new Callback() {

wmz
  • 3,645
  • 1
  • 14
  • 22