0

My IDE (IntelliJ) shows this when highlighting over 'toArray':

java.util.ArrayList

public Object[] toArray()

This returns an Object[]; which to my knowledge means that casting is necessary for most uses. I'm not however sure if it will work in this case (this method is part of a generic class with parameter <Type>):

private Type evaluate(int operator_index){
        ArrayList<Type> this_args = new ArrayList<Type>();

        for (int index : operator_inputtedBy.get(operator_index)) {
            if(index >= 0) {
                this_args.add(evaluate(index));
            }else{
                this_args.add(args[-1-index]);
            }
        }
        return operators.get(operator_index).operation((Type[]) this_args.toArray());

    }

In what conditions will this cast fail (if any)? And what are the alternatives that do not require casting?

Community
  • 1
  • 1
Vye
  • 49
  • 4

2 Answers2

3

It always fails.

I'm sure you're wondering why. Let's try to explain:

Unlike generics, which are entirely a figment of the compiler's imagination, an array does know its 'component' type. You can ask it:

String[] strings = new String[10];
Object[] o = strings; // compiles. This is itself problematic, see later.
System.out.println(o.getClass().getComponentType()); // prints 'java.lang.String'
o[0] = Integer.valueOf(0); // hmmmmmmmm!!!!!
String str0 = strings[0]; // ... but... wait.. that's an Integer, not a String!

Tossing the above in a java file, it....

compiles??

Yes, it does. It shouldn't have, really - the reason it compiles, even today, is that java started out allowing the above, and java does not like breaking backwards compatibility. This code used to compile, so it still does. But this is obviously not right - I have put an integer in a string array. And that's why when you run it, this does not work: You get an ArrayStoreException on the line where you put an integer into o[0].

Let's try this with generics. The same thing, just, genericsed:

List<String> strings = new ArrayList<String>();
List<Object> o = strings; // the evil occurs here
// hmmm, List doesn't appear to have a getComponentType()....
o.add(Integer.valueOf(0));
String str0 = strings.get(0);

Now, sanity has been restored: That second line, where the evil occurs? The compiler doesn't let you do that. As it shouldn't, because then you run into the problems that these snippets show. In computer science-ese, array assignment is covariant, whereas with generics it is invariant. In the real world, this stuff is invariant (hence why the first snippet blows up), so generics got it right, arrays got it wrong, but we can't fix it - java should be backwards compatible.

Now that you know this, the problems with toArray() become clear:

  1. It is not possible for the toArray() method to know what component type the list is; therefore, if you have a List<String> and call toArray() on it, the toArray code cannot possibly obtain String from itself, unlike with arrays the component type is not stored. Generics are a figment of the compiler's imagination, it is nowhere in your class files, only in your source files. Thus, it doesn't know, so, it makes an array of objects (as in, the array that falls out of toArray() is technically an Object, calling its getComponentType will always print class java.lang.Object.

  2. It is legal to assign a String[] to Object[], because the java lang spec defines arrays to be covariant. But real life isn't like that. Also, the lang spec also says that the reverse does not hold: You cannot cast an instance of Object[] to type String[]. This is why it will always fail - the toArray() method ALWAYS produces an Object[], which cannot be cast like that.

  3. But let's say you could have hypothetically done that: If you then assign a non-string object to one of the array's slots, the ArrayStoreException does not happen, because the component type is incorrect.

  4. And now you get a classcastexception later on when you read it back in.

Let's try it:

List<String> list = new ArrayList<String>();
list.add("Hello!");
String[] strings = (String[]) list.toArray(); // this fails...
Object[] o = strings;
o[0] = Integer.valueOf(5);
String str0 = strings[0]; // but otherwise you'd be in trouble here.

The solution

The fix is trivial:

NB: In java, we write 'operatorIndex', not 'operator_index'. When in rome, be like romans.

return operators.get(operatorIndex).operation(thisArgs.toArray(Type[]::new));

now you get no warnings, you don't need the cast, all is well. In this scenario at runtime the system does know how to make an array of Type (because you pass in a lambda that makes it). An alternative is this:

return operators.get(operatorIndex).operation(thisArgs.toArray(new Type[0]));

the provided array here (new Type[0]) is used only to get the component type, then the array is tossed in the garbage (it's a very short lived object, so it's free garbage, fortunately), and a new one is made, but now it WILL have the proper type; it'll be a Type[], and not an Object[].

TL;DR: Do not, ever, use collections' .toArray() method. It still exists, and will continue to exist... because of the backwards compatibility thing I talked about. That's the only reason.

rzwitserloot
  • 44,252
  • 4
  • 27
  • 37
0

Short answer: You cannot cast Object[] to String[] or MyClass[].

To solve your issue, you should use the version of toArray which accepts an array, for example: list.toArray(new String[0])

Here is a small working example which will show why it fails:

public static void main(String[] args) {
    final ArrayList<String> list = new ArrayList<>();
    list.add("abc");
    list.add("def");
    try {
        final Integer[] intArray = (Integer[])list.toArray();
    } catch (final Exception e) {
        System.out.println(e.getMessage());
    }
    try {
        final String[] stringArray = (String[])list.toArray();
    } catch (final Exception e) {
        System.out.println(e.getMessage());
    }
    try {
        String[] stringArrayValid = list.toArray(new String[0]);
        System.out.println("No Error!");
    } catch (final Exception e) {
        System.out.println(e.getMessage());
    }
    try {
        Integer[] integerArrayInvalid = list.toArray(new Integer[0]);
    } catch (final Exception e) {
        System.out.println("Got Exception: " + e.getClass().getName());
    }
}

Which prints:

[Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
[Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
No Error!
Got Exception: java.lang.ArrayStoreException
sinujohn
  • 2,336
  • 3
  • 18
  • 25