Les génériques Java expliqués sous forme courte avec des exemples

Veuillez consulter cette FAQ pour plus d’informations :

http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html#SECTION1

Voici en bref quelques points importants de cette FAQ.

Exemple (de type générique) :

class Pair<X,Y>  { 
  private X first;
  private Y second;   public Pair(X a1, Y a2) {
    first  = a1;
    second = a2;
  }
  public X getFirst()  { return first; }
  public Y getSecond() { return second; }
  public void setFirst(X arg)  { first = arg; }
  public void setSecond(Y arg) { second = arg; }
}

La classe Paire a deux paramètres de type X et Oui .  Ils sont remplacés par des arguments de type lorsque le type générique Paire est instancié. Par exemple, dans la déclaration Paire<Chaîne, Date> le paramètre de type X est remplacé par l’argument type Chaîne et Oui is replaced by  Date .
La portée des identifiants X et Oui est la définition complète de la classe.  Dans cette portée, les deux paramètres de type X et Oui sont utilisés comme s’il s’agissait de types (avec quelques restrictions). Dans l’exemple ci-dessus, les paramètres de type sont utilisés comme argument et type de retour des méthodes d’instance et types de champs d’instance.
Les paramètres de type peuvent être déclarés avec des limites. Les limites donnent accès aux méthodes du type inconnu représenté par le paramètre type. Dans notre exemple, nous n’invoquons aucune méthode de type inconnu X et Oui . Pour cette raison, les deux paramètres de type ne sont pas limités.

Exemple (d’un type paramétré concret) :

paire d'impression vide publique ( Paire<String,Long> paire) {
  System.out.println("("+pair.getFirst()+","+pair.getSecond()+")");
} Paire<String,Long> limite = nouveau Paire<String,Long> ("maximum",1024L);
printPair(limite);

Exemple (d’un type paramétré par caractère générique) :

paire d'impression vide publique ( Paire<?,?> paire) {
  System.out.println("("+pair.getFirst()+","+pair.getSecond()+")");
} Paire<?,?> limite = nouveau Paire<String,Long> ("maximum",1024L);
printPair(limite);

Toutes les instanciations d’un type générique partagent la même représentation du type d’exécution, à savoir la représentation du type brut. Par exemple, les instanciations d’un type générique Liste , tel que Liste<Date> , Liste<Chaîne> , Liste<Long> , etc. ont des types statiques différents au moment de la compilation, mais le même type dynamique Liste au moment de l’exécution.

Un casting se compose de deux parties :

  • une vérification de type statique effectuée par le compilateur au moment de la compilation et
  • une vérification de type dynamique effectuée par la machine virtuelle au moment de l’exécution.

La partie statique trie les castings absurdes, qui ne peuvent pas réussir, comme le casting de Chaîne que Date ou de Liste<Chaîne> que Liste<Date> .

La partie dynamique utilise les informations de type d’exécution et effectue une vérification de type au moment de l’exécution.  Cela soulève un ClassCastException si le type dynamique de l’objet n’est pas le type cible (ou un sous-type du type cible) de la distribution. Des exemples de moulages avec une partie dynamique sont les moulages de Objet que Chaîne ou de Objet que Liste<Chaîne> .  Ce sont ce qu’on appelle les downcasts, d’un supertype à un sous-type.

Des exemples de types paramétrés concrets sont Liste<Chaîne> , Carte<String,Date> , mais pas Liste<? étend le numéro> ou Carte<String,?> .

On s’attend parfois à ce qu’un Liste<Objet> serait un supertype de Liste<Chaîne> , parce que Objet est un supertype de Chaîne .  This expectation stems from the fact that such a type relationship exists for arrays:  Objet[] est un supertype de Chaîne[] , parce que Objet est un supertype de Chaîne . (Ce type de relation est connu sous le nom de covariance .) La relation de super-sous-type des types de composants s’étend aux types de tableau correspondants. Aucune relation de type de ce type n’existe pour les instanciations de types génériques. (Les types paramétrés sont pas covariante.)

L’absence de relation de super-sous-type entre les instanciations du même type générique a diverses conséquences.  Voici un exemple.

Exemple:

void printAll (ArrayList<Object> c) {
  for (Object o : c) 
    System.out.println(o);
} ArrayList<String> liste = new ArrayList<String>();
... remplir la liste ...
printAll(liste);   // erreur

UN TableauListe<Chaîne> l’objet ne peut pas être passé en argument à une méthode qui demande un TableauListe<Objet> car les deux types sont des instanciations du même type générique, mais pour différent arguments de type, et pour cette raison, ils ne sont pas compatibles les uns avec les autres.

Les types paramétrés concrets sont des instanciations concrètes d’un type générique.  Ce sont presque des types ; il n’y a que quelques restrictions.  Ils ne peuvent PAS être utilisés aux fins suivantes :

  • pour la création de tableaux
  • dans la gestion des exceptions
  • dans une classe littérale
  • dans un instance de expression

Les tableaux contenant des éléments dont le type est un type paramétré concret sont illégaux.

Exemple (de type de tableau illégal) :

test de vide statique() {
  Paire<Integer,Integer>[] intPairArr = nouveau Paire<Integer,Integer>[10] ; // erreur
  addElements(intPairArr); 
  Pair<Integer,Integer> pair = intPairArr[1];
  Integer i = pair.getFirst();
  pair.setSecond(i);
}
addElements vides statiques ( Objet[] objArr) {
  objArr[0] = new Pair<Integer,Integer>(0,0);
  objArr[1] = new Pair<String,String>("","");      // should fail with ArrayStoreException
}

Le compilateur interdit la création de tableaux dont le type de composant est un type paramétré concret, comme Paire<Integer,Integer> dans notre exemple.  Nous avons expliqué dans l’entrée précédente pourquoi il est raisonnable que le compilateur qualifie un Paire<Integer,Integer>[] comme illégal.  Le problème clé est que le compilateur et le système d’exécution doivent garantir qu’un tableau est un homogène séquence d’éléments du même type.  L’une des vérifications de type, à savoir la vérification du magasin de tableau effectuée par la machine virtuelle au moment de l’exécution, ne parvient pas à détecter l’insertion incriminée d’un élément étranger. Dans l’exemple, la deuxième insertion dans le ajouter des éléments La méthode devrait échouer, car ils ajoutaient une paire de chaînes à un tableau de valeurs intégrales, mais elle n’échoue pas comme prévu. Les raisons ont été discutées dans l’entrée précédente.

Si nous ne pouvons pas utiliser de tableaux contenant des éléments dont le type est un type paramétré concret, qu’utilisons-nous comme solution de contournement ?

Considérons 3 solutions de contournement envisageables :

  • tableau de types bruts
  • tableau de type paramétré par caractère générique illimité
  • collection au lieu d’un tableau

Bien que les tableaux de types paramétrés concrets soient illégaux, les collections de types paramétrés concrets sont autorisées.

Un type paramétré par caractère générique est une instanciation d’un type générique où au moins un argument de type est un caractère générique.  Des exemples de types paramétrés avec des caractères génériques sont Collection<?> , Liste<? étend le numéro> , Comparateur<? super chaîne> et Paire<String,?> . Un type paramétré générique désigne une famille de types comprenant des instanciations concrètes d’un type générique.  Le type de caractère générique utilisé détermine quels types paramétrés concrets appartiennent à la famille.  Par exemple, le type paramétré par caractère générique Collection<?> désigne la famille de toutes les instanciations du Collection interface quel que soit l’argument de type.  Le type paramétré par caractère générique Liste<? étend le numéro> denotes the family of all list types where the element type is a subtype of  Nombre .  Le type paramétré par caractère générique Comparateur<? super chaîne> est la famille de toutes les instanciations du Comparateur interface pour les types d’arguments de type qui sont des supertypes de Chaîne.

Un type paramétré générique n’est pas un type au sens habituel (différent d’une classe/interface non paramétrée ou d’un type brut).

Les types paramétrés par caractères génériques peuvent être utilisés pour la saisie (comme les classes et les interfaces non paramétrées) :

  • comme argument et types de méthodes de retour
  • comme type de champ ou de variable de référence locale
  • comme type de composant d’un tableau comme argument de type d’autres types paramétrés
  • comme type de cible dans les lancers

Non seulement les types peuvent être génériques, mais les méthodes peuvent également l’être. Les méthodes statiques et non statiques ainsi que les constructeurs peuvent avoir des paramètres de type. La syntaxe de déclaration des paramètres de type formel est similaire à la syntaxe des types génériques. La section des paramètres de type est délimitée par des crochets angulaires et apparaît avant le type de retour de la méthode. Sa syntaxe et sa signification sont identiques à la liste de paramètres de type d’un type générique.

Voici l’exemple d’un générique maximum méthode qui calcule la plus grande valeur dans une collection d’éléments d’un type inconnu UN .

Exemple (d’une méthode générique) :

Collections de classe {
  public static <A étend Comparable<A>> A max(Collection< UN >xs) {
    Iterator< UN > xi = xs.iterator();
    UN w = xi.next();
    while (xi.hasNext()) {
      UN x = xi.next();
      if (w.compareTo(x) < 0) w = x;
    }
    return w;
  }
} 

Le maximum La méthode a un paramètre de type, nommé UN .  Il s’agit d’un espace réservé pour le type d’élément de la collection sur laquelle la méthode fonctionne. Le paramètre type a une limite ; ce doit être un type qui est un sous-type de Comparable<A>

Exemple (du même type générique, cette fois avec bornes):

interface publique Comparable<T> {
  public int compareTo(T arg);
}
classe publique TreeMap<Key étend Comparable<Key> ,Données>{
  private static class Entry<K,V> {
     K key;
     V value;
     Entry<K,V> left = null;
     Entry<K,V> right = null;
     Entry<K,V> parent;
  }
  private transient Entry<Key,Data> root = null;
  ...
  private Entry<Key,Data> getEntry(Key key) {
     Entry<Key,Data> p = root;
     Key k = key;
     while (p != null) {
       int cmp = k. comparerÀ(p.key) ; 
       if (cmp == 0)
         return p;
       else if (cmp < 0)
         p = p.left;
       else 
         p = p.right; 
     }
     return null;
  }
  public boolean containsKey(Key key) {
     return getEntry(key) != null;
  }
  ...
}

Dans l’exemple ci-dessus, le paramètre type Clé a la limite Comparable<Clé> .  La spécification d’une limite a deux effets :

  • Il donne accès aux méthodes spécifiées par la limite .  Dans l’exemple, la limite Comparable<Clé> donne accès à comparerÀ méthode que nous souhaitons invoquer dans la mise en œuvre de notre ArbreCarte classe.
  • Seuls les types « dans les limites » peuvent être utilisés pour l’instanciation du type générique.   In the example, a parameterized type such as TreeMap<Number,String> serait rejeté, car le type Nombre n’est pas un sous-type de Comparable<Numéro> .  Un type paramétré comme TreeMap<Chaîne,Chaîne> serait accepté, car le type String est dans les limites, c’est-à-dire est un sous-type de Comparable<String> .

Toutes les classes, interfaces et types d’énumération peuvent être utilisés comme paramètres de type liés, y compris les types imbriqués et internes.  Ni les types primitifs ni les types de tableaux ne doivent être utilisés comme paramètre de type lié.

Exemples (de limites de paramètres de type) :

la classe X0 <T s'étend int > { ... }      // erreur
la classe X1 <T s'étend Objet[] > { ... } // erreur
la classe X2 <T s'étend Nombre > { ... }
la classe X3 <T s'étend Chaîne > { ... }
la classe X4 <T s'étend Exécutable > { ... }
la classe X5 <T s'étend Thread.State > { ... }
la classe X6 <T s'étend Liste > { ... }
la classe X7 <T s'étend Liste<Chaîne> > { ... }
la classe X8 <T s'étend Liste<? étend le numéro> > { ... }
la classe X9 <T s'étend Comparable<? super numéro> > { ... }
la classe X10<T s'étend Map.Entry<?,?> > { ... }

L’exemple de code montre que les types primitifs tels que int et les types de tableaux tels que Objet[] ne sont pas autorisés en tant que paramètres de type liés.

Types de classes, tels que Nombre ou Chaîne et les types d’interface, tels que Exécutable , sont autorisés en tant que paramètres de type liés.

Types d’énumération, tels que Thread.State sont également autorisés en tant que paramètres de type liés. Thread.State est un exemple de type imbriqué utilisé comme paramètre de type lié.  Les types intérieurs non statiques sont également autorisés.

Les types bruts sont autorisés en tant que paramètres de type liés ; Liste est un exemple.

Les types paramétrés sont autorisés en tant que paramètres de type liés, y compris les types paramétrés concrets tels que Liste<Chaîne> , types paramétrés génériques limités tels que Liste<? étend le numéro> et Comparable<? super Long> , et les types paramétrés génériques illimités tels que Map.Entry<?,?> .  Une limite qui est un type paramétré par caractère générique autorise comme argument de type tous les types appartenant à la famille de types désignée par le caractère générique.  La limite de type paramétrée par caractère générique ne donne qu’un accès restreint aux champs et aux méthodes ; les restrictions dépendent du type de caractère générique.

Exemple (de type paramétré par caractère générique en tant que paramètre de type lié) :

classe X< T étend List< ? étend le numéro> > {
  public void someMethod(T t) {
    t.add(new Long(0L));    // erreur
    Number n = t.remove(0);
  }
}
classe Test {
  public static void main(String[] args) {
     X<ArrayList< Long >>   x1 = new X<ArrayList<Long>>(); 
     X<ArrayList< Chaîne >> x2 = nouveau X<ArrayList<String>>(); // erreur
  }
}

Variables de référence de type T (le paramètre de type) sont traités comme des variables de référence d’un type générique (le paramètre de type lié).  Dans notre exemple, la conséquence est que le compilateur rejette l’invocation de méthodes qui prennent un argument du type “inconnu” que représente le paramètre type, tel que Liste.ajouter , car la limite est un type paramétré par caractère générique avec une limite supérieure.
En même temps, la limite Liste<? étend le numéro> détermine les types qui peuvent être utilisés comme arguments de type. Le compilateur accepte tous les arguments de type appartenant à la famille de types Liste<? étend le numéro> , c’est-à-dire tous les sous-types de Liste avec un argument de type qui est un sous-type de Nombre .

Un paramètre de type peut être utilisé comme limite d’un autre paramètre de type.

Exemple (d’un paramètre de type utilisé comme paramètre de type lié) :

classe Triple <T> {
  private T fst, snd, trd;
  public < U étend T , V étend T , W étend T > Triple (U arg1, V arg2, W arg3) {
    fst = arg1;
    snd = arg2;
    trd = arg3;
  }
}

Dans cet exemple, le paramètre type T de la classe paramétrée est utilisée comme limite des paramètres de type U ,  V et W d’une méthode d’instance paramétrée de cette classe.

D’autres possibilités d’utilisation de paramètres de type comme limites d’autres paramètres de type incluent les situations dans lesquelles un type imbriqué est défini dans un type générique ou une classe locale est définie dans une méthode générique.  Il est même permis d’utiliser un paramètre de type comme limite d’un autre paramètre de type dans la même section de paramètres de type.

Une limite qui est une classe donne accès à tous ses membres publics, c’est-à-dire aux champs publics, aux méthodes et aux types imbriqués. Seuls les constructeurs ne sont pas rendus accessibles, car il n’y a aucune garantie qu’une sous-classe de la limite ait les mêmes constructeurs que la limite.

Exemple (d’une classe utilisée comme limite d’un paramètre de type) :

classe publique SuperClasse {     // static members
    public enum EnumType {THIS, THAT}
    public static Object staticField;
    public static void staticMethod() { ... }

    // non-static members
    public class InnerClass { ... }
    public Object nonStaticField;
    public void nonStaticMethod() { ... }

    // constructors
    public SuperClass() { ... }

    // private members
    private Object privateField; 

    ...
}

la classe finale publique SomeClass<T s'étend SuperClasse > {
    private T object;
    public SomeClass(T t) { object = t; } 

    public String toString() {
        return
         "static nested type    : "+T.EnumType.class+"\n" 
        +"static field          : "+T.staticField+"\n"
        +"static method         : "+T.staticMethod()+"\n"
        +"non-static nested type: "+T.InnerClass.class+"\n"
        +"non-static field      : "+object.nonStaticField+"\n"
        +"non-static method     : "+object.nonStaticMethod()+"\n"
        +"constructor           : "+(new T())+"\n"                    // error
        +"private member        : "+object.privateField+"\n"          // error 
        ;
    }
}

La limite SuperClasse donne accès à ses types imbriqués, ses champs et méthodes statiques et ses champs et méthodes non statiques.  Seul le constructeur n’est pas accessible.  C’est parce que les constructeurs ne sont pas hérités. Chaque sous-classe définit ses propres constructeurs et n’a pas besoin de prendre en charge les constructeurs de sa superclasse.  Il n’y a donc aucune garantie qu’une sous-classe de SuperClasse aura le même constructeur que sa superclasse.

Le contexte dans lequel Enum<E étend Enum<E>> ” apparaît est la déclaration du Énumération cours en paquet java.lang :

classe abstraite publique Enum<E étend Enum<E>> {
  ...
}

Le genre Énumération est la classe de base commune à tous les types d’énumération.  En Java, un type d’énumération tel que Couleur est traduit en classe Couleur qui s’étend Énumération<Couleur> . Le but de la superclasse Énumération est de fournir une fonctionnalité commune à tous les types d’énumération.

Voici un croquis de la classe Énumération :

classe abstraite publique Énumération< E étend Enum<E>> implémente Comparable< E >, sérialisable {
  private final String name;
  public  final String name() { ... }   private final int ordinal;
  public  final int ordinal() { ... }

  protected Enum(String name, int ordinal) { ... }

  public String           toString() { ... }
  public final boolean    equals(Object other) { ... }
  public final int        hashCode() { ... }
  protected final Object  clone() throws CloneNotSupportedException { ... }
  public final int        compareTo( E o) { ... }

  public final Class< E > getDeclaringClass() { ... }
  public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { ... }
}

La caractéristique surprenante de la déclaration “ Enum<E étend Enum<E>> ” est le fait que la classe nouvellement définie Énumération et son paramètre de type nouvellement défini E apparaissent dans la limite de ce même paramètre de type.  Cela signifie que le Énumération le type doit être instancié pour l’un de ses sous-types.  Afin de comprendre pourquoi cela a du sens, considérons que chaque type d’énumération est traduit en un sous-type de Énumération .

Voici le type d’énumération artificiel Couleur :

énumération Couleur {ROUGE, BLEU, VERT}

Le compilateur le traduit dans la classe suivante :

cours final public Couleur s'étend Énumération<Couleur> {
  public static final Color[] values() { return (Color[])$VALUES.clone(); }
  public static Color valueOf(String name) { ... }   private Color(String s, int i) { super(s, i); }

  public static final Color RED;
  public static final Color BLUE;
  public static final Color GREEN;

  private static final Color $VALUES[];

  static {
    RED = new Color("RED", 0);
    BLUE = new Color("BLUE", 1);
    GREEN = new Color("GREEN", 2);
    $VALUES = (new Color[] { RED, BLUE, GREEN });
  }
}

L’héritage a pour effet que le Couleur type hérite de toutes les méthodes implémentées dans Énumération<Couleur> .  Parmi eux se trouve le comparerÀ méthode.  Le Couleur. comparerÀ la méthode devrait probablement prendre un Couleur comme argument.  Pour que cela se produise, classe Énumération est générique et le Enum.compareTo la méthode prend Énumération Le paramètre de type de E comme argument.  En conséquence, tapez Couleur dérivé de Énumération<Couleur> hérite d’un comparerÀ méthode qui prend un Couleur comme et argument, exactement comme il se doit.
Si l’on décortique la déclaration “ Enum<E étend Enum<E>> ” nous pouvons voir que ce modèle a plusieurs aspects.

Premièrement, il y a le fait que le paramètre de type lié est le type lui-même : “ Énumération <E étend Énumération <E>> “. Il s’assure que seuls les sous-types de type Énumération sont autorisés comme arguments de type. (En théorie, tapez Énumération pourrait être instancié sur lui-même, comme dans Énumération<Enum> , mais ce n’est certainement pas prévu et il est difficile d’imaginer une situation dans laquelle une telle instanciation serait utile.)

Deuxièmement, il y a le fait que le type lié au paramètre est le type paramétré Énumération <E> , qui utilise le paramètre type E comme argument de type de la limite. Cette déclaration garantit que la relation d’héritage entre un sous-type et une instanciation de Énumération est de la forme X étend Enum<X> “. Un sous-type tel que “ X étend Enum<Y> ” ne peut pas être déclaré à cause de l’argument de type Oui ne serait pas dans les limites; seulement des sous-types de Énumération<X> sont dans les limites.

Troisièmement, il y a le fait que Énumération est générique en premier lieu.  Cela signifie que certaines des méthodes de classe Énumération prendre un argument ou renvoyer une valeur d’un type inconnu (ou dépendre d’un type inconnu). Comme nous le savons déjà, ce type inconnu sera plus tard un sous-type X de Énumération<X> .  Ainsi, dans le type paramétré Énumération<X> , ces méthodes impliquent le sous-type X , et ils sont hérités dans le sous-type X .  Le comparerÀ la méthode est un exemple d’une telle méthode ; il est hérité de la superclasse dans chaque sous-classe et possède une signature spécifique à la sous-classe dans chaque cas.

Pour résumer, la déclaration “ Enum<E étend Enum<E>> ” peut être déchiffré comme suit : Énumération est un type générique qui ne peut être instancié que pour ses sous-types, et ces sous-types hériteront de certaines méthodes utiles, dont certaines prennent des arguments spécifiques au sous-type (ou dépendent du sous-type).

Paramètres de type de limite inférieure des classes

Les paramètres de type peuvent avoir plusieurs limites, comme dans la classe Box<T étend Number> {...} . Mais un paramètre de type ne peut avoir aucune limite inférieure, c’est-à-dire une construction telle que Boîte de classe<T super Numéro> {...} n’est pas autorisé.  Pourquoi pas?  La réponse est : cela ne sert à rien car cela ne vous achèterait rien si cela était autorisé.  Voyons pourquoi les paramètres de type de limite inférieure des classes prêtent à confusion en explorant ce que signifie une limite supérieure sur un paramètre de type.

La limite supérieure d’un paramètre de type a trois effets :

    1. Instanciation restreinte. La limite supérieure restreint l’ensemble des types pouvant être utilisés pour l’instanciation du type générique.  Si nous déclarons un la classe Box<T étend Number> {...} alors le compilateur garantirait que seuls les sous-types de Nombre peut être utilisé comme argument de type.  C’est-à-dire un Boîte<Numéro> ou un Boîte<Long> est autorisé, mais un Boîte<Objet> ou Boîte<Chaîne> serait rejeté

Exemple (d’instanciation restreinte en raison d’une limite supérieure sur un paramètre de type) :
class Box<T extends Number> { 
   private T value; 
   public Box(T t) { value = t; } 
   ...
}  classe Test { 
   public static void main(String[] args) { 
       Box<Long>    boxOfLong   = new Box<Long>(0L);   // bien
       Box<String>  boxOfString = new Box<String>(""); // erreur : la chaîne n'est pas dans les limites
   }
}

  1. Accès aux membres non statiques. La limite supérieure donne accès à toutes les méthodes et champs publics non statiques de la limite supérieure.  Dans la mise en œuvre de notre la classe Box<T étend Number> {...} nous pouvons invoquer toutes les méthodes publiques non statiques définies dans la classe Nombre , tel que valeurint() par exemple.  Sans la limite supérieure, le compilateur rejetterait une telle invocation. Exemple (d’accès aux membres non statiques en raison d’une limite supérieure sur un paramètre de type) :

class Box<T extends Number> { 
   private T value; 
   public Box(T t) { value = t; } 
   public int increment() { return value.intValue()+1; } // <= serait une erreur sans le nombre lié
   ...
} 

    1. Tapez Effacement. La limite supérieure la plus à gauche est utilisée pour l’effacement de type et remplace le paramètre de type dans le code d’octet.  Dans notre la classe Box<T étend Number> {...} toutes les occurrences de T serait remplacé par la limite supérieure Nombre .  Par exemple, si la classe Boîte a un champ privé de type T et une méthode ensemble vide (contenu T) pour définir ce champ privé, alors le champ serait de type Nombre après l’effacement du type et la méthode serait traduite en méthode ensemble vide (contenu numérique) .

Exemple (d’utilisation de la limite supérieure sur un paramètre de type en effacement de type – avant tapez effacement) :
Boîte de classe< T étend le nombre > { 
   privé T valeur; 
   public Box( T t) { valeur = t ; } 
   ...
} 

      Exemple (d’utilisation de la limite supérieure sur un paramètre de type en effacement de type –

après

      tapez effacement) :

 

Boîte de classe { 
   privé Nombre valeur; 
   public Box( Nombre t) { valeur = t ; } 
   ...
}

    De plus, la limite supérieure la plus à gauche apparaît à d’autres emplacements, tels que les moulages insérés automatiquement et les méthodes de pont.

Si des limites inférieures étaient autorisées sur les paramètres de type, quels effets secondaires auraient-elles ou devraient-elles avoir ? Si une construction telle que Boîte de classe<T super Numéro> {...} étaient autorisés, qu’est-ce que cela signifierait ?  Que signifieraient les 3 effets secondaires d’une limite supérieure de paramètre de type – instanciation restreinte, accès à un membre non statique, effacement de type – pour une limite inférieure ?

Instanciations restreintes. Le compilateur pourrait restreindre l’ensemble des types pouvant être utilisés pour l’instanciation du type générique avec un paramètre de type limité inférieur.  Par exemple, le compilateur pourrait autoriser des instanciations telles que Boîte<Numéro> et Boîte<Objet> d’un Boîte<T super Numéro> et rejeter les instanciations telles que Boîte<Long> ou Boîte<Court> .  Ce serait un effet conforme à l’effet secondaire restrictif décrit pour les limites supérieures des paramètres de type.

Accès aux membres non statiques. Une limite inférieure de paramètre de type ne donne accès à aucune méthode particulière au-delà de celles héritées de la classe Objet . Dans l’exemple de Boîte<T super Numéro> les supertypes de Nombre n’ont rien en commun, si ce n’est qu’ils sont des types référence et donc des sous-types de Objet .  Le compilateur ne peut pas supposer que le champ de type T est du type Nombre ou un sous-type de celui-ci.  Au lieu de cela, le champ de type T peut être de n’importe quel supertype de Nombre , tel que Sérialisable ou Objet .  L’invocation d’une méthode telle que valeurint() n’est plus de type sécurisé et le compilateur devrait le rejeter. En conséquence, la limite inférieure du paramètre de type ne donnerait pas accès aux membres non statiques au-delà de ceux définis dans la classe Objet et a donc le même effet que “pas de limite”.

Tapez Effacement .  En suivant cette logique, cela n’a pas de sens de remplacer les occurrences du paramètre type par sa limite inférieure la plus à gauche.  Déclarer une méthode comme le constructeur Boîte( T t) en tant que constructeur Boîte( Nombre t) cela n’a pas de sens, étant donné que T est remplacé par un supertype de Nombre .  Un Objet pourrait être à juste titre transmis au constructeur dans une instanciation Boîte<Objet> et le constructeur le rejetterait.  Ce serait complètement faux.   Ainsi, l’effacement de type remplacerait toutes les occurrences de la variable de type T par type Objet , et non par sa borne inférieure.  Là encore, la limite inférieure aurait le même effet que “aucune limite”.

Voulez-vous comprendre ce que cela signifierait si les deux diminuaient _et_ les limites supérieures étaient-elles autorisées ?   Personnellement, je ne veux même pas y penser et je préférerais le classer sous « non gérable », si vous le permettez.

L’essentiel est : tout ça est un “ super ” ce qui vous achèterait est la restriction selon laquelle seuls les supertypes de Nombre peuvent être utilisés comme arguments de type.  Et même cela est souvent mal compris.  Cela ne voudrait PAS dire que classe Box<T super Numéro> {...} contient uniquement des instances de supertypes de Nombre .  Bien au contraire, comme le montre l’exemple ci-dessous !

Exemple (d’utilisation de la limite supérieure sur un paramètre de type en effacement de type – avant tapez effacement) :

Boîte de classe< T super numéro > {
   privé T valeur;
   public Box( T t) { valeur = t ; }
   ...
}

Exemple (d’utilisation de la limite supérieure sur un paramètre de type en effacement de type – après tapez effacement) :

Boîte de classe {
   privé Objet valeur;
   public Box( Objet t) { valeur = t ; }
   ...
}

UN Boîte de classe<T super Numéro> {...} serait traduit par effacement de type en un Boîte contenant un Objet champ et son constructeur serait traduit en Boîte (Objet t) . C’est fondamentalement différent d’un Boîte de classe<T s'étend Numéro> {...} , qui se traduirait par un Boîte contenant un Nombre champ et son constructeur serait traduit en Boîte (numéro t) . Par conséquent, un Boîte<Numéro> instancié à partir d’un la classe Box<T étend Number> {...} serait différent d’un Boîte<Numéro> instanciation à partir d’un classe Box<T super Numéro> {...} , ce qui est susceptible de prêter à confusion.  Pour cette raison, les limites inférieures n’ont pas de sens sur les paramètres de type des classes.

 


 

Paramètres de type de limite inférieure des méthodes

En conjonction avec les méthodes et leurs types d’arguments, un paramètre de type avec une limite inférieure peut parfois être utile.

Exemple (d’une méthode qui bénéficierait d’un paramètre de type avec une borne inférieure) :

classe Pair<X,Y> {
   private X first;
   private Y second;
   ...
   public < Un super X , B super Y > B addToMap(Map<A,B> map) { // erreur : le paramètre de type ne peut pas avoir de limite inférieure
      return map.put(first, second);
   }
}
classe Test { 
   public static void main(String[] args) { 
     Pair<String,Long> pair = new Pair<>("ABC",42L);
     Map<CharSequence, Number> map = HashMap<CharSequence, Number>();
     Number number = pair.addToMap(map);
   }
}

Le addToMap() La méthode ajoute le contenu de la paire à une carte.  Toute carte pouvant contenir des supertypes de X et Oui ferait.  La carte mettre() La méthode renvoie la valeur trouvée dans la carte pour la clé donnée, s’il existe déjà une entrée clé-valeur pour la clé dans la carte.  La valeur de retour de la carte mettre() méthode doit être renvoyée par le addToMap() méthode.  Dans ces circonstances, on voudrait déclarer la méthode comme indiqué ci-dessus : La carte est paramétrée avec des supertypes des paramètres de type de la paire et le addToMap() Le type de retour de la méthode est le type de valeur de la carte.

Étant donné que le compilateur n’autorise pas les limites inférieures des paramètres de type, nous avons besoin d’une solution de contournement.

Une solution qui me vient à l’esprit est l’utilisation d’un caractère générique, car les caractères génériques peuvent avoir une limite inférieure.  Voici une solution de contournement utilisant un caractère générique.

Exemple (de solution de contournement pour l’exemple précédent utilisant des caractères génériques) :

classe Pair<X,Y> {
   private X first;
   private Y second;
   ...
   public Objet addToMap(Carte< ? Super X , ? super Y Carte >) { 
      return map.put(first, second);
   }
}
classe Test { 
   public static void main(String[] args) { 
     Pair<String,Long> pair = new Pair<>("ABC",42L);
     Map<CharSequence, Number> map = HashMap<CharSequence, Number>();
     Number number = (Nombre) paire.addToMap(map);
   }
}

Cela fonctionne, sauf qu’il n’y a aucun moyen de déclarer le type de retour comme souhaité.  Ce serait le supertype de Oui que le compilateur capture à partir du type de carte, mais il n’existe aucune syntaxe pour le spécifier.  Il ne faut pas déclarer le type de retour un “ ? super Y “, parce que “ ? super Y ” est un caractère générique et non un type et n’est donc pas autorisé comme type de retour. Nous n’avons pas le choix et devons utiliser Objet à la place comme type de retour de notre méthode.  Ce type de retour plutôt imprécis force à son tour les appelants du addToMap() méthode pour réduire la valeur de retour de Objet à son type réel.  Ce n’est pas exactement ce que nous avions en tête.

Une autre solution consiste à utiliser des méthodes statiques.  Voici une solution de contournement avec une méthode statique au lieu d’une méthode non statique.

Exemple (de solution de contournement pour l’exemple précédent utilisant une méthode statique) :

classe Pair<X,Y> {
   private X first;
   private Y second;
   ...
   public statique < UN , B ,X étend A ,Y étend B > B addToMap(Paire<X,Paire Y>, Map< UN , B Carte >) {
      return map.put(pair.first,pair.second);
   }
}
classe Test { 
   public static void main(String[] args) { 
     Pair<String,Long> pair = new Pair<>("ABC",42L);
     Map<CharSequence, Number> map = HashMap<CharSequence, Number>();
     Number number = Paire .addToMap( paire ,carte);
   }
}

Le générique addToMap() la méthode a quatre paramètres de type : deux espaces réservés X et Oui pour le type de la paire et deux espaces réservés UN et B pour le type de la carte. UN et B sont des supertypes de X et Oui , parce que X et Oui   are declared with UN et B comme leurs limites supérieures. (Notez que les paramètres de type de la méthode générique X et Oui n’a rien à voir avec le Paire la classe X et Oui paramètres.  Les noms X et Oui sont réutilisés pour la méthode générique afin de les rendre facilement reconnaissables en tant que paramètres de type de la paire. En utilisant quatre paramètres de type, nous pouvons déclarer le type de retour précis comme nous le souhaitons : c’est le même type que le type valeur de la carte.

Exemple (d’utilisation illégale du paramètre de type dans le contexte statique d’une classe générique) :

classe UneClasse <T> {
  // initialiseur statique, champ statique, méthode statique
  statique {
    SomeClass< T Test > = nouveau SomeClass< T >(); // erreur
  }
  privé statique T globalInfo;          // erreur
  public  statique T getGlobalInfo() {        // erreur
    return globalInfo;
  }
  // initialiseur non statique, champ non statique, méthode non statique
  { 
    SomeClass< T Test > = nouveau SomeClass< T >();
  }
  privé        T infolocale ;
  public         T getLocalInfo() { 
    return localInfo;
  }
  // types imbriqués statiques
  public statique classe L'échec étend l'exception {
    private final T info;           // erreur
    public Failure( T t) { infos = t ; }      // erreur
    public T getInfo() { return info; }    // erreur
   }
  privé interface Copiable {
    T copy();                       // erreur
  }
  privé énumération État {
    VALID, INVALID;
    privé T informations ; // erreur
    public void setInfo( T t) { infos = t ; } // erreur
    public T getInfo() { return info; }    // erreur
  }
  // types imbriqués non statiques
  public classe Accesseur {
    public T getInfo() { return localInfo; }
  }
}

L’exemple illustre que le paramètre type ne peut pas être utilisé dans le contexte statique d’une classe générique.  Cela montre également que les interfaces imbriquées et les types enum sont considérés comme des membres de type statique de la classe.  Seules les classes internes, c’est-à-dire les classes imbriquées non statiques, peuvent utiliser le paramètre de type de la classe générique englobante.

La portée du paramètre de type d’une interface est la définition complète de l’interface, à l’exception des champs ou des types imbriqués.  En effet, les champs et les types imbriqués définis dans une interface sont implicitement statiques.

Exemple (d’utilisation illégale du paramètre type dans une interface générique) :

interface UneInterface <T> {
  // champ
  SomeClass< T Valeur > = nouveau SomeClass< T >();  // erreur
  // type imbriqué
  classe Accesseur {
    public T getInfo() {            // erreur
      return value.getGlobalInfo();
    }
  }
  // méthodes
  T getValue();
}

L’exemple montre que les champs d’une interface sont implicitement statiques, de sorte que le paramètre type ne peut être utilisé nulle part dans la déclaration d’un champ d’une interface générique.  De même, la classe imbriquée est considérée comme une classe imbriquée statique, et non comme une classe interne, et pour cette raison, l’utilisation du paramètre type n’importe où dans la classe imbriquée est illégale.

Solution de contournement pour les classes et interfaces statiques imbriquées :

Dans le cas de classes et d’interfaces imbriquées statiques, ce n’est pas une calamité majeure.  Pour contourner ce problème, nous pouvons générer la classe statique ou l’interface elle-même.

Exemple (solution de contournement : générer le type statique imbriqué) :

Emballage de classe <T> {
  private final T l'Objet ;

  public Wrapper( T t) { l'Objet = t; }
  public T getWrapper() { return theObject; }   public Immutable <T> makeImmutable() {
        return new Immutable <T> (l'Objet);
  }
  public Mutable <T> makeMutable() {
        return new Mutable <T> (l'Objet);
  }
  private static <A> A makeClone(A theObject) { ... }

  public static final class Immutable <A> { // est maintenant une classe générique
        private final UN l'Objet ;

        public Immutable( UN argument) {
            theObject = makeClone(arg);
        }
        public UN getWrappedItem() {
            return makeClone(theObject);
        }
  }
  public static class Mutable <A> {
        ... similar ...
  }
}

Il n’existe pas de solution de contournement pour les types d’énumérations imbriqués car ils ne peuvent pas être génériques.

Solution de contournement pour les classes statiques imbriquées :

Si le type statique imbriqué est une classe, une autre solution consisterait à transformer la classe statique en une classe interne non statique.

Exemple (solution de contournement : utilisez une classe interne) :

Emballage de classe <T> {
  private final T l'Objet ;

  public Wrapper( T t) { l'Objet = t; }
  public T getWrapper() { return theObject; }   public Mutable makeMutable() {
      return ce. nouveau Mutable (l'Objet);
  }
  public Immutable makeImmutable() {
      return ce. new Immuable (l'Objet);
  }
  private static <A> A makeClone(A theObject) { ... }

  public final class Immutable {  // n'est plus une classe statique
      private final T l'Objet ;

      public Immutable( T argument) {
          theObject = makeClone(arg);
      }
      public T getWrappedItem() {
          return makeClone(theObject);
      }
  }
  public class Mutable 
        ... similar ...
  }
}

Cette solution de contournement s’accompagne d’une certaine surcharge car toutes les classes internes ont une référence cachée à une instance du type externe, dont elles n’ont ni besoin ni profit dans cet exemple ; la référence cachée n’est qu’un bagage.  Souvent, les classes internes sont combinées avec des interfaces afin de conserver la classe interne comme détail d’implémentation privé de la classe englobante.  Nous pouvons faire la même chose ici.

Exemple (la solution de contournement précédente affinée) :

Emballage de classe <T> {
  private final T l'Objet ;

  public Wrapper( T t) { l'Objet = t; }
  public T getWrapper() { return theObject; }   public Mutable <T> makeMutable() {
      return this.new Mutable(theObject);
  }
  public Immutable <T> makeImmutable() {
      return this.new Immutable(theObject);
  }
  private static <A> A makeClone(A theObject) { ... }

  interface publique Immuable<S> {
      S getWrappedItem();
  }
  interface publique Mutable<S> {
      S getWrappedItem();
      void setWrappedItem(S arg);
  }
  privé classe finale ImmutableImplementation implémente Immutable<T> { 

      private final T theObject;

      public ImmutableImplementation(T arg) {
          theObject = makeClone(arg);
      }
      public T getWrappedItem() {
          return makeClone(theObject);
      }
  }
  privé classe MutableImplementation implémente Mutable<T> { 
        ... similar ...
  }
}

Comme vous pouvez le voir, les interfaces publiques imbriquées doivent être génériques tandis que les classes privées internes peuvent utiliser le paramètre de type de la classe englobante.

 

 

 

 

 

Laisser une réponse

Découvrez-en davantage sur le blog de Velislav Krastev

Abonnez-vous maintenant pour continuer à lire et accéder aux archives complètes.

Continuer la lecture