Recherche…


Introduction

Les génériques sont une fonctionnalité de programmation générique qui étend le système de type Java pour permettre à un type ou à une méthode de fonctionner sur des objets de différents types tout en offrant une sécurité de type à la compilation. En particulier, la structure de collections Java prend en charge les génériques pour spécifier le type d'objets stockés dans une instance de collection.

Syntaxe

  • class ArrayList <E> {} // une classe générique avec le paramètre de type E
  • class HashMap <K, V> {} // une classe générique avec deux paramètres de type K et V
  • <E> void print (élément E) {} // une méthode générique avec le paramètre de type E
  • ArrayList <String> noms; // déclaration d'une classe générique
  • ArrayList <?> Objets; // déclaration d'une classe générique avec un paramètre de type inconnu
  • new ArrayList <String> () // instanciation d'une classe générique
  • new ArrayList <> () // instanciation avec inférence de type "diamond" (Java 7 ou ultérieur)

Remarques

Les génériques sont implémentés dans Java through Type Erasure, ce qui signifie que pendant l'exécution, les informations de type spécifiées dans l'instanciation d'une classe générique ne sont pas disponibles. Par exemple, l'instruction List<String> names = new ArrayList<>(); produit un objet liste à partir duquel le type d'élément String ne peut pas être récupéré à l'exécution. Toutefois, si la liste est stockée dans un champ de type List<String> , ou transmise à un paramètre de méthode / constructeur du même type ou renvoyée par une méthode de ce type de retour, les informations de type complètes peuvent être récupérées à l'exécution via l'API Java Reflection.

Cela signifie également que lors de la conversion vers un type générique (par exemple: (List<String>) list ), la distribution est une distribution non contrôlée . Étant donné que le paramètre <String> est effacé, la machine virtuelle Java ne peut pas vérifier si une conversion d'une List<?> Vers une List<String> est correcte. La JVM ne voit un cast que pour List to List à l'exécution.

Créer une classe générique

Les génériques permettent aux classes, interfaces et méthodes de prendre d'autres classes et interfaces en tant que paramètres de type.

Cet exemple utilise la classe générique Param pour prendre un seul paramètre de type T , délimité par des crochets ( <> ):

public class Param<T> {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

Pour instancier cette classe, fournissez un argument de type à la place de T Par exemple, Integer :

Param<Integer> integerParam = new Param<Integer>();

L'argument type peut être n'importe quel type de référence, y compris les tableaux et autres types génériques:

Param<String[]> stringArrayParam;
Param<int[][]> int2dArrayParam;
Param<Param<Object>> objectNestedParam;

Dans Java SE 7 et versions ultérieures, l'argument de type peut être remplacé par un ensemble vide d'arguments de type ( <> ) appelé diamant :

Java SE 7
Param<Integer> integerParam = new Param<>();

Contrairement aux autres identificateurs, les paramètres de type ne comportent aucune contrainte de dénomination. Cependant, leurs noms sont généralement la première lettre de leur but en majuscule. (Cela est vrai même dans les JavaDocs officiels.)
Les exemples incluent T pour "type" , E pour "élément" et K / V pour "clé" / "valeur" .


Extension d'une classe générique

public abstract class AbstractParam<T> {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

AbstractParam est une classe abstraite déclarée avec un paramètre de type T Lors de l'extension de cette classe, ce paramètre de type peut être remplacé par un argument de type écrit à l'intérieur de <> ou le paramètre de type peut rester inchangé. Dans les premier et deuxième exemples ci-dessous, String et Integer remplacent le paramètre type. Dans le troisième exemple, le paramètre de type reste inchangé. Le quatrième exemple n'utilise pas de génériques du tout, il est donc similaire à si la classe avait un paramètre Object . Le compilateur avertira que AbstractParam est un type brut, mais il compilera la classe ObjectParam . Le cinquième exemple a 2 paramètres de type (voir «paramètres de type multiples» ci-dessous), en choisissant le deuxième paramètre comme paramètre de type transmis à la super-classe.

public class Email extends AbstractParam<String> {
    // ...
}

public class Age extends AbstractParam<Integer> {
    // ...
}

public class Height<T> extends AbstractParam<T> {
    // ...
}

public class ObjectParam extends AbstractParam {
    // ...
}

public class MultiParam<T, E> extends AbstractParam<E> {
    // ...
}

Ce qui suit est l'utilisation:

Email email = new Email();
email.setValue("[email protected]");
String retrievedEmail = email.getValue();

Age age = new Age();
age.setValue(25);
Integer retrievedAge = age.getValue();
int autounboxedAge = age.getValue();

Height<Integer> heightInInt = new Height<>();
heightInInt.setValue(125);

Height<Float> heightInFloat = new Height<>();
heightInFloat.setValue(120.3f);

MultiParam<String, Double> multiParam = new MultiParam<>();
multiParam.setValue(3.3);

Notez que dans la classe Email , la T getValue() agit comme si elle avait une signature de String getValue() et que la void setValue(T) agit comme si elle avait été déclarée void setValue(String) .

Il est également possible d'instancier avec une classe interne anonyme avec des accolades vides ( {} ):

AbstractParam<Double> height = new AbstractParam<Double>(){};
height.setValue(198.6);

Notez que l' utilisation du diamant avec des classes internes anonymes n'est pas autorisée.


Paramètres de type multiple

Java permet d'utiliser plusieurs paramètres de type dans une classe ou une interface générique. Plusieurs paramètres de type peuvent être utilisés dans une classe ou une interface en plaçant une liste de types séparés par des virgules entre les crochets. Exemple:

public class MultiGenericParam<T, S> {
    private T firstParam;
    private S secondParam;
   
    public MultiGenericParam(T firstParam, S secondParam) {
        this.firstParam = firstParam;
        this.secondParam = secondParam;
    }
    
    public T getFirstParam() {
        return firstParam;
    }
    
    public void setFirstParam(T firstParam) {
        this.firstParam = firstParam;
    }
    
    public S getSecondParam() {
        return secondParam;
    }
    
    public void setSecondParam(S secondParam) {
        this.secondParam = secondParam;
    }
}

L'utilisation peut se faire comme ci-dessous:

MultiGenericParam<String, String> aParam = new MultiGenericParam<String, String>("value1", "value2");
MultiGenericParam<Integer, Double> dayOfWeekDegrees = new MultiGenericParam<Integer, Double>(1, 2.6);

Déclaration d'une méthode générique

Les méthodes peuvent aussi avoir des paramètres de type génériques .

public class Example {

    // The type parameter T is scoped to the method
    // and is independent of type parameters of other methods.
    public <T> List<T> makeList(T t1, T t2) {
        List<T> result = new ArrayList<T>();
        result.add(t1);
        result.add(t2);
        return result;
    }

    public void usage() {
        List<String> listString = makeList("Jeff", "Atwood");
        List<Integer> listInteger = makeList(1, 2);
    }
}

Notez que nous n'avons pas à transmettre un argument de type réel à une méthode générique. Le compilateur déduit l'argument de type pour nous, en fonction du type de cible (par exemple, la variable à laquelle nous attribuons le résultat) ou des types d'arguments réels. Il infère généralement l'argument de type le plus spécifique qui rendra le type d'appel correct.

Parfois, bien que rarement, il peut être nécessaire de remplacer cette inférence de type par des arguments de type explicites:

void usage() {
    consumeObjects(this.<Object>makeList("Jeff", "Atwood").stream());
}

void consumeObjects(Stream<Object> stream) { ... }

Il est nécessaire dans cet exemple parce que le compilateur ne peut pas "regarder en avant" pour voir que Object est désiré pour T après avoir appelé stream() et que cela impliquerait autrement String basé sur les arguments makeList . Notez que le langage Java ne supporte pas omettre la classe ou de l' objet sur lequel la méthode est appelée ( this dans l'exemple ci - dessus) lorsque des arguments de type sont explicitement prévus.

Le diamant

Java SE 7

Java 7 a introduit le Diamond 1 pour supprimer certaines plaques chauffantes autour de l'instanciation de classe générique. Avec Java 7+, vous pouvez écrire:

List<String> list = new LinkedList<>();

Où vous deviez écrire dans les versions précédentes, ceci:

List<String> list = new LinkedList<String>();

L'une des limitations concerne les classes anonymes , dans lesquelles vous devez toujours fournir le paramètre type dans l'instanciation:

// This will compile:

Comparator<String> caseInsensitiveComparator = new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.compareToIgnoreCase(s2);
    }
};

// But this will not:

Comparator<String> caseInsensitiveComparator = new Comparator<>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.compareToIgnoreCase(s2);
    }
};
Java SE 8

Bien que l'utilisation du diamant avec Anonymous Inner Classes ne soit pas prise en charge dans Java 7 et 8, il sera inclus en tant que nouvelle fonctionnalité dans Java 9 .


Note de bas de page:

1 - Certaines personnes appellent l’utilisation <> l’ opérateur diamantaire. Ceci est une erreur. Le diamant ne se comporte pas comme un opérateur et n'est ni décrit ni répertorié en tant qu'opérateur dans les tutoriels Java (officiels) ou JLS. En effet, <> n'est même pas un jeton Java distinct. C'est plutôt un jeton < suivi d'un jeton > , et il est légal (bien que de mauvais style) d'avoir des espaces ou des commentaires entre les deux. Le JLS et les tutoriels se réfèrent systématiquement à <> comme "le diamant", ce qui en fait le terme correct.

Nécessitant plusieurs bornes supérieures ("étend A & B")

Vous pouvez exiger un type générique pour étendre plusieurs limites supérieures.

Exemple: nous voulons trier une liste de numéros, mais le Number n’implémente pas Comparable .

public <T extends Number & Comparable<T>> void sortNumbers( List<T> n ) {
  Collections.sort( n );
}

Dans cet exemple, T doit étendre Number et implémenter Comparable<T> qui devrait correspondre à toutes les implémentations de nombres intégrées "normales" comme Integer ou BigDecimal mais ne correspond pas aux plus exotiques comme Striped64 .

Étant donné que l'héritage multiple n'est pas autorisé, vous pouvez utiliser au plus une classe en tant que limite et ce doit être la première liste. Par exemple, <T extends Comparable<T> & Number> n'est pas autorisé car Comparable est une interface et non une classe.

Créer une classe générique limitée

Vous pouvez restreindre les types valides utilisés dans une classe générique en limitant ce type dans la définition de classe. Étant donné la hiérarchie de type simple suivante:

public abstract class Animal {
    public abstract String getSound();
}

public class Cat extends Animal {
    public String getSound() {
        return "Meow";
    }
}

public class Dog extends Animal {
    public String getSound() {
        return "Woof";
    }
}

Sans génériques limités , nous ne pouvons pas créer une classe de conteneur à la fois générique et sachant que chaque élément est un animal:

public class AnimalContainer<T> {

    private Collection<T> col;

    public AnimalContainer() {
        col = new ArrayList<T>();
    }

    public void add(T t) {
        col.add(t);
    }

    public void printAllSounds() {
        for (T t : col) {
            // Illegal, type T doesn't have makeSound()
            // it is used as an java.lang.Object here
            System.out.println(t.makeSound()); 
        }
    }
}

Avec la liaison générique dans la définition de classe, c'est désormais possible.

public class BoundedAnimalContainer<T extends Animal> { // Note bound here.

    private Collection<T> col;

    public BoundedAnimalContainer() {
        col = new ArrayList<T>();
    }

    public void add(T t) {
        col.add(t);
    }

    public void printAllSounds() {
        for (T t : col) {
            // Now works because T is extending Animal
            System.out.println(t.makeSound()); 
        }
    }
}

Cela limite également les instanciations valides du type générique:

// Legal
AnimalContainer<Cat> a = new AnimalContainer<Cat>();

// Legal
AnimalContainer<String> a = new AnimalContainer<String>();
// Legal because Cat extends Animal
BoundedAnimalContainer<Cat> b = new BoundedAnimalContainer<Cat>();

// Illegal because String doesn't extends Animal
BoundedAnimalContainer<String> b = new BoundedAnimalContainer<String>();

Décider entre `T`,`? super T`, et `? étend T`

La syntaxe des génériques Java limite les caractères génériques, représentant le type inconnu par ? est:

  • ? extends T représente un caractère générique supérieur. Le type inconnu représente un type qui doit être un sous-type de T ou un type T lui-même.

  • ? super T représente un caractère générique limité inférieur. Le type inconnu représente un type qui doit être un supertype de T ou un type T lui-même.

En règle générale, vous devez utiliser

  • ? extends T si vous avez seulement besoin d'un accès "read" ("input")
  • ? super T si vous avez besoin d'un accès "écriture" ("sortie")
  • T si vous avez besoin des deux ("modifier")

Utiliser extends ou super est généralement préférable car cela rend votre code plus flexible (comme dans: autoriser l'utilisation de sous-types et de supertypes), comme vous le verrez ci-dessous.

class Shoe {}
class IPhone {}
interface Fruit {}
class Apple implements Fruit {}
class Banana implements Fruit {}
class GrannySmith extends Apple {}

   public class FruitHelper {

        public void eatAll(Collection<? extends Fruit> fruits) {}

        public void addApple(Collection<? super Apple> apples) {}
}

Le compilateur sera maintenant capable de détecter certaines mauvaises utilisations:

 public class GenericsTest {
      public static void main(String[] args){
  FruitHelper fruitHelper = new FruitHelper() ;
    List<Fruit> fruits = new ArrayList<Fruit>();
    fruits.add(new Apple()); // Allowed, as Apple is a Fruit
    fruits.add(new Banana()); // Allowed, as Banana is a Fruit
    fruitHelper.addApple(fruits); // Allowed, as "Fruit super Apple"
    fruitHelper.eatAll(fruits); // Allowed

    Collection<Banana> bananas = new ArrayList<>();
    bananas.add(new Banana()); // Allowed
    //fruitHelper.addApple(bananas); // Compile error: may only contain Bananas!
    fruitHelper.eatAll(bananas); // Allowed, as all Bananas are Fruits

    Collection<Apple> apples = new ArrayList<>();
    fruitHelper.addApple(apples); // Allowed
    apples.add(new GrannySmith()); // Allowed, as this is an Apple
    fruitHelper.eatAll(apples); // Allowed, as all Apples are Fruits.
    
    Collection<GrannySmith> grannySmithApples = new ArrayList<>();
    fruitHelper.addApple(grannySmithApples); //Compile error: Not allowed.
                                   // GrannySmith is not a supertype of Apple
    apples.add(new GrannySmith()); //Still allowed, GrannySmith is an Apple
    fruitHelper.eatAll(grannySmithApples);//Still allowed, GrannySmith is a Fruit

    Collection<Object> objects = new ArrayList<>();
    fruitHelper.addApple(objects); // Allowed, as Object super Apple
    objects.add(new Shoe()); // Not a fruit
    objects.add(new IPhone()); // Not a fruit
    //fruitHelper.eatAll(objects); // Compile error: may contain a Shoe, too!
}

Choisir le bon T , ? super T ou ? extends T est nécessaire pour permettre l'utilisation avec des sous-types. Le compilateur peut alors assurer la sécurité du type; vous ne devriez pas avoir besoin de lancer (ce qui n'est pas sûr et peut causer des erreurs de programmation) si vous les utilisez correctement.

Si ce n'est pas facile à comprendre, n'oubliez pas la règle PECS :

P roducer utilise des "E" et C xtends utilise onsumer "S uper".

(Le producteur n'a qu'un accès en écriture et le consommateur n'a qu'un accès en lecture)

Avantages de la classe et de l'interface génériques

Le code qui utilise des génériques présente de nombreux avantages par rapport au code non générique. Voici les principaux avantages


Contrôles de type plus forts au moment de la compilation

Un compilateur Java applique une vérification de type forte au code générique et génère des erreurs si le code viole la sécurité de type. Corriger les erreurs de compilation est plus facile que de corriger les erreurs d'exécution, ce qui peut être difficile à trouver.


Elimination des moulages

L'extrait de code suivant sans génériques nécessite la conversion en:

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

Lors de la réécriture pour utiliser des génériques , le code ne nécessite pas de conversion:

List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);   // no cast

Permettre aux programmeurs d'implémenter des algorithmes génériques

En utilisant des génériques, les programmeurs peuvent implémenter des algorithmes génériques qui fonctionnent sur des collections de différents types, peuvent être personnalisés et sont de type sûr et plus faciles à lire.

Liaison du paramètre générique à plus de 1 type

Les paramètres génériques peuvent également être liés à plusieurs types à l'aide de la syntaxe T extends Type1 & Type2 & ... .

Supposons que vous souhaitiez créer une classe dont le type générique devrait implémenter Flushable et Closeable , vous pouvez écrire

class ExampleClass<T extends Flushable & Closeable> {
}

Maintenant, ExampleClass accepte uniquement comme paramètres génériques, les types qui implémentent à la fois Flushable et Closeable .

ExampleClass<BufferedWriter> arg1; // Works because BufferedWriter implements both Flushable and Closeable

ExampleClass<Console> arg4; // Does NOT work because Console only implements Flushable
ExampleClass<ZipFile> arg5; // Does NOT work because ZipFile only implements Closeable

ExampleClass<Flushable> arg2; // Does NOT work because Closeable bound is not satisfied.
ExampleClass<Closeable> arg3; // Does NOT work because Flushable bound is not satisfied.

Les méthodes de classe peuvent choisir d'inférer des arguments de type générique comme Closeable ou Flushable .

class ExampleClass<T extends Flushable & Closeable> {
    /* Assign it to a valid type as you want. */
    public void test (T param) {
        Flushable arg1 = param; // Works
        Closeable arg2 = param; // Works too.
    }

    /* You can even invoke the methods of any valid type directly. */
    public void test2 (T param) {
        param.flush(); // Method of Flushable called on T and works fine.
        param.close(); // Method of Closeable called on T and works fine too.
    }
}

Remarque:

Vous ne pouvez pas lier le paramètre générique à l'un des types utilisant la clause OR ( | ). Seule la clause AND ( & ) est prise en charge. Le type générique ne peut étendre qu'une classe et de nombreuses interfaces. La classe doit être placée au début de la liste.

Instancier un type générique

En raison de l'effacement de type, les éléments suivants ne fonctionneront pas:

public <T> void genericMethod() {
    T t = new T(); // Can not instantiate the type T.
}

Le type T est effacé. Puisque, à l'exécution, la JVM ne sait pas ce que T était à l'origine, elle ne sait pas quel constructeur appeler.


Solutions de contournement

  1. Passer la classe de T lors de l'appel de genericMethod :

    public <T> void genericMethod(Class<T> cls) {
        try {
            T t = cls.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
             System.err.println("Could not instantiate: " + cls.getName());
        }
    }
    
    genericMethod(String.class);
    

    Ce qui génère des exceptions, car il n'y a aucun moyen de savoir si la classe passée a un constructeur par défaut accessible.

Java SE 8
  1. Passer une référence au constructeur de T :

    public <T> void genericMethod(Supplier<T> cons) {
        T t = cons.get();
    }
    
    genericMethod(String::new);
    

Se référant au type générique déclaré dans sa propre déclaration

Comment allez-vous utiliser une instance d'un type générique (éventuellement supplémentaire) hérité dans une déclaration de méthode dans le type générique lui-même déclaré? C'est l'un des problèmes que vous rencontrerez lorsque vous approfondirez un peu les génériques, mais que vous rencontrerez un problème assez commun.

Supposons que nous ayons un type DataSeries<T> (interface ici), qui définit une série de données générique contenant des valeurs de type T Il est fastidieux de travailler directement avec ce type lorsque nous souhaitons effectuer de nombreuses opérations avec, par exemple, des valeurs doubles. Nous définissons donc DoubleSeries extends DataSeries<Double> . Supposons maintenant que le type DataSeries<T> ait une méthode add(values) qui ajoute une autre série de la même longueur et en renvoie une nouvelle. Comment pouvons-nous appliquer le type de values et le type de retour à DoubleSeries plutôt qu'à DataSeries<Double> dans notre classe dérivée?

Le problème peut être résolu en ajoutant un paramètre de type générique renvoyant à et étendant le type en cours de déclaration (appliqué à une interface ici, mais il en va de même pour les classes):

public interface DataSeries<T, DS extends DataSeries<T, DS>> {
    DS add(DS values);
    List<T> data();
}

Ici, T représente le type de données que la série contient, par exemple Double et DS la série elle-même. Un type hérité (ou des types) peut maintenant être facilement implémenté en substituant le paramètre mentionné ci-dessus par un type dérivé correspondant, produisant ainsi une définition concrète à Double base du formulaire:

public interface DoubleSeries extends DataSeries<Double, DoubleSeries> {
    static DoubleSeries instance(Collection<Double> data) {
        return new DoubleSeriesImpl(data);
    }
}

À ce moment, même un IDE implémentera l'interface ci-dessus avec les types corrects en place, ce qui, après un peu de remplissage de contenu, pourrait ressembler à ceci:

class DoubleSeriesImpl implements DoubleSeries {
    private final List<Double> data;

    DoubleSeriesImpl(Collection<Double> data) {
        this.data = new ArrayList<>(data);
    }

    @Override
    public DoubleSeries add(DoubleSeries values) {
        List<Double> incoming = values != null ? values.data() : null;
        if (incoming == null || incoming.size() != data.size()) {
            throw new IllegalArgumentException("bad series");
        }
        List<Double> newdata = new ArrayList<>(data.size());
        for (int i = 0; i < data.size(); i++) {
            newdata.add(this.data.get(i) + incoming.get(i)); // beware autoboxing
        }
        return DoubleSeries.instance(newdata);
    }

    @Override
    public List<Double> data() {
        return Collections.unmodifiableList(data);
    }
}

Comme vous pouvez le voir, la méthode add est déclarée en tant que DoubleSeries add(DoubleSeries values) et le compilateur est satisfait.

Le modèle peut être imbriqué si nécessaire.

Utilisation de instanceof avec Generics

Utiliser des génériques pour définir le type dans instanceof

Considérons la classe générique suivante Example déclaré avec le paramètre formel <T> :

class Example<T> {
    public boolean isTypeAString(String s) {
        return s instanceof T; // Compilation error, cannot use T as class type here
    }
}

Cela provoquera toujours une erreur de compilation car dès que le compilateur compile le source Java en bytecode Java, il applique un processus appelé effacement de type , qui convertit tout le code générique en code non générique, rendant impossible la distinction entre les types T au moment de l'exécution. Le type utilisé avec instanceof doit être reifiable , ce qui signifie que toutes les informations sur le type doivent être disponibles au moment de l'exécution, ce qui n'est généralement pas le cas pour les types génériques.

La classe suivante représente à quoi ressemblent deux classes différentes d' Example , Example<String> et Example<Number> après que les génériques ont été supprimés par type d'effacement :

class Example { // formal parameter is gone
    public boolean isTypeAString(String s) {
        return s instanceof Object; // Both <String> and <Number> are now Object
    }
}

Comme les types ont disparu, la JVM ne peut pas savoir quel type est T


Exception à la règle précédente

Vous pouvez toujours utiliser des caractères génériques sans limite (?) Pour spécifier un type dans l' instanceof comme suit:

    public boolean isAList(Object obj) {
        return obj instanceof List<?>;
    }

Cela peut être utile pour évaluer si une instance obj est une List ou non:

System.out.println(isAList("foo")); // prints false
System.out.println(isAList(new ArrayList<String>()); // prints true
System.out.println(isAList(new ArrayList<Float>()); // prints true

En fait, les caractères génériques non liés sont considérés comme un type reifiable.


Utiliser une instance générique avec instanceof

Le revers de la médaille est que l'utilisation d'une instance t de T avec instanceof est légale, comme le montre l'exemple suivant:

class Example<T> {
    public boolean isTypeAString(T t) {
        return t instanceof String; // No compilation error this time
    }
}

car après le type d'effacement, la classe ressemblera à ceci:

class Example { // formal parameter is gone
    public boolean isTypeAString(Object t) {
        return t instanceof String; // No compilation error this time
    }
}

Étant donné que, même si l'effacement de type se produit de toute façon, la machine virtuelle Java peut maintenant distinguer différents types de mémoire, même s'ils utilisent le même type de référence ( Object ), comme le montre l'extrait suivant:

Object obj1 = new String("foo"); // reference type Object, object type String
Object obj2 = new Integer(11); // reference type Object, object type Integer
System.out.println(obj1 instanceof String); // true
System.out.println(obj2 instanceof String); // false, it's an Integer, not a String

Différentes manières d'implémenter une interface générique (ou d'étendre une classe générique)

Supposons que l'interface générique suivante ait été déclarée:

public interface MyGenericInterface<T> {
    public void foo(T t);
}

Vous trouverez ci-dessous la liste des moyens possibles pour la mettre en œuvre.


Implémentation de classe non générique avec un type spécifique

Choisissez un type spécifique pour remplacer le paramètre de type formel <T> de MyGenericClass et implémentez-le, comme le montre l'exemple suivant:

public class NonGenericClass implements MyGenericInterface<String> {
    public void foo(String t) { } // type T has been replaced by String
}

Cette classe ne traite que de String , ce qui signifie que l'utilisation de MyGenericInterface avec des paramètres différents (par exemple, Integer , Object etc.) ne sera pas compilée, comme le montre l'extrait de code suivant:

NonGenericClass myClass = new NonGenericClass();
myClass.foo("foo_string"); // OK, legal
myClass.foo(11); // NOT OK, does not compile
myClass.foo(new Object()); // NOT OK, does not compile

Implémentation de classe générique

Déclarez une autre interface générique avec le paramètre de type formel <T> qui implémente MyGenericInterface , comme suit:

public class MyGenericSubclass<T> implements MyGenericInterface<T> {
    public void foo(T t) { } // type T is still the same
    // other methods...
}

Notez qu'un paramètre de type formel différent peut avoir été utilisé, comme suit:

public class MyGenericSubclass<U> implements MyGenericInterface<U> { // equivalent to the previous declaration
    public void foo(U t) { }
    // other methods...
}

Implémentation de la classe de type brute

Déclarez une classe non générique qui implémente MyGenericInteface tant que type brut (sans utiliser de générique du tout), comme suit:

public class MyGenericSubclass implements MyGenericInterface {
    public void foo(Object t) { } // type T has been replaced by Object
    // other possible methods
}

Cette méthode n'est pas recommandée, car elle n'est pas sécurisée à 100% à l'exécution, car elle mélange le type brut (de la sous-classe) avec les génériques (de l'interface), ce qui est également source de confusion. Les compilateurs Java modernes émettront un avertissement avec ce type d'implémentation, néanmoins le code - pour des raisons de compatibilité avec les anciennes JVM (1.4 ou antérieures) - sera compilé.


Toutes les méthodes énumérées ci-dessus sont également autorisées lorsque vous utilisez une classe générique en tant que super-type au lieu d'une interface générique.

Utilisation de génériques pour la diffusion automatique

Avec les génériques, il est possible de renvoyer tout ce que l'appelant attend:

private Map<String, Object> data;
public <T> T get(String key) {
    return (T) data.get(key);
}

La méthode compilera avec un avertissement. Le code est en fait plus sûr qu'il n'y paraît, car le runtime Java effectuera un transtypage lorsque vous l'utiliserez:

Bar bar = foo.get("bar");

C'est moins sûr quand vous utilisez des types génériques:

List<Bar> bars = foo.get("bars");

Ici, la distribution fonctionnera lorsque le type renvoyé est un type quelconque de List (c’est-à-dire que le retour de List<String> ne déclenche pas une ClassCastException ; vous l’obtiendrez éventuellement lors de la sortie d’éléments de la liste).

Pour contourner ce problème, vous pouvez créer une API qui utilise des clés tapées:

public final static Key<List<Bar>> BARS = new Key<>("BARS");

avec cette méthode put() :

public <T> T put(Key<T> key, T value);

Avec cette approche, vous ne pouvez pas insérer le mauvais type dans la carte, le résultat sera donc toujours correct (à moins que vous ne créiez accidentellement deux clés portant le même nom mais des types différents).

En relation:

Obtenir une classe qui vérifie le paramètre générique à l'exécution

De nombreux paramètres génériques non liés, tels que ceux utilisés dans une méthode statique, ne peuvent pas être récupérés à l'exécution (voir Autres threads sur l' effacement ). Cependant, une stratégie commune est utilisée pour accéder au type satisfaisant un paramètre générique sur une classe à l'exécution. Cela permet un code générique qui dépend de l'accès au type sans avoir à envoyer des informations de type à chaque appel.

Contexte

Le paramétrage générique sur une classe peut être inspecté en créant une classe interne anonyme. Cette classe capturera les informations de type. En général, ce mécanisme est désigné sous le nom de jeton de type super , qui est détaillé dans l' article du blog de Neal Gafter .

Les implémentations

Trois implémentations courantes en Java sont:

Exemple d'utilisation

public class DataService<MODEL_TYPE> {
     private final DataDao dataDao = new DataDao();
     private final Class<MODEL_TYPE> type = (Class<MODEL_TYPE>) new TypeToken<MODEL_TYPE>
                                                                (getClass()){}.getRawType();
     public List<MODEL_TYPE> getAll() {
         return dataDao.getAllOfType(type);
    }
}

// the subclass definitively binds the parameterization to User
// for all instances of this class, so that information can be 
// recovered at runtime
public class UserService extends DataService<User> {}

public class Main {
    public static void main(String[] args) {
          UserService service = new UserService();
          List<User> users = service.getAll();
    }
}


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow