web-dev-qa-db-fra.com

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

En raison de l'implémentation de Java génériques, vous ne pouvez pas avoir un 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 implémenter ceci tout en maintenant la sécurité de type?

J'ai vu une solution sur les forums Java qui se présente comme suit:

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.

1017
tatsuhirosatou

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

  • Vérifié : frappe forte . GenSet sait explicitement quel type d'objets il contient (c'est-à-dire que son constructeur a été explicitement appelé avec un argument _Class<E>_, et les méthodes lèveront une exception lorsqu'elles recevront 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é : frappe faible . Aucune vérification de type n'est en réalité 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 erasure 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 cela résulte d’une faiblesse connue et délibérée des génériques en Java: elle a été implémentée à l’aide de erasure, de sorte que les classes "génériques" ne savent pas quel argument de type elles ont été créées au moment 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 implémenté.

658
Varkhan

Tu peux le faire:

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

C’est l’une des méthodes suggérées pour implémenter une collection générique dans Effective Java; Item 26. Aucune erreur de type, pas besoin de lancer le tableau à plusieurs reprises. Cependant cela déclenche un avertissement car il est potentiellement dangereux et doit être utilisé avec prudence. Comme indiqué en détail dans les commentaires, ce Object[] se présente maintenant comme notre type E[] et peut provoquer des erreurs inattendues ou ClassCastExceptions s'il est utilisé de manière non sécurisée.

En règle générale, ce comportement est sans danger tant que le tableau de conversion est utilisé en interne (par exemple, pour sauvegarder une structure de données), et n'est pas renvoyé ou exposé au code client. Si vous devez renvoyer un tableau d'un type générique à un autre code, la classe de réflexion Array que vous mentionnez est la bonne façon de procéder.


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

189
dimo414

Voici comment utiliser les génériques pour obtenir un tableau du type que vous recherchez tout en préservant la sécurité du type (par opposition aux autres réponses, qui vous redonneront un tableau Object ou donneront lieu à des avertissements lors 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, quel que soit le type pour lequel vous déclarez une instance de GenSet, vous pouvez affecter a à un tableau de ce type, et vous pouvez assigne un élément de a à une variable de ce type, ce qui signifie que le tableau et les valeurs qu'il contient sont du type correct.

Cela fonctionne en utilisant des littéraux de classe en tant que jetons de type à l'exécution, 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, suivez simplement le nom d'une classe avec .class. Ainsi, String.class agit comme un objet Class représentant la classe String. Cela fonctionne également pour les interfaces, les énumérations, les tableaux de toutes dimensions (par exemple String[].class), les primitives (par exemple int.class) et le mot clé void (c'est-à-dire void.class).

Class est elle-même générique (déclarée comme Class<T>, où T représente le type que l'objet Class 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 transmettez un littéral de classe pour le premier argument représentant un tableau du type déclaré de l'instance GenSet (par exemple, 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, l'appel de la méthode cast renvoie l'argument passé Object à la classe représentée par l'objet Class sur lequel la méthode a été appelée. L'appel de la méthode statique newInstance dans Java.lang.reflect.Array renvoie sous la forme Object un tableau du type représenté par l'objet Class passé comme premier argument et de la longueur spécifiée par le int passé comme deuxième argument. L'appel de la méthode getComponentType renvoie un objet Class représentant le type de composant du tableau représenté par l'objet Class sur lequel la méthode a été appelée (par exemple String.class pour String[].class, null si l'objet Class ne représente pas un tableau).

Cette dernière phrase n'est pas tout à fait exacte. L'appel de String[].class.getComponentType() renvoie un objet Class représentant la classe String, mais son type est Class<?> et non Class<String>, raison pour laquelle vous ne pouvez pas faire quelque chose comme le suivant.

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

Il en va de même pour chaque méthode de Class qui renvoie un objet Class.

En ce qui concerne le commentaire de Joachim Sauer sur cette réponse (je n'ai pas assez de réputation pour commenter moi-même), l'exemple utilisant le transtypage en T[] donnera lieu à un avertissement car le compilateur peut ' t garantir la sécurité du type dans ce cas.


Edit concernant les commentaires d'Ingo:

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

C'est la seule réponse qui soit de type safe

E[] a;

a = newArray(size);

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

Pour étendre à plus de dimensions, ajoutez simplement les paramètres de [] et les dimensions à newInstance() (T est un paramètre de type, cls est un Class<T>, d1 à 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.

29
Jason C

Dans Java 8, nous pouvons créer une sorte de tableau générique à l'aide d'une référence lambda ou d'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[]>) .

Cela pourrait également être effectué avant Java 8 à l'aide de classes anonymes, mais c'est plus lourd.

12
Radiodef

Ceci est couvert dans le chapitre 5 (Génériques) de Effective Java, 2e édition , élément 25 ... Préfère les listes aux tableaux

Votre code fonctionnera, même s'il générera 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 plutôt qu’un tableau.

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

10
Jeff Olson

Les génériques Java fonctionnent en vérifiant les types au moment de la compilation et en insérant les conversions appropriées, mais en effaçant les types des fichiers compilés. Cela rend les bibliothèques génériques utilisables par 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 pas normalement trouver le type au moment de l'exécution.

Le constructeur public Stack(Class<T> clazz,int capacity) requiert que vous passiez un objet Class au moment de l'exécution, ce qui signifie que les informations sur la classe is sont disponibles au moment de l'exécution pour coder celles qui en ont besoin. Et la forme Class<T> signifie que le compilateur vérifiera que l'objet Class que vous passez est précisément l'objet Class pour le type T. Ce n'est pas une sous-classe de T, pas une superclasse de T, mais précisément T.

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

7
Bill Michell

Bonjour, même si le fil est mort, je voudrais attirer votre attention sur ceci:

Les génériques sont utilisés pour la vérification de type pendant la compilation:

  • Par conséquent, le but est de vérifier que ce qui entre est ce dont vous avez besoin.
  • Ce que vous retournez, c'est ce dont le consommateur a besoin.
  • Vérifie ça:

enter image description here

Ne vous inquiétez pas de la conversion des avertissements lorsque vous écrivez une classe générique. Inquiétez-vous quand vous l'utilisez.

6
puneeth

Qu'en est-il de cette solution?

@SafeVarargs
public static <T> T[] toGenericArray(T ... elems) {
    return elems;
}

Cela fonctionne et semble trop simple pour être vrai. Y a-t-il un inconvénient?

6
Benjamin M

Vous n'avez pas besoin de transmettre l'argument de classe au constructeur. Essaye ça.

public class GenSet<T> {
    private final T[] array;
    @SuppressWarnings("unchecked")
    public GenSet(int capacity, T... dummy) {
        if (dummy.length > 0)
            throw new IllegalArgumentException(
              "Do not provide values for dummy argument.");
        Class<?> c = dummy.getClass().getComponentType();
        array = (T[])Array.newInstance(c, capacity);
    }
    @Override
    public String toString() {
        return "GenSet of " + array.getClass().getComponentType().getName()
            + "[" + array.length + "]";
    }
}

et

GenSet<Integer> intSet = new GenSet<>(3);
System.out.println(intSet);
System.out.println(new GenSet<String>(2));

résultat:

GenSet of Java.lang.Integer[3]
GenSet of Java.lang.String[2]
5
saka1029

Regardez aussi ce code:

public static <T> T[] toArray(final List<T> obj) {
    if (obj == null || obj.isEmpty()) {
        return null;
    }
    final T t = obj.get(0);
    final T[] res = (T[]) Array.newInstance(t.getClass(), obj.size());
    for (int i = 0; i < obj.size(); i++) {
        res[i] = obj.get(i);
    }
    return res;
}

Il convertit une liste d'objets de tout type en un tableau du même type.

5
MatheusJardimB

J'ai trouvé un moyen rapide et facile qui fonctionne pour moi. Notez que je ne l’utilise que sur Java JDK 8. Je ne sais pas si cela fonctionnera avec les versions précédentes.

Bien que nous ne puissions pas instancier un tableau générique d'un paramètre de type spécifique, nous pouvons passer un tableau déjà créé à un constructeur de classe générique.

class GenArray <T> {
    private T theArray[]; // reference array

    // ...

    GenArray(T[] arr) {
        theArray = arr;
    }

    // Do whatever with the array...
}

Nous pouvons maintenant créer le tableau comme suit:

class GenArrayDemo {
    public static void main(String[] args) {
        int size = 10; // array size
        // Here we can instantiate the array of the type we want, say Character (no primitive types allowed in generics)
        Character[] ar = new Character[size];

        GenArray<Character> = new Character<>(ar); // create the generic Array

        // ...

    }
}

Pour plus de flexibilité avec vos tableaux, vous pouvez utiliser une liste chaînée, par exemple. ArrayList et d'autres méthodes trouvées dans la classe Java.util.ArrayList.

5
Nikos

L'exemple utilise Java réflexion pour créer un tableau. Cela n’est généralement pas recommandé, car il n’est pas typé. Au lieu de cela, vous devez simplement utiliser une liste interne et éviter le tableau du tout.

4
Ola Bini

Passer une liste de valeurs ...

public <T> T[] array(T... values) {
    return values;
}
3
Rodrigo Asensio

En fait, un moyen plus facile de le faire est de créer un tableau d’objets et de le convertir selon le type souhaité, comme dans l’exemple suivant:

T[] array = (T[])new Object[SIZE];

SIZE est une constante et T est un identificateur de type

3
Pedram Esmaeeli

J'ai créé cet extrait de code pour instancier de manière réfléchie une classe transmise pour un utilitaire de test automatisé simple.

Object attributeValue = null;
try {
    if(clazz.isArray()){
        Class<?> arrayType = clazz.getComponentType();
        attributeValue = Array.newInstance(arrayType, 0);
    }
    else if(!clazz.isInterface()){
        attributeValue = BeanUtils.instantiateClass(clazz);
    }
} catch (Exception e) {
    logger.debug("Cannot instanciate \"{}\"", new Object[]{clazz});
}

Notez ce segment:

    if(clazz.isArray()){
        Class<?> arrayType = clazz.getComponentType();
        attributeValue = Array.newInstance(arrayType, 0);
    }

pour l'initiation du tableau où Array.newInstance (classe du tableau, taille du tableau). La classe peut être à la fois primitive (int.class) et objet (Integer.class).

BeanUtils fait partie du printemps.

3
Bobster

La distribution forcée suggérée par d'autres personnes n'a pas fonctionné pour moi, ce qui constitue une exception à la distribution illégale.

Cependant, cette distribution implicite a bien fonctionné:

Item<K>[] array = new Item[SIZE];

où Item est une classe que j'ai définie et qui contient le membre:

private K value;

De cette façon, vous obtenez un tableau de type K (si l'élément n'a que la valeur) ou tout type générique que vous souhaitez définir dans la classe Item.

1
vnportnoy

Personne d'autre n'a répondu à la question de savoir ce qui se passe dans l'exemple que vous avez posté.

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

Comme d'autres l'ont dit, les génériques sont "effacés" lors de la compilation. Ainsi, au moment de l'exécution, une instance d'un générique ne sait pas quel est son type de composant. La raison en est historique, Sun a voulu ajouter des génériques sans casser l’interface existante (source et binaire).

Les tableaux par contre do connaissent leur type de composant au moment de l'exécution.

Cet exemple contourne le problème en demandant au code qui appelle le constructeur (qui connaît le type) de transmettre un paramètre indiquant à la classe le type requis.

Donc, l'application construirait la classe avec quelque chose comme

Stack<foo> = new Stack<foo>(foo.class,50)

et le constructeur sait maintenant (au moment de l'exécution) ce qu'est le type de composant et peut utiliser ces informations pour construire le tableau via l'API de réflexion.

Array.newInstance(clazz, capacity);

Enfin, nous avons une conversion de type car le compilateur n'a aucun moyen de savoir que le tableau renvoyé par Array#newInstance() est le type correct (même si nous le savons).

Ce style est un peu moche, mais il peut parfois être la solution la moins mauvaise pour créer des types génériques qui ont besoin de connaître leur type de composant au moment de l'exécution pour quelque raison que ce soit (création de tableaux ou création d'instances de type de composant, etc.).

1
plugwash

J'ai trouvé une sorte de solution à ce problème.

La ligne ci-dessous renvoie une erreur de création de tableau générique

List<Person>[] personLists=new ArrayList<Person>()[10];

Cependant, si j'encapsule List<Person> dans une classe séparée, cela fonctionne.

import Java.util.ArrayList;
import Java.util.List;


public class PersonList {

    List<Person> people;

    public PersonList()
    {
        people=new ArrayList<Person>();
    }
}

Vous pouvez exposer les personnes de la classe PersonList par un getter. La ligne ci-dessous vous donnera un tableau, qui a un List<Person> dans chaque élément. En d'autres termes, tableau de List<Person>.

PersonList[] personLists=new PersonList[10];

J'avais besoin de quelque chose comme ça dans un code sur lequel je travaillais et c'est ce que j'ai fait pour le faire fonctionner. Jusqu'ici pas de problèmes.

1
developer747

En fait, j'ai trouvé une solution assez unique pour éviter l'incapacité d'initier un tableau générique. Ce que vous devez faire est de créer une classe qui intègre la variable générique T comme suit:

class GenericInvoker <T> {
    T variable;
    public GenericInvoker(T variable){
        this.variable = variable;
    }
}

puis dans votre classe array, démarrez comme ceci:

GenericInvoker<T>[] array;
public MyArray(){
    array = new GenericInvoker[];
}

le démarrage d'un new Generic Invoker[] provoquera un problème non coché, mais il ne devrait pas y en avoir.

Pour obtenir du tableau, vous devez appeler le tableau [i] .variable comme suit:

public T get(int index){
    return array[index].variable;
}

Le reste, tel que redimensionner le tableau peut être fait avec Arrays.copyOf () comme ceci:

public void resize(int newSize){
    array = Arrays.copyOf(array, newSize);
}

Et la fonction add peut être ajoutée comme suit:

public boolean add(T element){
    // the variable size below is equal to how many times the add function has been called 
    // and is used to keep track of where to put the next variable in the array
    arrays[size] = new GenericInvoker(element);
    size++;
}
0
Crab Nebula

Vous pouvez utiliser un casting:

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

    public GenSet(int s) {
        a = (Item[]) new Object[s];
    }
}
0
samir benzenine

essaye ça.

private int m = 0;
private int n = 0;
private Element<T>[][] elements = null;

public MatrixData(int m, int n)
{
    this.m = m;
    this.n = n;

    this.elements = new Element[m][n];
    for (int i = 0; i < m; i++)
    {
        for (int j = 0; j < n; j++)
        {
            this.elements[i][j] = new Element<T>();
        }
    }
}
0
David Bernard

Je me demande si ce code créerait un tableau générique efficace?

public T [] createArray(int desiredSize){
    ArrayList<T> builder = new ArrayList<T>();
    for(int x=0;x<desiredSize;x++){
        builder.add(null);
    }
    return builder.toArray(zeroArray());
}

//zeroArray should, in theory, create a zero-sized array of T
//when it is not given any parameters.

private T [] zeroArray(T... i){
    return i;
}

Edit: Peut-être une autre manière de créer un tel tableau, si la taille requise était-elle connue et petite, serait-elle simplement d’alimenter le nombre requis de "nuls" dans la commande zeroArray?

Bien que ce soit évidemment moins polyvalent que l’utilisation du code createArray.

0
Cambot

Une solution de contournement facile, mais désordonnée, consisterait à imbriquer une deuxième classe "titulaire" dans votre classe principale et à l'utiliser pour conserver vos données.

public class Whatever<Thing>{
    private class Holder<OtherThing>{
        OtherThing thing;
    }
    public Holder<Thing>[] arrayOfHolders = new Holder<Thing>[10]
}
0
StarMonkey

Peut-être sans rapport avec cette question, mais alors que je recevais l'erreur "generic array creation" pour avoir utilisé

Tuple<Long,String>[] tupleArray = new Tuple<Long,String>[10];

Je découvre les œuvres suivantes (et a travaillé pour moi) avec @SuppressWarnings({"unchecked"}):

 Tuple<Long, String>[] tupleArray = new Tuple[10];
0
Mohsen Afshin

Vous pouvez créer un tableau d'objets et le diffuser en E partout. Ouais, ce n'est pas très propre, mais ça devrait au moins fonctionner.

0
Esko