Generic type casting vs parameterized type casting in Java

Viewed 575

I know the general differences between generic types and parameterized types, and I know some general rules:

  • List<A> and List<B> have no inheritance relationship, even if A and B are related through an inheritance chain;

  • Object[] cannot be cast to String[], unless the Object[] array was constructed using new String[n].

But my question is a bit specific. So I am going to give some code.

According to the above general rules, the cast in the following 2 examples is invalid:

static List<String> f1a(List<String> list) {
    return List.of((String[]) list.toArray());  // ClassCastException
}

static List<String> f2a(List<String> list) {
    return (List<String>) List.of(list.toArray());  // compile-time error: Inconvertible types
}

Now if I replace the String type with a generic type parameter E, the casting works! But I really don't understand why?

// f1 is a generic version f1a, where String -> E
static <E> List<E> f1(List<E> list) {
    return List.of((E[]) list.toArray());
}

// f2 is a generic version f2a, where String -> E
static <E> List<E> f2(List<E> list) {
    return (List<E>) List.of(list.toArray());
}

The following demo shows that f1 and f2 are valid, while f1a and f2a are problematic:

public static void main(String[] args) {
    List<String> list = List.of("hello", "world");

    List<String> copy1 = f1(list);  // works
    System.out.println(copy1);

    List<String> copy2 = f2(list);  // works
    System.out.println(copy2);

    List<String> copy1a = f1a(list);  // ClassCastException
    System.out.println(copy1a);

    List<String> copy2a = f2a(list);  // compile-time error
    System.out.println(copy2a);
}
2 Answers

f1 works because of type erasure. The cast to E[] is completely removed at runtime, meaning that your code would behave something like this at runtime, but it does let the compiler infer that you're returning a List<E> and not a List<Object>, therefore letting the program compile.

static List f1(List list) {
  return List.of(list.toArray());
}

f2 works because of basically the same reason. This time, the result of List.of(list.toArray()) is inferred to be a List<Object> because toArray returns an Object[], but the cast, which is later erased, makes it compile.


f1a has a ClassCastException because you're trying to turn an Object[] into a String[]. In Java, arrays know the type they were declared with even at runtime, so the array returned by toArray thinks of itself as an Object[] even though it really only holds strings and doesn't like being turned into a String[].

f2a doesn't work because you can't turn a List<Object> into a List<String>.

If you want to turn your list into an array of strings, try list.toArray(new String[0]). This way, it will return a proper String[] and not cause problems.

If you convert a List<Object> to a List<String>, cast to a raw type with (List) List.of(list.toArray())

f1a case:

Here, the explicit casting is done, which means that compiler expects the same type at the compile time. By providing explicit cast (String[]) list.toArray(), you tell to your compiler, that whatever is returned by .toArray(), it should attempt to cast it to String[], and hence you want to build a List.of(String[]), which also constitutes a return type of your method. This is why it doesn't have any compile-time problem, but rather it throws run-time exception, as it can't cast Object to String. Think of it as at the compile time, explicitly given instruction is correct for the compiler, but it doesn't work at run-time. To understand more about casting, please also see Checked Casts at Runtime and maybe also type erasure.

f2acase:

.toArray() method of ArrayList, by method definition returns Object[]. That's why you get the list of objects, which can't be downcasted to list of Strings, and this happens, because it can't cast it and this is a checked cast, and you get compile-time Exception, i.e. your code doesn't compile.

Related