LECON 208


La généricité en Java
Pour ce concept, ajouté au JDK depuis sa version 1.5, nous allons surtout travailler avec des exemples tout au long de ce chapitre.
Le principe de la généricité est de faire des classes qui n'acceptent qu'un certain type d'objet ou de donnée, mais de façon dynamique ! .

Avec ce que nous avons vu au chapitre précédent, vous avez sûrement poussé un soupir de soulagement lorsque vous avez vu que ces objets acceptent tous types de données !
Par contre, un problème de taille se pose : lorsque vous voudrez travailler avec ces données, vous allez devoir faire un cast ! Et peut-être même un cast de cast, voire même un cast de cast de cast...

C'est là que se situe le problème... Mais comme je vous le disais, depuis la version 1.5 du JDK, la généricité est là pour vous aider !

Sommaire du chapitre :

  • Notion de base
  • Plus loin dans la généricité !
  • Généricité et collection
  • Héritage et généricité
  • Ce qu'il faut retenir


Notion de base

Bon, pour vous montrer la puissance de la généricité, nous allons tout de suite voir un exemple de classe qui ne l'utilise pas !
Ne vous en faites pas... Ayez confiance en moi...

Il y a un exemple très simple, que vous pourrez retrouver aisément sur le net, car il s'agit d'un des cas les plus simples pour expliquer les bases de la généricité. Nous allons coder une classe Solo. Celle-ci va travailler avec des références de type String.
Voici le diagramme de classe :

Vous pouvez voir que le code de cette classe est très rudimentaire ! On affecte une valeur, on peut la mettre à jour et la récupérer...
Maintenant, si je vous demande de me faire une classe qui permette de travailler avec n'importe quel type de données, j'ai une vague idée de ce que vous allez faire... Ce ne serait pas un truc comme ça :


J'en étais sûr... :D. Créez-la et créez-vous aussi une classe avec une méthode main !
Mais si vous voulez utiliser les données de l'objet Solo, vous allez devoir faire un cast.
Testez ce code dans votre main :

Code : Java -  
1
2
3
4
5
6
7
8
9
public class Test {
 
        public static void main(String[] args) {
                
                Solo val = new Solo(12);
                int nbre = val.getValeur();             
        }
 
}



Vous constatez que vous tentez vainement de mettre un objet de type Object dans un objet de type Integer. Ceci est interdit ! !
La classe Object est plus globale que la classe Integer, vous ne pouvez donc pas faire cette manipulation, sauf si vous "castez" votre objet en Integer, comme ceci :

Code : Java -  
1
2
3
4
5
6
7
8
9
public class Test {
 
        public static void main(String[] args) {
                
                Solo val = new Solo(12);
                int nbre = (Integer)val.getValeur();            
        }
 
}


Pour le moment, on peut dire que votre classe peut travailler avec tous les types de données, mais les choses se corsent un peu à l'utilisation... Vous serez peut-être tentés de faire une classe par type de donnée (SoloInt, SoloString).
Et c'est là que la généricité est pratique. Car avec ceci, vous allez pouvoir savoir ce que contient votre objet Solo, et vous n'aurez qu'une seule classe à développer !
Voilà le diagramme de classe de cet objet :



Et voici son code :


Code : Java -  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Solo<T> {
 
        /**
         * Variable d'instance
         */
        private T valeur;
        
        /**
         * Constructeur par défaut
         */
        public Solo(){
                this.valeur = null;
        }
        
        /**
         * Constructeur avec paramètre
         * Inconnu pour l'instant
         * @param val
         */
        public Solo(T val){
                this.valeur = val;
        }
        
        
        /**
         * Définit la valeur avec le paramètre
         * @param val
         */
        public void setValeur(T val){
                this.valeur = val;
        }
        
        /**
         * retourne la valeur déjà "castée" par la signature de la méthode !
         * @return
         */
        public T getValeur(){
                return this.valeur;
        }       
}

Impressionnant, n'est-ce pas ?
Dans cette classe, le T n'est pas encore défini. Vous le ferez à l'instanciation de cette classe. Par contre, une fois instancié avec un type, l'objet ne pourra travailler qu'avec le type de données que vous lui avez spécifié ! Exemple de code :

Code : Java -  
1
2
3
4
5
6
7
8
public class Test {
 
        public static void main(String[] args) {
                // TODO Auto-generated method stub
                Solo<Integer> val = new Solo<Integer>(12);
                int nbre = val.getValeur();             
        }
}

Ce code fonctionne très bien, mais si vous essayez de faire ceci :
Code : Java -  
1
2
3
4
5
6
7
8
public class Test {
 
        public static void main(String[] args) {
                // TODO Auto-generated method stub
                Solo<Integer> val = new Solo<Integer>("toto"); //Ici on essaie de mettre une chaîne de caractère à la place d'un entier
                int nbre = val.getValeur();             
        }
}


...ou encore ceci :
Code : Java -  
1
2
3
4
5
6
7
8
public class Test {
 
        public static void main(String[] args) {
                // TODO Auto-generated method stub
                Solo<Integer> val = new Solo<Integer>(12);
                val.setValeur(12.2f);   //Ici on essaie de mettre un float à la place d'un entier        
        }
}


...vous verrez une erreur dans votre zone de saisie. Ceci vous indique que votre objet ne reçoit pas le bon type d'argument, ou que votre réceptacle n'a pas le bon type de données ! Dans tous les cas de figure, il y a conflit entre le type de données que vous avez passé à votre instance lors de sa création et le type de données que vous essayez d'utiliser avec celle-ci !
Par contre, vous devez savoir que cette classe ne fonctionne pas seulement avec des Integer. Vous pouvez utiliser tous les types que vous souhaitez !


Voici une démonstration de mes dires :
Code : Java -  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Test {
 
        public static void main(String[] args) {
                // TODO Auto-generated method stub
                Solo<Integer> val = new Solo<Integer>();
                Solo<String> valS = new Solo<String>("TOTOTOTO");
                Solo<Float> valF = new Solo<Float>(12.2f);
                Solo<Double> valD = new Solo<Double>(12.202568);                
        }
}

Vous devez avoir remarqué que je n'ai pas utilisé ici les types de données que vous utilisez pour déclarer des variables de type primitif ! Ce sont les classes de ces types primitifs !

En effet, lorsque vous déclarez une variable de type primitif, vous pouvez utiliser leurs classes de définition, mais c'est rarement utilisé car très lourd à la lecture. Par exemple :
Code : Java -  
1
2
3
4
5
6
7
8
9
public class Test{
 
    public static void main(String[] args){
 
        int i = new Integer(12); // est équivalent à int i = 12;
        double d = new Double(12.2586); // est équivalent à double d = 12.2586;
        //...
    }
}


Bon ! Maintenant que vous avez un bel exemple de généricité, nous allons complexifier un peu les choses !

Plus loin dans la généricité !

Vous devez savoir que la généricité peut être multiple !
Nous avons créé une classe Solo, mais rien ne vous empêche de créer une classe Duo, qui elle, prend deux paramètres génériques ! Voici la modélisation de cette classe :



Vous pouvez voir que cette classe prend deux types de références non encore définies !
Voilà le code source de cette classe :
Code : Java -  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class Duo<T, S> {
 
        /**
         * Variable d'instance de type T
         */
        private T valeur1;
        /**
         * Variable d'instance de type S
         */
        private S valeur2;
        
        /**
         * Constructeur par défaut
         */
        public Duo(){
                this.valeur1 = null;
                this.valeur2 = null;
        }
        
        /**
         * Constructeur avec paramètres
         * @param val1
         * @param val2
         */
        public Duo(T val1, S val2){
                this.valeur1 = val1;
                this.valeur2 = val2;
        }
        
        /**
         * Méthodes d'initialisation des deux valeurs
         * @param val1
         * @param val2
         */
        public void setValeur(T val1, S val2){
                this.valeur1 = val1;
                this.valeur2 = val2;
        }
 
        /**
         * Retourne la valeur T
         * @return
         */
        public T getValeur1() {
                return valeur1;
        }
 
        /**
         * Définit la valeur T
         * @param valeur1
         */
        public void setValeur1(T valeur1) {
                this.valeur1 = valeur1;
        }
 
        /**
         * retourne la valeur S
         * @return
         */
        public S getValeur2() {
                return valeur2;
        }
 
        /**
         * définit la valeur S
         * @param valeur2
         */
        public void setValeur2(S valeur2) {
                this.valeur2 = valeur2;
        }
        
}


Voici un code que vous pouvez tester :
Code : Java -  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Test {
 
        public static void main(String[] args) {
                
                Duo<String, Boolean> dual = new Duo<String, Boolean>("toto", true);
                System.out.println("Valeur de l'objet dual: val1 = " + dual.getValeur1() + ", val2 = " + dual.getValeur2());
                
                Duo<Double, Character> dual2 = new Duo<Double, Character>(12.25895, 'C');
                System.out.println("Valeur de l'objet dual2: val1 = " + dual2.getValeur1() + ", val2 = " + dual2.getValeur2()); 
                
        }
}

Et voici le résultat :


Vous voyez qu'il n'y a rien de bien méchant ici. Ce principe fonctionne exactement comme l'exemple précédent. La seule différence est dans le fait qu'il n'y a pas un, mais deux paramètres génériques !
Attends une minute... Lorsque je déclare une référence de type Duo<String, Boolean>, je ne peux plus la changer en un autre type !

En fait, avec ce que je vous ai fait voir, non.
Pour le moment, si vous faites :
Code : Java -  
1
2
3
4
5
6
7
8
9
public class Test {
 
        public static void main(String[] args) {
                
                Duo<String, Boolean> dual = new Duo<String, Boolean>("toto", true);
                System.out.println("Valeur de l'objet dual: val1 = " + dual.getValeur1() + ", val2 = " + dual.getValeur2());
                dual = new Duo<Double, Character>();
        }
}

vous violez la contrainte que vous avez émise lors de la déclaration du type de référence ! Mais il existe un moyen de contourner ça.
Tout simplement en disant, à la déclaration, que votre objet va accepter tous types de références ! Comment en utilisant ce qu'on appelle le wildcard : ?.
Comme ceci :
Code : Java -  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Test {
 
        public static void main(String[] args) {
                
                Duo<?, ?> dual = new Duo<String, Boolean>("toto", true);
                
                System.out.println("Valeur de l'objet dual: val1 = " + dual.getValeur1() + ", val2 = " + dual.getValeur2());
                dual = new Duo<Double, Character>();
                dual = new Duo<Integer, Float>();
                dual = new Duo<Solo, Solo>();
        }
}


Avec ce type de déclaration, votre objet accepte bien n'importe quel type de référence !
Intéressant, non ?
Donc si vous suivez bien, on va pouvoir encore corser la chose !

Généricité et collection

Vous pouvez aussi utiliser la généricité sur les objets servant à gérer des collections.
C'est même l'un des points les plus utiles de la généricité !

En effet, lorsque vous listiez le contenu d'un ArrayList par exemple, vous n'étiez JAMAIS sûrs à 100 % de savoir sur quel type de référence vous alliez tomber... Eh bien ce calvaire est terminé et le polymorphisme va pouvoir réapparaître, plus puissant que jamais !

Voyez comment utiliser (même si vous l'aviez deviné) la généricité avec les collections :
Code : Java -  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.ArrayList;
 
 
public class Test {
 
        public static void main(String[] args) {
                
                System.out.println("Liste de String");
                System.out.println("------------------------------");
                ArrayList<String> listeString= new ArrayList<String>();
                listeString.add("Une chaîne");
                listeString.add("Une Autre");
                listeString.add("Encore une autre");
                listeString.add("Allez, une dernière");
                
                for(String str : listeString)
                        System.out.println(str);
                
                
 
                System.out.println("\nListe de float");
                System.out.println("------------------------------");
                
                ArrayList<Float> listeFloat = new ArrayList<Float>();
                listeFloat.add(12.25f);
                listeFloat.add(15.25f);
                listeFloat.add(2.25f);
                listeFloat.add(128764.25f);
                
                for(float f : listeFloat)
                        System.out.println(f);
        }
}


Voici le résultat de ce code :

La généricité sur les listes est régie par les mêmes lois vues précédemment !
Pas de type float dans un ArrayList<String>.

Vu qu'on y va crescendo, on pimente à nouveau le tout !

Héritage et généricité

Là où les choses sont pernicieuses, c'est quand vous utilisez des classes usant de la généricité avec des objets usant de la notion d'héritage !
L'héritage dans la généricité est une des choses les plus complexes à comprendre en Java. Pourquoi ? Tout simplement parce qu'elle va à l'encontre de ce que vous avez appris jusqu'à présent...

Acceptons le postulat suivant


Nous avons une classe Voiture dont hérite une autre classe VoitureSansPermis, ce qui nous donnerait le diagramme suivant :


Jusque-là, c'est simplissime.
Maintenant, ça se complique :
Code : Java -  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import java.util.ArrayList;
 
 
public class Test {
 
        public static void main(String[] args) {
                
                ArrayList<Voiture> listVoiture = new ArrayList<Voiture>();
                ArrayList<VoitureSansPermis> listVoitureSP = new ArrayList<VoitureSansPermis>();
                
                listVoiture = listVoitureSP;//Interdit ! ! ! ! 
        }
}

Je sais que même si vous aviez l'habitude de la covariance des variables, ceci n'existe pas sous cette forme avec la généricité !
Pourquoi cela ?

Imaginez deux secondes que l'instruction interdite soit permise !
Dans listVoiture, vous avez le contenu de la liste des voitures sans permis, et rien ne vous empêche d'ajouter une voiture... Là où le problème prend toute son envergure, c'est lorsque vous allez vouloir sortir toutes les voitures sans permis de votre variable listVoiture, eh oui ! Vous y avez rajouté une voiture !
Lors du balayage de la liste vous aurez, à un moment, une référence de type VoitureSansPermis à qui vous tentez d'affecter une référence de type Voiture. Voilà pourquoi ceci est INTERDIT ! !

L'une des solutions consiste à utiliser le wildcard.

Je vais maintenant vous indiquer quelque chose d'important !
Avec la généricité, vous pouvez aller encore plus loin... Nous avons vu comment restreindre le contenu d'une de nos listes. Mais nous pouvons aussi élargir son contenu ! Si je veux par exemple qu'un ArrayList puisse avoir toutes les instances de Voiture et de ses classes filles. Comment faire ?
Ce qui suit s'applique aussi aux interfaces susceptibles d'être implémentées par une classe !


Attention les yeux, ça pique :
Code : Java -  
1
2
3
4
5
6
7
8
import java.util.ArrayList;
public class Test {
 
        public static void main(String[] args) {
                //Voici un ArrayList n'acceptant que des instances de Voiture ou de ses sous-classes
                ArrayList<? extends Voiture> listVoitureSP = new ArrayList<VoitureSansPermis>();           
        }
}

Et une application de ceci consiste à faire des méthodes génériques, comme par exemple avoir une méthode qui permette de lister toutes les valeurs de notre ArrayList citée précédemment. Voici :
Code : Java -  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import java.util.ArrayList;
public class Test {
 
        public static void main(String[] args) {
                
                ArrayList<? extends Voiture> listVoitureSP = new ArrayList<VoitureSansPermis>(); 
                afficher(listVoitureSP);          
        }
        
        /**
         * Méthode générique !
         * @param <T>
         * @param list
         */
        static void afficher(ArrayList<? extends Voiture> list){
                for(Voiture v : list)
                        System.out.println(v.toString());
        }
        
}


Eh ! Attends, on a voulu ajouter des objets dans notre collection et le programme ne compile plus !


Oui, alors, ce que je ne vous avait pas dis, c'est que, dès que vous utilisez le wildcard combiné avec le mot clé extends, vos listes seront verrouillées en insertion : Elles se transforment en collections en lecture seule...
Pourquoi ça ?

En fait, il faut déjà savoir que c'est à la compilation du programme que Java ne vous laisse pas faire.
Le verrou vient du fait que, vu que le wildcard signifie "tout objet" combiné avec extends signifiant "héritant", au moment de la compilation, Java n'a aucune idée de l'objet qu'on vient d'assigner à notre collection : les concepteurs ont donc préféré bloquer ce mode d'utilisation.

Par contre, ce type d'utilisation fonctionne à merveille pour la lecture :
Code : Java -  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.ArrayList;
import java.util.List;
 
 
public class Main {
 
        public static void main(String[] args){
               
               //Liste de voiture
               ArrayList<Voiture> listVoiture = new ArrayList<Voiture>();
               listVoiture.add(new Voiture());
               listVoiture.add(new Voiture());
               
               ArrayList<VoitureSansPermis> listVoitureSP = new ArrayList<VoitureSansPermis>();
               listVoitureSP.add(new VoitureSansPermis());
               listVoitureSP.add(new VoitureSansPermis());
               
               affiche(listVoiture);
               affiche(listVoitureSP);
        }
        
        /**
         * Avec cette méthode, on accepte aussi bien les collections de Voiture 
         * que les collection de VoitureSansPermis
         * @param list
         */
        static void affiche(List<? extends Voiture> list){
 
               for(Voiture v : list)
                       System.out.print(v.toString());
        }
        
}


Avant que vous ne posiez la question : NON ! Déclarer la méthode comme ceci affiche(List<Voiture> list) ne vous permet pas de parcourir des listes de VoitureSansPermis, même si celle-ci hérite de la classe Voiture.
Les méthodes déclarées avec un type générique sont verrouillées afin de n'être utilisées qu'avec ce type bien précis, toujours pour les mêmes raisons que ci-dessus !
Pfiou ! C'est bien compliqué tout ça...

Attendez, ce n'est pas encore fini. Nous avons vu comment élargir le contenu de nos collections (pour la lecture), nous allons voir comment restreindre les collections acceptées par nos méthodes.
La méthode :
Code : Java -  
1
2
3
4
5
static void affiche(List<? extends Voiture> list){
 
  for(Voiture v : list)
        System.out.print(v.toString());
}

Autorise un objet de type List de n'importe quel type dont Voiture est la super classe.

L'instruction suivante signifie :
La méthode autorise un objet de type List de n'importe quel super classe de la classe Voiture, Voiture y compris.

Code : Java -  
1
2
3
4
5
static void affiche(List<? super Voiture> list){
 
  for(Object v : list)
        System.out.print(v.toString());
}


Ce code fonctionne donc parfaitement :

Code : Java -  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args){
               
               //Liste de voiture
               List<Voiture> listVoiture = new ArrayList<Voiture>();
               listVoiture.add(new Voiture());
               listVoiture.add(new Voiture());
               
               ArrayList<Object> listVoitureSP = new ArrayList<Object>();
               listVoitureSP.add(new Object());
               listVoitureSP.add(new Object());
               
               affiche(listVoiture);          
        }
        
        /**
         * Avec cette méthode, on accepte aussi bien les collections de Voiture 
         * que les collection d' Object : super classe de toutes les classes
         * @param list
         */
        static void affiche(List<? super Voiture> list){
 
               for(Object v : list)
                       System.out.print(v.toString());
        }



Je conçois bien que ceci est un peu ardu à comprendre... Mais vous en aurez sûrement besoin dans une de vos prochaines applications !

Bon : je crois que nous avons fait un bon tour du sujet même si nous n'avons pas tout abordé... Allez, le topo classique.

Ce qu'il faut retenir

  • La généricité est apparue depuis le JDK 1.5.
  • Ce concept est très utile pour développer des objets pouvant travailler avec plusieurs types de données.
  • Vous passerez donc moins de temps à développer des classes pour traiter de façon identique des données différentes.
  • La généricité permet de ré-utiliser le polymorphisme sans risque avec les collections.
  • Cela vous permet une meilleure robustesse du code.
  • Vous pouvez coupler les collections avec la généricité !
  • Le wildcard (?) permet de dire que n'importe quel type peut être traité et donc accepté !
  • Dès que le wildcard (?) est utilisé sur une collection, cela revient à rendre la dite collection en lecture seule !
  • Vous pouvez élargir le champ d'acceptation d'une collection générique grâce au mot clé extends.
  • L'instruction <? extends MaClasse> autorise toutes les collections de classes ayant pour super type MaClasse.
  • L'instruction <? super MaClasse> autorise toutes les collections de classes ayant pour type MaClasse et tous ses supers types !
  • Pour ce genre de cas, les méthodes génériques sont particulièrement adaptées et permettent d'utiliser le polymorphisme dans toute sa splendeur !

J'espère que ce chapitre n'a été trop... lourd...
En attendant, nous avons presque terminé cette seconde partie... La programmation d'interface graphique se rapproche !
Mais il nous reste une dernière chose à aborder qui peut s'avérer importante ! La réflexivité.


0 comments to "LECON 208"

Post a Comment

About This Blog

Aller au debut de la page