2

I got into Java Generics just recently and wanted to replicate the JavaScript map function you can use with Arrays. Now I can't figure out what's going wrong in my code:

public class Test {

public interface Function<T> {
    public T call(T... vals);
}

public <E> E[] map(E[] array, Function<E> func) {
    for (int i = 0; i < array.length; i++) {
        array[i] = func.call(array[i]);    <--- Exception
    }
    return array;
}

public Test() {
    Integer foo[] = {3, 3, 4, 9};

    foo = map(foo, new Function<Integer>() {
        @Override
        public Integer call(Integer... vals) {
            return vals[0] += 2;
        }
    });
    for (Integer l : foo) {
        System.out.println(l);
    }
}

public static void main(String[] args) {
    new Test();
}
}

I am encountering a ClassCastException at the specified line:

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;

I'm not casting the Integer Array back to a Object Array anywhere and I can't really figure out what is going wrong since in that line the array is still an Integer Array and it never gets into the anonymous method.

Sorry if this is a question I should easily find solutions for, I tried but I found none. Also if you downvote it please tell me why.

  • why do you need to have `T... vals`? – Lino May 14 '18 at 14:52
  • Because I want to be able to use it multipurposely – kartoffelx86 May 14 '18 at 14:54
  • 1
    Possible duplicate of [How to create a generic array in Java?](https://stackoverflow.com/questions/529085/how-to-create-a-generic-array-in-java) – Lino May 14 '18 at 14:55
  • 5
    short answer, you can't create generic arrays, and that is why the compiler creates an `Object[]` and tries to cast that to `Integer[]` but arrays are not polymorphic and thus the `ClassCastException` – Lino May 14 '18 at 14:56
  • you should have received a warning – newacct May 28 '18 at 04:01

1 Answers1

3

The practical answer

Generics and arrays, and generics and varargs, don't mix well in Java. Arrays are not used much in Java anyway; in Java it's much more common to use a List<Integer> than an Integer[] and doing so will avoid all the pain you're about to hear about. So for practical programs, just don't use generics and arrays, or generics and varargs, together and you'll be fine.

Don't read any further than this! Just use List<T> instead of T[] and don't use varags with generics!

You've been warned.

The technical answer

Let's unpack the problem function a bit. We could rewrite it more explicitly as:

public <E> E[] map(E[] array, Function<E> func) {
  E e = array[0];
  E res = func.call(e);
  array[0] = res;
  return array;
}

If you step through this function in a debugger, you'll see that the line E res = func.call(e); is throwing the exception, but that we never even get to the body of the function call.

To understand why, you have to understand how arrays, generics, and varargs work (or don't work) together. call is declared as public T call(T... vals). In Java that's syntactic sugar that means two things:

  1. call actually has the type T call(T[] vals)
  2. Any callsite call(T t1, T t2, /*etc*/) should be transformed into call(new T[]{t1, t2, /*etc*/}), implicitly building an array in the caller's code before calling the method.

If instead of using a type variable like T in the declaration of call we'd used a plain type like Integer or something like that, this would be the end of the story. But since it's a type variable, we have to understand a bit more about the interplay between generics and arrays.

Generics and arrays

Arrays in Java carry around some runtime information about what type they are: for instance, when you say new Integer[]{7}, the array object itself remembers that it was created as an Integer array. This is for a couple of reasons, both related to casting:

  • If you have an Integer[], Java throws a runtime error if you try to do something sneaky like ((Object[]) myIntegerArray)["not a number!"] which would otherwise put you in a weird situation where your integer array contained a string; to do that it needs to check at runtime that each value you put in an array is compatible with Integer
  • You can cast Integer[] up to Object[] and back down to Integer[], but not back down to, say, String[] -- if you try you'll get a class-cast exception on the downcast at runtime. Java needs to know that the value was created as an Integer[] to support that.

Generics, on the other hand, don't have a runtime component. You may have heard of erasure, and that's what that refers to: all the generics stuff is checked at compile time but gets removed from the program and doesn't affect runtime at all.

So what happens if you try to make an array of some generic type, like new T[]{}? It won't compile, because the array needs to know what T is at runtime in order to work, but Java doesn't know what T is at runtime. So the compiler just doesn't let you build one of those at all.

But there is a loophole. If you call a varargs function with a generic type, Java allows the program to compile. Recall that the callsite of a varargs method creates a new array, though -- so what type does it give that array? In this case, it'll just create it as an Object[], since Object is the erasure of T.

In some cases this will work just fine, but your program is not one of them. In your program, the value that func takes on is

public Integer call(Integer... vals) {
  return vals[0] += 2;
}

The body isn't important; you could replace it with return null; and the program would behave identically. What's important is that it takes Integer..., not Object... or T... or something like that. Remember that Integer... is syntactic sugar, and after compilation it really takes Integer[]. So when you call this method on an array, the first thing it's going to do is cast it to Integer[].

And that's the problem: At the callsite, all we knew was that this function took T..., so we compiled it with a newly-created Object[] (which happened to be filled with an integer at runtime, but the compiler didn't know that statically). But the callee needed an Integer[], and you can't downcast an array that was constructed as new Object[]{} into an Integer[] for reasons discussed above. When you try, you get java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer; which is exactly what your program produces.

The moral of the story

There's no reason to try to learn all the details of what went wrong above. Instead, here's what I hope you take away:

  • Prefer Java collections such as List to arrays. Collections are the Java way.
  • Varargs and generics don't mix. You can easily walk into nasty traps like this one when you mix them together. Don't open the door to these kinds of things.

Hope that helps.

jacobm
  • 12,582
  • 1
  • 21
  • 24
  • 1
    "Prefer Java collections such as List to arrays." But that's not really relevant to the issue here. They can get rid of `E[]` and change the `map` function to map a single value of `E` to a single value of `E`, and would have the same problem. The problem is with varargs. – newacct May 28 '18 at 04:04