2

I've got this Generic class, with a method returning a Generic Array:

public class ZTagField<T> extends JTextPane {
    public ZTagField(StringBasedFactory<T> factory) {
        assert (factory != null);
        this.factory = factory;
        init();
    }

    public T[] getItems() {
        ...
        T[] arrItems = (T[]) currentItems.toArray((T[])new Object[0]);
        return arrItems;
    }

And another one using it:

public class Xxx {
    ZTagField<clTag> txtTags = null;
    public Xxx() {
        txtTags = new ZTagField<clTag>(createFactory());
    }

    public clTag[] getSelectedTags() {
        return txtTags.getItems(); 
    }
}

This latter txtTags.getItems()gives me an exception : ==> Exception [Object cannot be cast to [clTag ????

Can anyone explain me why ?

I've trie to apply as much of this How to create a generic array, to no avail. I've got an ugly workaround :

return Arrays.asList(txtTags.getItems()).toArray(new clTag[0])

But I'd like to have it in then ZTagFieldClass.

lvr123
  • 619
  • 4
  • 19
  • Please post the stack trace. – Turing85 May 27 '17 at 16:49
  • All the good 3 approaches. As returning a List instead of an array would be far easier, I will first get back to the design and see why I designed this class (a long time ago) to return an array instead of a List. Thanks. – lvr123 May 27 '17 at 20:05

3 Answers3

4

Arrays are reified. That means that they hold a reference to their component type, and when inserting an element, it uses that reference to check if the inserted element is actually a subtype of the component type.

Because of this, to create a T[], you need a concrete reference to the component type T, and since generics are erased, the generic T doesn't count. (That's also why you can't straight up create a T[] like T[] arr = new T[].)

The toArray method of a collection gets around this by having the user pass an array of the component type. But you try to cheat this by casting your Object[] to a T[], which doesn't actually create an array of T (where would the reference to the concrete T come from?). Such a cast would fail if it weren't unchecked, unless T was actually Object.

That's also where the ClassCastException comes from. You create an Object[], so the component type is Object, no matter if you cast it to T[], the component type stays Object. But later on, you know the actual component type you want (clTag):

public clTag[] getSelectedTags() {
    return txtTags.getItems(); 
}

So the compiler will insert an implicit cast here to clTag[]:

public clTag[] getSelectedTags() {
    return (clTag[]) txtTags.getItems();
}

But you can not cast an Object[] to a clTag[], just like you can not cast an Object to clTag.

Your workaround works, because you're actually supplying a reference to the component type:

Arrays.asList(txtTags.getItems()).toArray(new clTag[0]) // <-- 'clTag' here

A more modern solution than passing an array of the component type is to pass a IntFuntion<T[]>, which encapsulates an array constructor, to the method:

public T[] getItems(IntFunction<T[]> arrCons) {
    ...
    T[] arrItems = currentItems.toArray(arrCons.apply(0));
    return arrItems;
}

...

txtTags.getItems(clTag[]::new);

But you can't get around having to pass the component type in some way or another, unless you switch to returning a List<T> (as GhostCat also suggested). Since generics are not reified, you can create a List<T> without a reference to a component type:

public List<T> getItems() {
    ...
    return new ArrayList<>(currentItems);
}
Jorn Vernee
  • 26,917
  • 3
  • 67
  • 80
3

Works as designed: at runtime there is no generic type.

It gets erased, and an array of Object is created. An array of Object can not be cast to another kind of array.

This is one of the restrictions of Java generics, based on the way how they are implemented.

That is way you are advised to be careful using arrays together with generics. You might prefer to use a generic List instead.

And just to be clear about this: yes, you can get arrays to work with generics, as elegantly shown by the other answers. But: you spend your time fighting symptoms doing so. Arrays and generics don't go together nicely in Java. Accept that, use generic lists and thus: fix the problem instead of working around it.

GhostCat
  • 127,190
  • 21
  • 146
  • 218
  • Short and effective. It is often enough :) – davidxxx May 27 '17 at 18:39
  • Thanks. And I agree to your assessment. That other answer is enlightening! – GhostCat May 27 '17 at 18:42
  • The fact that an array of Object is created has nothing to do with erasure or generics, but rather is just the documented runtime semantics of the method he is calling based on what he passed in. – newacct Jun 01 '17 at 01:53
2

After compilation, the types are erased.
Since T is not bounded to a specific type, T will be replaced by Object.

So, this :

T[] arrItems = (T[]) currentItems.toArray((T[])...);
return arrItems;

will not create and return an array of the specific type used by the instance of the class at runtime but will only create an array of Object.

Besides, in Collection.toArray() you cannot pass either an array (new T[]) because it is not valid to create a generic array.

Consequently, if you want to use the toArray() method, you can finally only pass an array of Object in this way :

 Object[] arrayObject = values.toArray(new Object[currentItems.size()]);

But an array doesn't work as a List type.
An array of a specific type cannot be cast to an array of another type even if the elements that it contains are of the type of the target of the cast.
So, you cannot cast an array of Object to an array of a specific type even if the array contains elements with this specific type such as.

So this will produce a ClassCastException :

clTag[] values = (clTag[]) arrayObject;

To solve your problem :


If you can use Java 8, using a functional interface is really a clean solution. Jorn Vernee has given a very good answer illustrating it.

Otherwise, before Java 8, the single way to create an array of the same type that the parameterized type used in a generic collection is :

1) Creating a new array with the specified type.
The java.lang.reflect.Array.newInstance(Class clazz, int length) method allows to create an array of the specified class and length.

2) Storing the class of the declared type in the instance of the generic class. You can do it by adding a class parameter in the constructor of it.

3) Populating it from the elements of the generic collection.
An easy way is using <Object, Object> Object[] java.util.Arrays.copyOf(Object[] original, int newLength, Class<? extends Object[]> newType) method but it is not effective as first you have to convert the collection into an array with toArray() to be able to pass it to the copyOf() method.

For example with a generic List, you could write :

public class ZTagField<T> {

    private class<T> clazz;
    private List<T> list = new ArrayList<>();

    public ZTagField (class<T> clazz){
         this.clazz = clazz;
    }

    public T[] get() {    
       T[] array = (T[]) Array.newInstance(clazz, list.size());            
       Class<? extends Object[]> clazzArray = array.getClass();
       array  = (T[]) Arrays.copyOf(values.toArray(), values.size(), clazzArray);
       return array;
    }
}

It works but as said it is not effective.
A more effective solution would be iterating on the list and adding elements in the new array instead of using Arrays.copyOf():

    public T[] get() {    
       T[] array = (T[]) Array.newInstance(clazz, list.size());            
        for (int i = 0; i < values.size(); i++) {
             array[i] = values.get(i);
        }
       return array;
    }
davidxxx
  • 104,693
  • 13
  • 159
  • 179
  • This one enlightens as well. – GhostCat May 27 '17 at 18:43
  • No problem. You inspired me to further enhance my answer. Now let's see what the op has to say. In case he ever comes back :-) – GhostCat May 27 '17 at 18:56
  • 1
    Improving an answer is always a good thing :) Wait and see indeed :)The 3 answers are different and handle really distinct aspects of the problem : design warning, before java 8 way, since java 8 way. – davidxxx May 27 '17 at 19:20
  • @davidxxx Thank you comments are not allowed. I deleted your "Thank you" comment. – John Militer May 30 '17 at 00:22