Compléments sur les types paramétrés

images/brownbelt.png

Cet exposé suppose connus les principes des codes paramétrés et de l'invariance (Le fait que si Manager extends Employe alors on ne peut pas utiliser une ArrayList<Manager> quand le type demandé est ArrayList<Employe>!)

Covariance

Soit les types suivants:

class Manager extends Employe
class PDG extends Manager

Du fait du principe d'invariance nous savons que si nous avons la méthode:

   public List<Departement> deptsEnReunion(List<Manager> réunion) {
      List<Departement> list = new ArrayList<>() ;
      for( Manager man : réunion) {
         list.add(man.getDept()) ; // méthode spécifique à Manager
      }
   }

Alors on ne peut pas coder:

   List<PDG> conseil = new ArrayList<>() ;
   // .... du code
   depts = hr.deptsEnReunion(conseil) ; /* _compiler_error_ */

Toutefois l’intention est louable et nous voudrions pouvoir compiler et exécuter ce code.

Dans ce cas la définition correcte est :

   public List<Departement> deptsEnReunion(List<? extends Manager> réunion) {
      List<Departement> list = new ArrayList<>() ;
      for( Manager man : réunion) {
         list.add(man.getDept()) ;
      }
   }

La definition :

List<? extends Manager>

est celle d’un type sous contrainte .

Elle signifie Un type qui est affectable à Manager (covariance).

  • La contrainte "un type affectable à X" autorise le compilateur à valider une affectation d’un type T paramétré par Y vers un type T' paramétré par Y à condition que T soit affectable à T' ET que Y soit affectable à X.

    Donc si ArrayList est affectable à List alors ArrayList<PDG> est affectable à List<? extends Manager>

  • Dans le code de la méthode un objet du type paramètre <? extends Manager> est affectable à une référence de type Manager. Mais la reciproque n’est pas vraie!
  • Le compilateur ne peut accepter qu’une référence de type Manager soit affectée à un objet du type extends Manager (en effet on ne peut mettre un simple Manager dans une liste de PDG!). Donc <? extends X> désigne un type pour lequel a priori on sait pas si on peut réaliser un affectation d’un type X.

    C’est que qui se produirait avec des méthodes de ArrayList<X> comme add<X>. Lorsque le type "cible" de l’affectation n’est pas connu a priori on ne peut pas la réaliser.

    Ainsi sur une référence de type List<? extends X> on ne peut réaliser un add(objet) quelque soit le type de l’objet (X, une sous-classe de X -dont ? extends X-, etc..).

       public void inviterAReunion(List<? extends Manager> réunion) {
          // On a un départment quelque part
          // et on invite son manager
          réunion.add(dept.getManager()) ; /* _compiler_error_ */
       }

L’introduction d’un contrainte "affectable à" (extends X) limite l’utilisation du type paramétré à des affectations réalisées depuis ce type. On ne peut réaliser des affectations vers ce type. Donc on ne peut appeler des méthodes dont le type d’argument se retranscrit comme extends X, mais on peut récupérer des résultats de ce type.

[Avertissement]Attention

Souvenez-vous que la sémantique "affectable à" fait référence à une sémantique plus subtile que celle du mot-clef extends (comme dans ClasseFille extends ClasseMere).

Si on déclare <? extends Comparable> on désigne tout type qui fait implements Comparable.

ArrayList<? extends X> maListeEnLecture sera utilisée comme référence opérationnelle à une liste dans laquelle on ne pourra rien ajouter.

public class PanneauBarre extends Panel {
  // ...
  public PanneauBarre(List<? extends Component> comps) {
   for(JComponent jc : comps) {this.add(jc) ; }
  }

Contravariance

Une définition correcte de la méthode inviterAReunion serait:

   public void inviterAReunion(List<? super Manager> réunion) {
      // On a un départment quelque part
      // et on invite son manager
      réunion.add(dept.getManager()) ; // OK!!!
   }

On peut ajouter un Manager à une liste d'Employe (ou a une liste d'Object!).

ArrayList<? super Manager> signifie une ArrayList contenant des références à "UN type affectable depuis une référence de type Manager " (contravariance).

  • La contrainte "UN type affectable depuis X " permet de contrôler les affectations depuis un type:

    ? super Manager est affectable depuis un Manager ;

    Il n’est pas garanti qu’un ? super Manager soit affectable depuis une autre classe qui puisse dériver de Employe (et ne pas faire extends Manager)

    Donc dans notre exemple il n’est pas possible de faire :

       Employe emp = ..... ;
       réunion.add(emp) ; // _compiler_error_
       // si réunion regroupe vraiment des Managers
       // il n'est pas possible d'y enrôler l'employé lambda!

    Seules des références de type Manager (et de ses sous-classes) pourront passer un paramètre à une méthode dont le type se retranscrit comme super Manager (une méthode comme add).

  • La contrainte "UN type affectable depuis X " ne permet pas de contrôler une affectation VERS un autre type (sauf Object).

    Il est impossible de savoir si un type qui répond à la contrainte super Manager peut être affecté à un type X quelconque! (y compris, bien sûr, Manager!).

    Donc quand une méthode renvoie un type qui se retranscrit comme super X on ne peut l’invoquer vers une référence d’un type quelconque sauf Object (c’est le cas de méthodes comme remove(int) dans ArrayList).

    Pour simplifier on pourrait dire que ArrayList<? super X> maListeEnEcriture ne peut être utilisée que pour certaines opérations en écriture. La réalité est un peu plus complexe car ce qui est important c’est la manière dont sont passés les paramètres et récupérés les résultats:

public class ConseilAdministration {
   protected HashSet<Manager> participants ;

   public void émarger (Set<? super Manager> listeARemplir){
      listARemplir.addAll(participants) ;
   }

   public boolean tousPrésents (Set<? super Manager> inscrits){
      return inscrits.containsAll(participants) ;// voir doc.
   }
   //....

Autres exemples

Une List<?> est une liste de quelque chose : voir méthode shuffle de Collections

[Note]

Une List est une collection de n’importe quoi ; une List<?> est une collection de quelque chose ! (bien se souvenir de la subtilité!).

Une List<? extends List & RandomAccess> est une liste d’objets qui implantent les deux interfaces List et RandomAccess.

Nous avons ici un cas où un typage implicite peut être intéressant:

      //
      var liste = List.of(1, 2.0, "3") ;

Exercices

Ici les exercices consistent à lire une documentation et comprendre les contraintes sur les types paramétrée.

Dans le module java.base , dans le package java.util :

  • Lire la documentation de la classe Collections : vous y trouverez un ensemble de descriptions de méthodes statiques utilisant les types paramétrés sous contrainte. Essayez de bien comprendre (et éventuellement d’expérimenter) les méthodes les plus complexes.

Comment fonctionnent les types paramétrés? Compatibilités.

De nombreuses classes standard ont été réécrites pour tenir compte des possibilités de la programmation générique (en particulier les classes de collection de java.util). Un grand soin a été apporté pour que les codes prééxistants continuent à fonctionner. En fait on n’est même pas obligé de tirer parti des services des types paramétrés:

ArrayList meeeting = new ArrayList() ;
réunion.add(new Manager(°°°°°)) ;

donne juste un avertissement pour non-utilisation des contrôles (voir option Xlint:unchecked) -une compilation avec option -source 1.4 ne donne aucun avertissement-

De la même manière des codes utilisant les anciennes versions des classes paramétrées sont compatibles au niveau binaire avec une exécution de codes supérieurs ou égal à 1.5.

Comment cette compatibilité est-elle obtenue?

Les informations sur l’utilisation de types paramétrés ne sont pas conservées dans le code exécutable binaire: les types sont remplacés par Object (ou parfois par un type représentant une borne supérieure comme Manager dans <? extends Manager>); et les transtypages (cast) nécessaires sont insérés dans le code.

On a donc un "effacement" (erasure) des contraintes de compile-time pour générer le code exécuté au run-time.

Ceci dit le code binaire supérieur à la version 1.5 contient aussi des informations supplémentaires qui permettent de connaître les déclarations et les utilisations des types paramétrés. Ces meta-informations seront utiles à tout compilateur qui voudra vérifier si les contraintes sont bien respectées.

En résumé dans le binaire du .class on a:

  • un code exécutable compatible avec les versions antérieures à la 1.5
  • des informations complémentaires spécifiques à cette version qui peuvent être exploitées par des compilateurs (ou tout autre programme recherchant des informations par introspection)

Utilisation des variables-type

L’utilisation des variables-types formelles (en dehors des types paramétrés) est limitée: certains expressions qui sont possibles avec des types "normaux" ne sont pas autorisées avec des variables-type.

On ne peut utiliser ces déclarations de type dans un contexte statique et:

T variable = new T() ; // impossible
T[] array = new T[nb] ; // impossible
if( obj instanceof T) {°°° // NON: de quel type parle-t'on ?
class Inner extends T {°°° // quid si T est "final" ?
void meth(T ... args) { °°° // Possible!
try{°°°°} catch(T exception){°°°//interdit pour le moment (1.6)

Utilisation des variables types dans des types paramétrés

Ici des règles différentes s’appliquent:

ArrayList<T> variable = new ArrayList<T>() ; // possible

// on neput créer directement des tableaux de types paramétrés
ArrayList<T>[] array = new ArrayList<T>[nb] ;
ArrayList<String>[] array2 = new ArrayList<String>[nb] ;

//pas d'information possible au runtime
if( obj instanceof ArrayList<T>) {

//possible! warning dans le code
void meth(List<T> ... args) {°°°
void meth(List<T>[] args) {°°° // correct
class Inner extends ArrayList<T> {°°° // possible
(List<T>) ref ;

Pour le moment on ne peut pas avoir de catch avec un type paramétre ; on ne peut pas paramétrer un Exception mais une Exception peut être un type paramètre.