2 votes

Exception générique de cast de tableau

J'ai une classe générique, avec une méthode qui renvoie un tableau générique :

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;
    }

Et un autre qui l'utilise :

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

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

Ce dernier txtTags.getItems() me donne une exception : ==> Exception [Object cannot be cast to [clTag ? ???

Quelqu'un peut-il m'expliquer pourquoi ?

J'ai essayé d'appliquer autant que possible cette Comment créer un tableau générique en vain. J'ai trouvé une solution de contournement peu glorieuse :

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

Mais j'aimerais l'avoir dans la ZTagFieldClass.

4voto

Jorn Vernee Points 18630

Les tableaux sont réifiés. Cela signifie qu'ils contiennent une référence à leur type de composant et que, lorsqu'ils insèrent un élément, ils utilisent cette référence pour vérifier si l'élément inséré est effectivement un sous-type du type de composant.

Pour cette raison, pour créer un T[] vous avez besoin d'une référence concrète au type de composant. T et, comme les génériques sont effacés, la méthode Générique T ne compte pas. (C'est aussi la raison pour laquelle vous ne pouvez pas directement créer une T[] comme T[] arr = new T[] .)

En toArray d'une collection contourne ce problème en demandant à l'utilisateur de passer un tableau du type de composant. Mais vous essayez de tricher en lançant votre Object[] à un T[] ce qui ne crée pas réellement un tableau de T (où serait la référence au béton T viennent-ils ?). Un tel moulage échouerait s'il n'était pas incontrôlé, à moins que T était en fait Object .

C'est aussi là que le ClassCastException vient de. Vous créez un Object[] donc le type de composant est Object peu importe si vous le lancez vers T[] le type de composant reste Object . Mais plus tard, vous connaissez le type de composant réel que vous voulez ( clTag ):

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

Ainsi, le compilateur insérera un cast implicite ici pour clTag[] :

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

Mais vous ne pouvez pas lancer un Object[] à un clTag[] tout comme vous ne pouvez pas lancer un Object a clTag .

Votre solution de contournement fonctionne, car vous fournissez en fait une référence au type de composant :

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

Une solution plus moderne que de passer un tableau du type de composant est de passer un fichier IntFuntion<T[]> qui encapsule un constructeur de tableau, à la méthode :

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

...

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

Mais vous ne pouvez pas éviter de passer le type de composant d'une manière ou d'une autre, à moins que vous ne retourniez un fichier List<T> (comme l'a également suggéré GhostCat). Puisque les génériques sont no réifié, vous pouvez créer un List<T> sans référence à un type de composant :

public List<T> getItems() {
    ...
    return new ArrayList<>(currentItems);
}

3voto

GhostCat Points 83269

Fonctionne comme prévu : au moment de l'exécution, il n'y a pas de type générique.

Il est effacé, et un tableau d'objets est créé. Un tableau d'objets ne peut pas être transformé en un autre type de tableau.

C'est l'une des restrictions des génériques Java, basée sur la façon dont ils sont mis en œuvre.

C'est pourquoi il est conseillé de faire attention à l'utilisation des tableaux avec les génériques. Vous pouvez préférer utiliser une liste générique à la place.

Et pour être clair sur ce point : oui, vous pouvez faire fonctionner des tableaux avec des génériques, comme l'ont montré élégamment les autres réponses. Mais : vous passez votre temps à combattre les symptômes en le faisant. Les tableaux et les génériques ne vont pas bien ensemble en Java. Acceptez-le, utilisez des listes génériques et ainsi : réglez le problème au lieu de le contourner.

2voto

davidh Points 107

Après la compilation, les types sont effacés.
Depuis T n'est pas lié à un type spécifique, T sera remplacé par Object .

Donc, ce :

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

ne créera pas et ne retournera pas un tableau du type spécifique utilisé par l'instance de la classe au moment de l'exécution, mais créera seulement un tableau de Object .

Par ailleurs, en Collection.toArray() vous ne pouvez passer ni un tableau ( new T[] ) car il n'est pas valable de créer un tableau générique .

Par conséquent, si vous voulez utiliser le toArray() vous pouvez finalement seulement passer un tableau de Object de cette façon :

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

Mais un tableau ne fonctionne pas comme un type de liste.
Un tableau d'un type spécifique ne peut pas être converti en un tableau d'un autre type, même si les éléments qu'il contient sont du type de la cible de la conversion.
Ainsi, vous ne pouvez pas lancer un tableau de Object à un tableau d'un type spécifique, même si le tableau contient des éléments de ce type spécifique, par exemple.

Cela produira donc une ClassCastException :

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

Pour résoudre votre problème :

Si vous pouvez utiliser Java 8, l'utilisation d'une interface fonctionnelle est vraiment une solution propre. Jorn Vernee a donné une très bonne réponse qui l'illustre.

Sinon, avant Java 8, la seule façon de créer un tableau du même type que le type paramétré utilisé dans une collection générique est :

1) Créer un nouveau tableau avec le type spécifié.
Le site java.lang.reflect.Array.newInstance(Class clazz, int length) méthode permet de créer un tableau de la classe et de la longueur spécifiées.

2) Stocker la classe du type déclaré dans l'instance de la classe générique. Vous pouvez le faire en ajoutant un paramètre de classe dans le constructeur de celle-ci.

3) Le remplir à partir des éléments de la collection générique.
Un moyen simple est d'utiliser <Object, Object> Object[] java.util.Arrays.copyOf(Object[] original, int newLength, Class<? extends Object[]> newType) mais elle n'est pas efficace car il faut d'abord convertir la collection en tableau avec la méthode toArray() pour pouvoir le transmettre à la copyOf() méthode.

Par exemple avec un générique List vous pourriez écrire :

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;
    }
}

Il fonctionne mais, comme on l'a dit, il n'est pas efficace.
Une solution plus efficace consisterait à itérer sur la liste et à ajouter des éléments dans le nouveau tableau au lieu d'utiliser l'option 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;
    }

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X