Question Comment créer un tableau générique en Java?


En raison de l'implémentation des génériques Java, vous ne pouvez pas avoir de code comme celui-ci:

public class GenSet<E> {
    private E a[];

    public GenSet() {
        a = new E[INITIAL_ARRAY_LENGTH]; // error: generic array creation
    }
}

Comment puis-je l'implémenter tout en maintenant la sécurité du type?

J'ai vu une solution sur les forums Java qui va comme ceci:

import java.lang.reflect.Array;

class Stack<T> {
    public Stack(Class<T> clazz, int capacity) {
        array = (T[])Array.newInstance(clazz, capacity);
    }

    private final T[] array;
}

Mais je ne comprends vraiment pas ce qui se passe.


880
2018-02-09 17:30


origine


Réponses:


Je dois poser une question en retour: est votre GenSet "coché" ou "décoché"? Qu'est-ce que ça veut dire?

  • Vérifié: typage fort. GenSet sait explicitement quel type d'objets il contient (c'est-à-dire que son constructeur a été explicitement appelé avec un Class<E> argument, et les méthodes vont lancer une exception quand ils sont passés des arguments qui ne sont pas de type E. Voir Collections.checkedCollection.

    -> dans ce cas, vous devriez écrire:

    public class GenSet<E> {
    
        private E[] a;
    
        public GenSet(Class<E> c, int s) {
            // Use Array native method to create array
            // of a type only known at run time
            @SuppressWarnings("unchecked")
            final E[] a = (E[]) Array.newInstance(c, s);
            this.a = a;
        }
    
        E get(int i) {
            return a[i];
        }
    }
    
  • Décoché: faible typage. Aucune vérification de type n'est effectuée sur les objets passés en argument.

    -> dans ce cas, vous devriez écrire

    public class GenSet<E> {
    
        private Object[] a;
    
        public GenSet(int s) {
            a = new Object[s];
        }
    
        E get(int i) {
            @SuppressWarnings("unchecked")
            final E e = (E) a[i];
            return e;
        }
    }
    

    Notez que le type de composant du tableau doit être le effacement du paramètre type:

    public class GenSet<E extends Foo> { // E has an upper bound of Foo
    
        private Foo[] a; // E erases to Foo, so use Foo[]
    
        public GenSet(int s) {
            a = new Foo[s];
        }
    
        ...
    }
    

Tout ceci résulte d'une faiblesse connue et délibérée des génériques en Java: elle a été implémentée en utilisant l'effacement, ainsi les classes "génériques" ne savent pas quel type d'argument elles ont été créées lors de l'exécution, et ne peuvent donc pas fournir de type. sécurité sauf si un mécanisme explicite (vérification de type) est mis en œuvre.


579
2018-02-09 22:19



Vous pouvez toujours faire ceci:

E[] arr = (E[])new Object[INITIAL_ARRAY_LENGTH];

C'est l'une des manières suggérées de mettre en œuvre une collection générique Java efficace; Article 26. Aucune erreur de type, pas besoin de lancer le tableau à plusieurs reprises. toutefois Cela déclenche un avertissement car il est potentiellement dangereux et doit être utilisé avec précaution. Comme détaillé dans les commentaires, cette Object[] est maintenant masquerading comme notre E[] tapez, et peut provoquer des erreurs inattendues ou ClassCastExceptions si utilisé de manière non sûre.

En règle générale, ce comportement est sûr tant que le tableau de distribution est utilisé en interne (par exemple pour sauvegarder une structure de données), et qu'il n'est pas retourné ou exposé au code client. Si vous avez besoin de retourner un tableau d'un type générique à un autre code, la réflexion Array classe que vous mentionnez est la bonne voie à suivre.


Il est important de mentionner que, dans la mesure du possible, vous aurez beaucoup plus de plaisir à travailler avec Lists plutôt que des tableaux si vous utilisez des génériques. Certes, vous n'avez parfois pas le choix, mais l'utilisation du framework de collections est beaucoup plus robuste.


160
2018-05-27 20:00



Voici comment utiliser les génériques pour obtenir exactement le type que vous cherchez tout en préservant la sécurité du type (par opposition aux autres réponses, qui vous donneront soit Object tableau ou entraîner des avertissements au moment de la compilation):

import java.lang.reflect.Array;  

public class GenSet<E> {  
    private E[] a;  

    public GenSet(Class<E[]> clazz, int length) {  
        a = clazz.cast(Array.newInstance(clazz.getComponentType(), length));  
    }  

    public static void main(String[] args) {  
        GenSet<String> foo = new GenSet<String>(String[].class, 1);  
        String[] bar = foo.a;  
        foo.a[0] = "xyzzy";  
        String baz = foo.a[0];  
    }  
}

Cela compile sans avertissements, et comme vous pouvez le voir dans main, pour quel que soit le type que vous déclarez une instance de GenSet comme, vous pouvez assigner a à un tableau de ce type, et vous pouvez attribuer un élément de a à une variable de ce type, ce qui signifie que le tableau et les valeurs dans le tableau sont du type correct.

Il fonctionne en utilisant des littéraux de classe en tant que jetons de type runtime, comme indiqué dans le Tutoriels Java. Les littéraux de classe sont traités par le compilateur comme des instances de java.lang.Class. Pour en utiliser un, il suffit de suivre le nom d'une classe avec .class. Alors, String.class agit en tant que Class objet représentant la classe String. Cela fonctionne également pour les interfaces, énumérations, tableaux quelconques (par ex. String[].class), des primitives (par ex. int.class), et le mot-clé void (c'est à dire. void.class).

Class lui-même est générique (déclaré Class<T>, où T est le type que le Class objet représente), ce qui signifie que le type de String.class est Class<String>.

Ainsi, chaque fois que vous appelez le constructeur pour GenSet, vous passez dans un littéral de classe pour le premier argument représentant un tableau de la GenSet le type déclaré de l'instance (par ex. String[].class pour GenSet<String>). Notez que vous ne pourrez pas obtenir un tableau de primitives, car les primitives ne peuvent pas être utilisées pour les variables de type.

Dans le constructeur, appeler la méthode cast renvoie le passé Object argument jeté à la classe représentée par le Class objet sur lequel la méthode a été appelée. Appel de la méthode statique newInstance dans java.lang.reflect.Array retourne en tant que Object un tableau du type représenté par le Class objet passé comme premier argument et de la longueur spécifiée par le int passé comme le deuxième argument. Appel de la méthode getComponentType retourne un Class objet représentant le type de composant du tableau représenté par le Class objet sur lequel la méthode a été appelée (par ex. String.class pour String[].class, null si la Class l'objet ne représente pas un tableau).

Cette dernière phrase n'est pas entièrement exacte. Appel String[].class.getComponentType() retourne un Class objet représentant la classe Stringmais son type est Class<?>, ne pas Class<String>, c'est pourquoi vous ne pouvez pas faire quelque chose comme ce qui suit.

String foo = String[].class.getComponentType().cast("bar"); // won't compile

Il en va de même pour toutes les méthodes Class cela renvoie un Class objet.

Concernant le commentaire de Joachim Sauer sur cette réponse (Je n'ai pas assez de réputation pour le commenter moi-même), l'exemple utilisant la distribution T[] entraînera un avertissement car le compilateur ne peut pas garantir la sécurité du type dans ce cas.


Éditer concernant les commentaires d'Ingo:

public static <T> T[] newArray(Class<T[]> type, int size) {
   return type.cast(Array.newInstance(type.getComponentType(), size));
}

53
2017-11-19 03:30



C'est la seule réponse qui est de type sûr

E[] a;

a = newArray(size);

@SafeVarargs
static <E> E[] newArray(int length, E... array)
{
    return Arrays.copyOf(array, length);
}

34
2017-11-08 15:28



Pour étendre à plus de dimensions, il suffit d'ajouter []et paramètres de dimension à newInstance() (T est un paramètre de type, cls est un Class<T>, d1 par d5 sont des entiers):

T[] array = (T[])Array.newInstance(cls, d1);
T[][] array = (T[][])Array.newInstance(cls, d1, d2);
T[][][] array = (T[][][])Array.newInstance(cls, d1, d2, d3);
T[][][][] array = (T[][][][])Array.newInstance(cls, d1, d2, d3, d4);
T[][][][][] array = (T[][][][][])Array.newInstance(cls, d1, d2, d3, d4, d5);

Voir Array.newInstance() pour plus de détails.


25
2017-08-15 13:47



En Java 8, nous pouvons faire une sorte de création de tableau générique en utilisant une référence lambda ou une méthode. Ceci est similaire à l'approche réflexive (qui passe un Class), mais ici nous n'utilisons pas la réflexion.

@FunctionalInterface
interface ArraySupplier<E> {
    E[] get(int length);
}

class GenericSet<E> {
    private final ArraySupplier<E> supplier;
    private E[] array;

    GenericSet(ArraySupplier<E> supplier) {
        this.supplier = supplier;
        this.array    = supplier.get(10);
    }

    public static void main(String[] args) {
        GenericSet<String> ofString =
            new GenericSet<>(String[]::new);
        GenericSet<Double> ofDouble =
            new GenericSet<>(Double[]::new);
    }
}

Par exemple, ceci est utilisé par <A> A[] Stream.toArray(IntFunction<A[]>).

Ce pourrait aussi être fait pré-Java 8 en utilisant des classes anonymes mais c'est plus encombrant.


11
2018-03-05 14:14



Ceci est couvert au chapitre 5 (Génériques) de Java efficace, 2e édition, article 25 ...Préférer les listes aux tableaux

Votre code fonctionnera, bien qu'il génère un avertissement non contrôlé (que vous pouvez supprimer avec l'annotation suivante:

@SuppressWarnings({"unchecked"})

Cependant, il serait probablement préférable d'utiliser une liste au lieu d'un tableau.

Il y a une discussion intéressante sur ce bug / cette fonctionnalité le site du projet OpenJDK.


10
2018-02-09 18:50



Les génériques Java fonctionnent en vérifiant les types au moment de la compilation et en insérant les distributions appropriées, mais effacement les types dans les fichiers compilés. Cela rend les bibliothèques génériques utilisables par le code qui ne comprend pas les génériques (ce qui était une décision de conception délibérée) mais qui signifie que vous ne pouvez normalement pas savoir quel est le type au moment de l'exécution.

Le public Stack(Class<T> clazz,int capacity) constructeur vous oblige à passer un objet de classe à l'exécution, ce qui signifie que les informations de classe est disponible au moment de l'exécution pour le code qui en a besoin. Et le Class<T> form signifie que le compilateur vérifiera que l'objet Class que vous passez est précisément l'objet Class pour le type T. Pas une sous-classe de T, pas une superclasse de T, mais précisément T.

Cela signifie que vous pouvez créer un objet tableau du type approprié dans votre constructeur, ce qui signifie que le type des objets que vous stockez dans votre collection verra leurs types vérifiés au moment où ils sont ajoutés à la collection.


7
2018-02-11 10:07