Generics, arrays, and the ClassCastException
Asked Answered
G

3

18

I think there must be something subtle going on here that I don't know about. Consider the following:

public class Foo<T> {
  private T[] a = (T[]) new Object[5];

  public Foo() {
    // Add some elements to a
  }

  public T[] getA() {
    return a;
  }
}

Suppose that your main method contains the following:

Foo<Double> f = new Foo<Double>();
Double[] d = f.getA();

You will get a CastClassException with the message java.lang.Object cannot be cast to java.lang.Double.

Can anyone tell me why? My understanding of ClassCastException is that it is thrown when you try to cast an object to a type that cannot be casted. That is, to a subclass of which it is not an instance (to quote the documentation). e.g.:

Object o = new Double(3.);
Double d = (Double) o; // Working cast
String s = (String) o; // ClassCastException

And it seems I can do this. If a was just a T instead of an array T[], we can get a and cast it without a problem. Why do arrays break this?

Thanks.

Gambill answered 16/12, 2008 at 18:42 Comment(1)
I'm assuming this is all because you can't create generic arrays. Have you considered using an ArrayList<T> instead?Littrell
E
21
Foo<Double> f = new Foo<Double>();

When you use this version of the generic class Foo, then for the member variable a, the compiler is essentially taking this line:

private T[] a = (T[]) new Object[5];

and replacing T with Double to get this:

private Double[] a = (Double[]) new Object[5];

You cannot cast from Object to Double, hence the ClassCastException.

Update and Clarification: Actually, after running some test code, the ClassCastException is more subtle than this. For example, this main method will work fine without any exception:

public static void main(String[] args) {
    Foo<Double> f = new Foo<Double>();
    System.out.println(f.getA());
}

The problem occurs when you attempt to assign f.getA() to a reference of type Double[]:

public static void main(String[] args) {
    Foo<Double> f = new Foo<Double>();
    Double[] a2 = f.getA(); // throws ClassCastException
    System.out.println(a2);
}

This is because the type-information about the member variable a is erased at runtime. Generics only provide type-safety at compile-time (I was somehow ignoring this in my initial post). So the problem is not

private T[] a = (T[]) new Object[5];

because at run-time this code is really

private Object[] a = new Object[5];

The problem occurs when the result of method getA(), which at runtime actually returns an Object[], is assigned to a reference of type Double[] - this statement throws the ClassCastException because Object cannot be cast to Double.

Update 2: to answer your final question "why do arrays break this?" The answer is because the language specification does not support generic array creation. See this forum post for more - in order to be backwards compatible, nothing is known about the type of T at runtime.

Electrograph answered 16/12, 2008 at 18:46 Comment(5)
you are right i figured after reading the link i posted in my answer :)Dilatant
for all those wanting to read it too: angelikalanger.com/GenericsFAQ/FAQSections/…?Dilatant
The above link has some good info in it - might help the original question-asker understand more about type-erasure and what is known at run-time vs compile-timeElectrograph
-1 because the explanation is unclear and cluttered. Pandya's one is better.Santalaceous
Java compiler erase generic by "Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded" (ref:docs.oracle.com/javase/tutorial/java/generics/erasure.html), so you said "replacing T with Double" is not correct, it should replacing T with Object actually @matt bEnthuse
B
6

There may be some small errors in @mattb's explanation.

The error is not

java.lang.Object cannot be cast to java.lang.Double.

It is:

[Ljava.lang.Object; cannot be cast to [Ljava.lang.Double

The [L means an array. That is, the error is that an array of Objects cannot be cast to an array of Double. This is the same case as following:

Object[] oarr = new Object[10];
Double[] darr = (Double[]) oarr;

This is obviously not allowed.

For your issue of creating typesafe arrays, another alternative is to except a class object in init and use Array.newInstance:

import java.lang.reflect.Array;

class Foo<T> {
  private T[] a ;

  public Foo(Class<T> tclass) {
    a = (T[]) Array.newInstance(tclass, 5);
  }

  public T[] getA() {
    return a;
  }

  public static <T> Foo<T> create(Class<T> tclass) {
    return new Foo<T>(tclass);
  }
}

class Array1
{
  public static final void main(final String[] args) {
    Foo<Double> f = Foo.create(Double.class);
    Double[] d = f.getA();
  }


}
Buckler answered 17/12, 2008 at 4:21 Comment(4)
Is there ever a case where you can cast from type A to B but not A[] to B[]?Electrograph
You can never cast from A[] to B[]. Consider two classes Fruit and Apple (extends Fruit). So you can cast from Apple to Fruit. But you cannot cast from Apple[] to Fruit[]. Because you can add /any/ Fruit (say Orange) to Fruit[], but only Apple to Apple[]Buckler
Oh wait, don't know what I was talking about. For two classes C1 < C2 (that is C2 can be assigned to C1), and a generic class G, C1[] < C2[] but ! G<C1> < G<C2>. That is, Apple[] can be assigned to Fruit[] but List<Apple> cannot be assigned to List<Fruit>....[contd]Buckler
But when you (try to) store an Orange in a Fruit[] whose dynamic type is Apple[] you get an ArrayStoreException.Buckler
G
3

@matt b: Thanks for the answer! Very helpful.

I have found a workaround for those interested: give the getA method an initialized array to populate. That way the type info is available.

public class Foo<T> {
  private T[] a = (T[]) new Object[5];

  public Foo() {
    // Add some elements to a
  }

  public T[] getA(T[] holdA) {
    // Check whether holdA is null and handle it...then:
    holdA = (T[]) Array.newInstance(holdA.getClass().getComponentType(), a.length);
    System.arraycopy(a, 0, holdA, 0, a.length);
    return holdA;
  }
}

Then for your main method:

public static void main(String[] args) {
  Foo<Double> f = new Foo<Double>();
  Double[] a2 = new Double[1];
  a2 = f.getA(a2);
}
Gambill answered 16/12, 2008 at 20:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.