II - Classes et interfaces II-A - L' objet par l'exemple
II- D - Les interfaces
Une interface est un ensemble de prototypes de méthodes ou de propriétés qui forme un contrat. Une classe qui décide d'implémenter une interface s'engage à fournir une implémentation de toutes les méthodes définies dans l'interface. C'est le compilateur qui vérifie cette implémentation.
Voici par exemple la définition de l'interface java.util.Enumeration : Method Summary
boolean hasMoreElements() Tests if this enumeration contains more elements.
Object nextElement() Returns the next element of this enumeration if this enumeration object has
at least one more element to provide. Toute classe implémentant cette interface sera déclarée comme
public class C : Enumeration{ ...
boolean hasMoreElements(){....} Object nextElement(){...} }
Les méthodes hasMoreElements() et nextElement() devront être définies dans la classe C.
Considérons le code suivant définissant une classe élève définissant le nom d'un élève et sa note dans une matière : // une classe élève
public class élève{
// des attributs publics public String nom; public double note; // constructeur
public élève(String NOM, double NOTE){ nom=NOM;
note=NOTE; }//constructeur }//élève
Nous définissons une classe notes rassemblant les notes de tous les élèves dans une matière : // classes importées
// import élève // classe notes
public class notes{
// attributs
protected String matière; protected élève[] élèves;
// constructeur
public notes (String MATIERE, élève[] ELEVES){ // mémorisation élèves & matière
matière=MATIERE; élèves=ELEVES; }//notes
public String toString(){
String valeur="matière="+matière +", notes=("; int i;
// on concatène toutes les notes for (i=0;i<élèves.length-1;i++){
valeur+="["+élèves[i].nom+","+élèves[i].note+"],"; };
//dernière note
if(élèves.length!=0){ valeur+="["+élèves[i].nom+","+élèves[i].note+"]";} valeur+=")";
// fin
return valeur; }//toString
}//classe
Les attributs matière et élèves sont déclarés protected pour être accessibles d'une classe dérivée. Nous décidons de dériver la classe notes dans une classe notesStats qui aurait deux attributs supplémentaires, la moyenne et l'écart-type des notes :
public class notesStats extends notes implements Istats { // attributs
private double _moyenne; private double _écartType;
La classe notesStats dérive de la classe notes et implémente l'interface Istats suivante : // une interface
public interface Istats{ double moyenne(); double écartType(); }//
Cela signifie que la classe notesStats doit avoir deux méthodes appelées moyenne et écartType avec la signature indiquée dans l'interface Istats. La classe notesStats est la suivante :
// classes importées // import notes; // import Istats; // import élève;
public class notesStats extends notes implements Istats { // attributs
private double _moyenne; private double _écartType;
// constructeur
public notesStats (String MATIERE, élève[] ELEVES){ // construction de la classe parente
super(MATIERE,ELEVES); // calcul moyenne des notes double somme=0;
for (int i=0;i<élèves.length;i++){ somme+=élèves[i].note; } if(élèves.length!=0) _moyenne=somme/élèves.length; else _moyenne=-1; // écart-type double carrés=0;
for (int i=0;i<élèves.length;i++){
carrés+=Math.pow((élèves[i].note-_moyenne),2); }//for if(élèves.length!=0) _écartType=Math.sqrt(carrés/élèves.length); else _écartType=-1; }//constructeur // ToString
public String toString(){
}//ToString
// méthodes de l'interface Istats public double moyenne(){
// rend la moyenne des notes return _moyenne;
}//moyenne
public double écartType(){ // rend l'écart-type return _écartType; }//écartType
}//classe
La moyenne _moyenne et l'écart-type _ecartType sont calculés dès la construction de l'objet. Aussi les méthodes
moyenne et écartType n'ont-elles qu'à rendre la valeur des attributs _moyenne et _ecartType. Les deux méthodes
rendent -1 si le tableau des élèves est vide. La classe de test suivante :
// classes importées // import élève; // import Istats; // import notes; // import notesStats; // classe de test
public class test{
public static void main(String[] args){ // qqs élèves & notes
élève[] ELEVES=new élève[] { new élève("paul",14),new élève("nicole",16), new
élève("jacques",18)};
// qu'on enregistre dans un objet notes notes anglais=new notes("anglais",ELEVES); // et qu'on affiche
System.out.println(""+anglais); // idem avec moyenne et écart-type anglais=new notesStats("anglais",ELEVES); System.out.println(""+anglais);
}//main }//classe
donne les résultats :
matière=anglais, notes=([paul,14.0],[nicole,16.0],[jacques,18.0])
matière=anglais, notes=([paul,14.0],[nicole,16.0],[jacques,18.0]),moyenne=16.0,écarttype= 1.632993161855452
Les différentes classes de cet exemple font toutes l'objet d'un fichier source différent : E:\data\serge\JAVA\interfaces\notes>dir 06/06/2002 14:06 707 notes.java 06/06/2002 14:06 878 notes.class 06/06/2002 14:07 1 160 notesStats.java 06/06/2002 14:02 101 Istats.java 06/06/2002 14:02 138 Istats.class 06/06/2002 14:05 247 élève.java 06/06/2002 14:05 309 élève.class 06/06/2002 14:07 1 103 notesStats.class 06/06/2002 14:10 597 test.java 06/06/2002 14:10 931 test.class
La classe notesStats aurait très bien pu implémenter les méthodes moyenne et écartType pour elle-même sans indiquer qu'elle implémentait l'interface Istats. Quel est donc l'intérêt des interfaces ? C'est le suivant : une fonction
peut admettre pour paramètre une donnée ayant le type d'une interface I. Tout objet d'une classe C implémentant l'interface I pourra alors être paramètre de cette fonction. Considérons l'interface suivante :
// une interface Iexemple
public interface Iexemple{ int ajouter(int i,int j); int soustraire(int i,int j); }//interface
L'interface Iexemple définit deux méthodes ajouter et soustraire. Les classes classe1 et classe2 suivantes implémentent cette interface.
// classes importées // import Iexemple;
public class classe1 implements Iexemple{ public int ajouter(int a, int b){ return a+b+10;
}
public int soustraire(int a, int b){ return a-b+20;
}
}//classe
// classes importées // import Iexemple;
public class classe2 implements Iexemple{ public int ajouter(int a, int b){ return a+b+100;
}
public int soustraire(int a, int b){ return a-b+200;
} }//classe
Par souci de simplification de l'exemple les classes ne font rien d'autre que d'implémenter l'interface Iexemple. Maintenant considérons l'exemple suivant :
// classes importées // import classe1; // import classe2; // classe de test
public class test{
// une fonction statique
private static void calculer(int i, int j, Iexemple inter){ System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j)); }//calculer
// la fonction main
public static void main(String[] arg){
// création de deux objets classe1 et classe2 classe1 c1=new classe1();
classe2 c2=new classe2();
// appels de la fonction statique calculer calculer(4,3,c1);
calculer(14,13,c2); }//main
}//classe test
La fonction statique calculer admet pour paramètre un élément de type Iexemple. Elle pourra donc recevoir pour ce paramètre aussi bien un objet de type classe1 que de type classe2. C'est ce qui est fait dans la fonction main avec les résultats suivants :
21 127 201
On voit donc qu'on a là une propriété proche du polymorphisme vu pour les classes. Si donc un ensemble de classes Ci non liées entre-elles par héritage (donc on ne peut utiliser le polymorphisme de l'héritage) présentent un ensemble de méthodes de même signature, il peut être intéressant de regrouper ces méthodes dans une interface I dont hériteraient toutes les classes concernées. Des instances de ces classes Ci peuvent alors être utilisées comme paramètres de fonctions admettant un paramètre de type I, c.a.d. des fonctions n'utilisant que les méthodes des objets Ci définies dans l'interface I et non les attributs et méthodes particuliers des différentes classes Ci.
Dans l'exemple précédent, chaque classe ou interface faisait l'objet d'un fichier source séparé : E:\data\serge\JAVA\interfaces\opérations>dir 06/06/2002 14:33 128 Iexemple.java 06/06/2002 14:34 218 classe1.java 06/06/2002 14:32 220 classe2.java 06/06/2002 14:33 144 Iexemple.class 06/06/2002 14:34 325 classe1.class 06/06/2002 14:34 326 classe2.class 06/06/2002 14:36 583 test.java 06/06/2002 14:36 628 test.class
Notons enfin que l'héritage d'interfaces peut être multiple, c.a.d. qu'on peut écrire
public class classeDérivée extends classeDeBase implements i1,i2,..,in{ ...
}
où les ij sont des interfaces.
II-E - Classes anonymes
Dans l'exemple précédent, les classes classe1 et classe2 auraient pu ne pas être définies explicitement. Considérons le programme suivant qui fait sensiblement la même chose que le précédent mais sans la définition explicite des classes classe1 et classe2 :
// classes importées // import Iexemple; // classe de test
public class test2{
// une classe interne
private static class classe3 implements Iexemple{ public int ajouter(int a, int b){
return a+b+1000; }
public int soustraire(int a, int b){ return a-b+2000;
}
};//définition classe3
// une fonction statique
private static void calculer(int i, int j, Iexemple inter){ System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j)); }//calculer
// la fonction main
public static void main(String[] arg){
// création de deux objets implémentant l'interface Iexemple Iexemple i1=new Iexemple(){
return a+b+10; }
public int soustraire(int a, int b){ return a-b+20;
}
};//définition i1
Iexemple i2=new Iexemple(){
public int ajouter(int a, int b){ return a+b+100;
}
public int soustraire(int a, int b){ return a-b+200;
}
};//définition i2 // un autre objet Iexemple Iexemple i3=new classe3();
// appels de la fonction statique calculer calculer(4,3,i1);
calculer(14,13,i2); calculer(24,23,i3); }//main
}//classe test
La particularité se trouve dans le code :
// création de deux objets implémentant l'interface Iexemple Iexemple i1=new Iexemple(){
public int ajouter(int a, int b){ return a+b+10;
}
public int soustraire(int a, int b){ return a-b+20;
}
};//définition i1
On crée un objet i1 dont le seul rôle est d'implémenter l'interface Iexemple. Cet objet est de type Iexemple. On peut donc créer des objets de type interface. De très nombreuses méthodes de classes Java rendent des objets de type interface c.a.d. des objets dont le seul rôle est d'implémenter les méthodes d'une interface. Pour créer l'objet i1, on pourrait être tenté d'écrire :
Iexemple i1=new Iexemple()
Seulement une interface ne peut être instantiée. Seule une classe implémentant cette interface peut l'être. Ici, on définit une telle classe "à la volée" dans le corps même de la définition de l'objet i1 :
Iexemple i1=new Iexemple(){
public int ajouter(int a, int b){ // définition de ajouter }
public int soustraire(int a, int b){ // définition de soustraire }
};//définition i1
La signification d'une telle instruction est analogue à la séquence :
public class test2{ ... // une classe interne
private static class classe1 implements Iexemple{ public int ajouter(int a, int b){
// définition de ajouter }
// définition de soustraire }
};//définition classe1 ...
public static void main(String[] arg){ ...
Iexemple i1=new classe1(); }//main
}//classe
Dans l'exemple ci-dessus, on instantie bien une classe et non pas une interface. Une classe définie "à la volée" est dite une classe anonyme. C'est une méthode souvent utilisée pour instantier des objets dont le seul rôle est d'implémenter une interface.
L'exécution du programme précédent donne les résultats suivants : 17 21 127 201 1047 2001
L'exemple précéent utilisait des classes anonymes pour implémenter une interface. Celles-ci peuvent être utilisées également pour dériver des classes n'ayant pas de constructeurs avec paramètres. Considérons l'exemple suivant :
// classes importées // import Iexemple;
class classe3 implements Iexemple{ public int ajouter(int a, int b){ return a+b+1000;
}
public int soustraire(int a, int b){ return a-b+2000;
}
};//définition classe3
public class test4{
// une fonction statique
private static void calculer(int i, int j, Iexemple inter){ System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j)); }//calculer
// méthode main
public static void main(String args[]){
// définition d'une classe anonymé dérivant classe3 // pour redéfinir soustraire
classe3 i1=new classe3(){
public int ajouter(int a, int b){ return a+b+10000;
}//soustraire };//i1
// appels de la fonction statique calculer calculer(4,3,i1);
}//main }//classe
Nous y retrouvons une classe classe3 implémentant l'interface Iexemple. Dans la fonction main, nous définissons une variable i1 ayant pour type, une classe dérivée de classe3. Cette classe dérivée est définie "à la volée" dans une classe anonyme et redéfinit la méthode ajouter de la classe classe3. La syntaxe est identique à celle de la classe anonyme implémentant une interface. Seulement ici, le compilateur détecte que classe3 n'est pas une interface mais une classe. Pour lui, il s'agit alors d'une dérivation de classe. Toutes les méthodes qu'il trouvera dans le corps de la classe anonyme remplaceront les méthodes de même nom de la classe de base.
L'exécution du programme précédent donne les résultats suivants : E:\data\serge\JAVA\classes\anonyme>java test4
10007 2001