Eléments de Java - Université Paris Ouest Nanterre
Transcription
Eléments de Java - Université Paris Ouest Nanterre
Université Paris Ouest - Nanterre - la Défense Master DEFI 2010 - 2011 Eléments de Java Marcel Cori 1 Introduction Le langage de programmation Java a été introduit par l’entreprise Sun Microsystems en 1995. Il est très lié au développement d’Internet. 1.1 1.1.1 Quelques caractéristiques du langage Java Java est un langage orienté-objet Java fait partie, à la suite de SMALLTALK et C++, des langages orientés-objet, qui sont apparus vers 1985. Les éléments primitifs du langage sont les objets, ou plutôt les types d’objets, autrement dit les classes. Les programmes, écrits relativement à ces classes, sont des méthodes. Plus exactement, les classes étant organisées selon une hiérarchie, les méthodes sont définies au niveau adéquat de la hiérarchie. 1.1.2 Compilation et interprétation - Le compilateur de Java engendre du code qui est indépendant de toute machine, et donc peut être transmis par Internet. - L’interpréteur de Java, appelé machine virtuelle (JVM), exécute le code compilé. Rien, dans le langage, n’est dépendant d’une implémentation particulière. Par exemple, la dimension de chacun des types primitifs est spécifiée par le langage. 1.1.3 Robustesse, sécurité - Les déclarations doivent être explicites. La compatibilité entre les types est vérifiée à la compilation. - A l’exécution, on ne peut accéder aux tableaux et aux suites de caractères que dans les bornes. - On ne peut accéder directement à la mémoire de l’ordinateur. 1.2 1.2.1 Aspects pratiques Définition et compilation des classes Une classe peut être définie de la manière suivante : public class NomDeLaClasse { hcorps de la classei } 2 La classe doit être stockée dans un fichier nommé NomDeLaClasse.java. On compile par la commande : javac NomDeLaClasse.java Dans le cas où il y a succès de la compilation, est engendré un fichier NomDeLaClasse.class 1.2.2 L’exécution d’un programme principal A l’intérieur d’une classe qui a l’attribut public1 , on définit une méthode dont le nom est main, de la manière qui suit : public static void main(String[ ] args) { hcorps de la méthodei } L’exécution s’effectue à l’aide de la commande : java NomDeLaClasse Par exemple, la classe qui suit doit être définie dans un fichier nommé Bonjour.java. public class Bonjour{ public static void main(String[ ] args){ System.out.println("Bonjour tout le monde") ; } } 1.2.3 Les commentaires En Java les commentaires peuvent s’écrire : - sur une ligne commençant par un double slash : //· · · - sur un nombre quelconque de lignes, entre slash étoile et étoile slash : /* · · · 1.2.4 */ Les paquets Java est donné avec un ensemble de classes prédéfinies, qui sont rangées dans des paquets (packages). Ainsi java.awt (pour les interfaces graphiques), java.io (pour les entrées/sorties), java.lang qui contient les éléments principaux du langage. Par exemple BufferedReader est une classe de java.io. On doit donc écrire java.io.BufferedReader Mais on peut éviter cette écriture grâce à l’instruction import : import java.io.BufferedReader ; ou, plus généralement, import java.io.* ; qui s’applique à toutes les classes du paquet. 1 Voir plus loin, page 20. 3 Cette instruction est placée au tout début du fichier où est stockée une classe, avant même la définition de la classe. On remarquera que l’instruction import java.lang.* ; est implicite. 1.2.5 Les applets Les applets sont des programmes écrits sur des pages Web, dont l’exécution est commandée par un navigateur comme Netscape ou Internet Explorer. Les applets ne contiennent pas de méthode main. Un exemple d’applet est donné ci-dessous : public class Oval extends Applet{ public void paint(Graphics g) { g.setColor(Color.green) ; g.fillOval(10,10,330,100) ; } } Cet applet peut être appelé par l’ouverture du fichier HTML : <HTML> <HEAD> <TITLE> Oval </TITLE> </HEAD> <BODY> <P> <APPLET code = "Oval.class" width = 380 height = 150> </APPLET> </BODY> </HTML> 4 2 La syntaxe de base 2.1 2.1.1 - - Variables et constantes Les types primitifs Il y a huit types primitifs en des types entiers : byte short int long des types réels : float double le type caractère : char le type booléen : boolean 2.1.2 Java : codé sur codé sur codé sur codé sur codé sur codé sur 8 bits 16 bits 32 bits 64 bits 32 bits 64 bits Les constantes Les constantes booléennes sont true et false. Les caractères constants sont notés entre “quotes” : ’a’. Certains caractères sont notés de manière particulière : ’\n’ nouvelle ligne ’\\’ backslash ’\’’ quote ’\"’ guillemet Les constantes de type String (non primitif) seront notées entre guillemets : "Bonjour" 2.1.3 Les variables et leurs déclarations Dans les identificateurs de Java, et en particulier dans les noms des variables, la différence majuscules/minuscules est significative. Conventionnellement, on fait commencer les noms des variables par des minuscules2 . En Java, les déclarations ne sont pas séparées des instructions exécutables. On déclare une variable à proximité de sa première utilisation. La forme d’une déclaration peut être la suivante : htypei hliste de nomsi ; 2 Les classes commenceront par des majuscules. 5 Par exemple : char a ; int x,y ; 2.1.4 Initialisation des variables et affectation Une variable doit être initialisée avant sa première utilisation. Cela s’effectue à l’aide de l’instruction d’affectation, qui a la forme générale suivante : hnomi = hvaleuri ; une valeur pouvant être donnée par une constante, une variable, ou une expression. Par exemple : x = 18 ; y = x; La déclaration de la variable peut être regroupée avec son initialisation. De la manière suivante : char lettre = ’a’, b = ’b’ ; char l1 = lettre ; 2.2 2.2.1 Opérateurs Les opérateurs arithmétiques Les opérateurs arithmétiques sont “+” (pour l’addition), “-” (pour la soustraction), “*” (pour la multiplication) et “/” (pour la division). Ce dernier opérateur ne va pas donner le même résultat selon le type des opérandes. Ainsi, à l’issue de l’exécution des trois instructions qui suivent float x = 5 ; float y = 2 ; float quotient = x/y ; la variable quotient va contenir la valeur 2.5, alors qu’à la suite des instructions int x = 5 ; int y = 2 ; float quotient = x/y ; la valeur contenue sera 2.0. 2.2.2 L’opérateur de conversion de types Un type entre parenthèses placé devant une valeur convertit la valeur dans le type. C’est ainsi que si on remplace la dernière instruction de la séquence ci-dessus par float quotient = (float) x/y ; on obtient encore3 pour résultat 2.5. 3 (float) x construit une valeur réelle, qui devient le premier opérande de la division, opérande qui décide du type du résultat. 6 2.2.3 Les affectations complexes On peut écrire a += b ; Cela équivaut à : a = a + b; De même on peut utiliser les affectations complexes “-=”, “*=” et “/=”. L’auto-incrémentation utilise le symbole “++”. L’écriture de ++ x ; ou de x ++ ; équivaut à x = x + 1; Toutefois, on n’obtiendra pas le même résultat si on écrit x = 3; y = ++ x ; ou si on écrit x = 3; y = x ++ ; Dans les deux cas, on aura x = 4, mais dans le premier cas on aura y = 4, alors que dans le deuxième cas, on aura y = 3. De même, l’auto-décrémentation utilise le symbole “--”. 2.2.4 Les opérateurs de comparaison Les opérateurs de comparaison sont “==” (pour l’égalité), “<” (inférieur), “<=” (inférieur ou égal), “>” (supérieur), “>=” (supérieur ou égal), “ !=” (différent). 2.2.5 Les opérateurs logiques Les opérateurs logiques sont “&&” ou “&” (et), “||” ou “|” (ou) et “ !” (non). Il est plus économique d’utiliser les opérateurs “&&” ou “||” plutôt que “&” ou “|”, et dans certains cas cela évite des erreurs à l’exécution. En effet, dans le calcul de l’expression y == 0 || x/y > 100 si on trouve la valeur true pour le membre gauche de la disjonction, on n’examine pas le membre droit. 2.2.6 La priorité entre opérateurs Les opérateurs * et / sont prioritaires par rapport aux opérateurs + et -. Les opérateurs arithmétiques sont prioritaires par rapport aux opérateurs de comparaison. Les opérateurs de comparaison sont prioritaires par rapport aux opérateurs logiques. L’opérateur de négation est prioritaire par rapprot aux opérateurs ou et et. L’opérateur et est prioritaire par rapport à l’opérateur ou. En cas de priorités égales, les opérations sont effectuées de gauche à droite. 7 2.3 2.3.1 Les instructions conditionnelles Les instructions if Les deux formes de l’instruction if sont les suivantes : if (hconditioni) hinstructioni if (hconditioni) hinstructioni else hinstructioni La condition est une expression qui produit un résultat de type booléen. Un bloc peut remplacer une instruction : un bloc est composé de plusieurs instructions qui se suivent, encadrées par des accolades. Il est possible, dans une expression, de calculer une valeur selon le résultat d’un test. On peut ainsi écrire : a = b < 2 ? 1 : 5; a prendra la valeur 1 si b est inférieur à 2, la valeur 5 sinon. 2.3.2 Les instructions itératives On définit les instructions while, for et do. while (hconditioni) hinstructioni for (hinitialisationi ;htesti ; hmise à jouri) hinstructioni do hinstructioni while (hconditioni) ; Les conditions sont encore des expressions qui produisent un résultat booléen. Les instructions peuvent encore être remplacées par des blocs. L’initialisation affecte une valeur (initiale) à au moins une variable entière. Par exemple : i = 1. La mise à jour incrémente ou décrémente des variables entières. Par exemple : i++. Le test est une condition qui détermine jusqu’à quel moment on reste dans la boucle. Par exemple : i <= n. L’instruction break ; permet de sortir d’une itération à n’importe quel moment. Elle permet de quitter une boucle plus extérieure si celle-ci a été repérée par une étiquette. On écrit par exemple hétiquettei : while ... et, à l’intérieur de cette boucle : break hétiquettei ; 8 L’instruction continue ; permet de repartir au début de la boucle, sans en sortir. On peut également écrire continue hétiquettei ; pour repartir au début d’une boucle plus extérieure. 2.3.3 Les aiguillages L’instruction switch permet d’exécuter conditionnellement une instruction selon une valeur de type entier ou caractère. Par exemple : switch(x) { case 2 : case 4 : hsuite d’instructions qui seront exécutées si x vaut 2 ou 4i break ; case 3 : hsuite d’instructions qui seront exécutées si x vaut 3i break ; default : hsuite d’instructions qui seront exécutées si x prend une autre valeuri break ; } La partie default de l’aiguillage est facultative. Si elle n’est pas présente, et si les valeurs explicitement prévues ne sont pas atteintes, aucune instruction n’est exécutée. 9 3 Objets, classes, méthodes 3.1 3.1.1 Les tableaux Déclaration et initialisation La déclaration d’un tableau s’effectue d’une des deux manières suivantes : htypei [ ] hnomi ; htypei hnomi [ ] ; Un tableau peut être initialisé par toutes les valeurs qui le composent. Par exemple : int [ ] tab = {5,4,3,2,1} ; Mais on peut également créer un tableau sans en donner les valeurs initiales, de la manière qui suit : htypei[ ] hnomi = new htypei[hvaleuri] ; Selon le type déclaré, les éléments du tableau seront initialisés à 0, au caractère “espace” ou à l’objet null4 . 3.1.2 Accès aux éléments On accède ensuite à chaque élément du tableau par : hnomi [hvaleuri] ; La première valeur possible renvoyant à un élément du tableau, autrement dit le premier indice possible, est 0 : sur l’exemple ci-dessus tab[0] est initialisé à 5. On notera que chaque fois que l’on accède à un élément du tableau, il y a une vérification que l’indice spécifié s’inscrit bien dans les bornes prévues. Par ailleurs, la dimension du tableau peut être obtenue par hnomi.length. Ainsi, si un tableau tab est tel que tab.length vaut 5, on accède aux éléments de tab[0] à tab[4]. 3.1.3 Tableaux à plusieurs dimensions Un tableau à plusieurs dimensions n’est rien d’autre qu’un tableau de tableaux. C’est ainsi que l’on peut déclarer : int [ ] [ ] matrice = new int[5][7] ; On accède à un élément par exemple par matrice[i][j]. 4 Voir ci-après, paragraphe 3.1.5. 10 On peut également initialiser un tableau à plusieurs dimensions par des valeurs constantes. Par exemple : char table [ ][ ]= {{’a’,’b’,’c’},{’a’,’b’},{’a’},{ }} ; On remarque que les tableaux à une dimension qui composent le tableau à deux dimensions n’ont pas nécessairement tous la même dimension, et même qu’un de ces tableaux peut avoir pour dimension 0. On peut fixer une des dimensions du tableau, par exemple par int matrice [ ] [ ] = new int[10] [ ] ; et ensuite obtenir chacune des autres dimensions après calcul : matrice[i] = new int[i++] ; matrice.length donne la première dimension du tableau, matrice[i].length donne une deuxième dimension, etc. 3.1.4 Les tableaux dans des listes d’arguments Dans une liste d’arguments, un tableau apparaı̂t selon l’une des deux manières qui suivent : a(int [ ] tab, ...) a(int tab[ ], ...) C’est ainsi que l’unique argument de la méthode main est un tableau de String, auquel nous avons donné plus haut (page 3) le nom args. 3.1.5 Les tableaux sont des références En Java, les types de données non primitifs, c’est-à-dire les tableaux et les objets, sont des références. C’est-à-dire que c’est l’adresse du tableau ou de l’objet qui est stockée. La déclaration d’un objet ou d’un tableau ne libère pas d’espace en mémoire pour cet objet, mais pour son adresse. D’où l’instruction new qui libère effectivement l’espace. Une conséquence en est que si t1 et t2 sont deux tableaux, une instruction telle que t1 = t2 ; produit l’identification des deux tableaux. Ensuite, l’écriture de t1[2] = 5 ; modifie aussi bien le tableau t2 que le tableau t1. Une référence particulière est la référence null. Elle représente une adresse qui ne réfère à rien. 11 3.2 3.2.1 Les String Caractéristiques particulières Les String sont des objets, mais qui partagent certaines caractéristiques avec les types primitifs : - il existe des String constants, comme "Bonjour" ; - il est possible d’initialiser un String sans passer par new : String s = "bonjour" ; String vide = "" ; String t = s ; - il existe un opérateur sur les String, l’opérateur de concaténation, noté +. Cet opérateur sera utilisé notamment pour l’écriture sur une même ligne de plusieurs données, car System.out.println prend pour argument un String unique. Par ailleurs, un String est non transformable (contrairement aux tableaux). En conséquence, si l’on écrit : String a = "bon" ; String b = a ; a = a + "jour" ; on obtient "bonjour" comme valeur associée à a, et "bon" comme valeur associée à b. Cela signifie que l’on a construit une nouvelle référence pour a. 3.2.2 Les méthodes sur les String Les méthodes s’appliquent à des classes d’objets. Ainsi les méthodes définies ci-dessous s’appliquent aux String. Soient a et b deux String, n et p deux nombres entiers. a.charAt(n) fournit le (n+1)-ième caractère de a ; a.length() fournit la longueur de a ; a.substring(n,p) fournit le String compris entre le (n+1)-ième caractère et le p-ième caractère de a ; a.indexOf(b) prend une valeur de type int : une valeur négative si b n’est pas une sous-suite de a, n-1 où n est la place de la première occurrence de b dans a sinon ; a.equals(b) prend la valeur vrai si a et b réfèrent à des String qui sont égaux, prend la valeur faux sinon ; a.compareTo(b) prend une valeur de type int : une valeur négative si a précède b dans l’ordre alphabétique, une valeur positive si b précède a, la valeur 0 si les deux String sont égaux. 3.3 Les classes Les classes décrivent des types d’objets : - les attributs de ces objets, sous la forme de champs, 12 - les actions qui s’appliquent à ces objets, sous la forme de méthodes. 3.3.1 La définition d’une classe Ci-dessous est définie la classe Etudiant, sans aucune méthode5 : public class Etudiant{ public String nom, prenom ; public int naissance ; public float note1, note2 ;} Cette classe comporte cinq champs : le nom, le prénom, l’année de naissance, ainsi que deux notes. 3.3.2 La création et la manipulation des objets Une fois une classe définie, on peut déclarer et créer des objets appartenant à cette classe. Cela s’effectue grâce à l’instruction new. Etudiant x = new Etudiant( ) ; Les différents champs de l’objet sont initialisés à des valeurs par défaut par l’instruction new : zéro pour les champs numériques, la valeur null pour les références, ainsi les String. On peut accéder aux différents champs de l’objet de la manière suivante : x.nom = "Dupont" ; x.note1 = 15 ; System.out.println("la note de " + x.nom + " est " + x.note1) ; 3.3.3 3.3.3.1 Les méthodes La définition des méthodes Considérons à nouveau la classe Etudiant, mais cette fois définie avec quatre méthodes6 . public class Etudiant{ public String nom, prenom ; public int naissance ; public float note1, note2 ; public int age(int anneeCourante) {return anneCourante - naissance ;} public float moyenne( ) {return note1<note2 ? note2 : (note1+note2)/2 ;} public boolean egal(Etudiant z) 5 La classe doit être enregistrée dans un fichier Etudiant.java et compilée séparément. On notera que l’ordre de déclaration des méthodes dans la classe n’a pas d’importance, de même que l’ordre de déclaration des champs. 6 13 {if (nom.equals(z.nom) && prenom.equals(z.prenom)) return true ; else return false ;} public void imprime( ) {System.out.println(prenom + " " + nom + ", né en " + naissance) ; if(note1 = 0 && note2 = 0) return ; System.out.println("première note : " + note1) ; System.out.println("deuxième note : " + note2) ;} } Une méthode est composée d’un en-tête et d’un corps. Pour l’instant, la forme de l’entête d’une méthode est : public htypei hméthodei (hliste de paramètresi) La liste de paramètres peut être vide (exemple des méthodes moyenne et imprime). Auquel cas les parenthèses demeurent. Le type, qui spécifie le type du résultat de la méthode, peut être remplacé par l’attribut void qui indique que la méthode ne fournit pas de résultat. On notera que plusieurs méthodes différentes portant le même nom peuvent être définies. C’est la signature de la méthode, c’est-à-dire la liste de types des paramètres, qui fait la différence. Le corps de la méthode est constitué d’un bloc d’instructions, entre accolades. Ce bloc doit contenir l’instruction return hvaleuri ; la valeur étant du type défini pour le résultat de la méthode. Si la méthode est définie avec l’attribut void, l’instruction est simplement return ; et elle n’est pas obligatoire. Elle permet simplement de sortir de la méthode avant la dernière instruction (exemple de la méthode imprime). 3.3.3.2 L’appel des méthodes Ci-dessous une suite d’instructions qui permettent d’initialiser un objet de type Etudiant et d’accéder aux méthodes définies dans la classe Etudiant. Etudiant x = new Etudiant( ) ; x.nom = "Dupont" ; x.prenom = "Jean" ; x.naissance = 1972 ; x.note1 = 15 ; x.note2 = 9 ; if x.moyenne( ) >= 10. System.out.println("l’étudiant est reçu"") ; else System.out.println("l’étudiant est collé"") ; 14 int a = 2004 ; System.out.println("^ age : " + x.age(a)) ; L’appel des méthodes s’effectue “par valeurs”. Cela signifie que les arguments qui sont d’un des types primitifs ne peuvent être altérés par l’exécution d’une méthode. 3.3.4 Les constructeurs L’instruction new fait appel à une méthode qui a le même nom qu’une classe. Cette méthode est un constructeur. Le constructeur peut être défini implicitement, par la définition de la classe, mais on peut aussi écrire explicitement des constructeurs. Par exemple : public class Etudiant{ public String nom, prenom ; public int naissance ; public float note1, note2 ; public Etudiant(String a, String b) {this.nom = b ; this.prenom = a ;} } this désigne l’objet de la classe que l’on construit. Les champs non initialisés par le constructeur prennent une valeur par défaut7 . Le constructeur est une méthode qui ne comporte pas d’instruction return (bien que n’ayant pas l’attribut void). L’appel du constructeur peut alors s’effectuer de la manière suivante : Etudiant x = new Etudiant("Jean","Dupont") ; Une même classe peut posséder plusieurs constructeurs : public class Etudiant{ public String nom, prenom ; public int naissance ; public float note1, note2 ; public Etudiant() { } public Etudiant(String a, String b) {this.nom = b ; this.prenom = a ;} public Etudiant(String a) {int i ; 7 C’est ce qui se produit pour tous les champs quand on a un constructeur implicite. 15 for (i=1 ; i <= a.length() ; i++) if (a.charAt(i) == ’ ’) break ; this.nom = a.substring(i,a.length()) ; this.prenom = a.substring(0,i) ;} public Etudiant(Etudiant x) {this.nom = x.nom ; this.prenom = x.prenom ; this.naissance= x.naissance ; this.note1 = x.note1 ; this.note2 = x.note2 ;} } Différents appels des constructeurs sont alors possibles : Etudiant Etudiant Etudiant Etudiant e1 e2 e3 e4 = = = = new new new new Etudiant("Jean","Dupont") ; Etudiant("Jean Dupont") ; Etudiant() ; Etudiant(e3) ; C’est encore la signature des constructeurs qui permet au système de faire la différence. Quand un constructeur explicite est défini, on ne peut plus utiliser le constructeur implicite, à moins de le redéfinir (comme c’est le cas ci-dessus). On remarquera enfin qu’on peut initialiser un objet de la classe Etudiant à partir d’un autre objet de la même classe (dernier constructeur défini). 3.3.5 3.3.5.1 Variables et méthodes de classe Les variables de classe Il est possible d’attacher une variable à une classe donnée. On dit alors qu’il s’agit d’une variable de classe. Une variable de classe est déclarée avec l’attribut static. Ainsi : public class Etudiant{ public String nom, prenom ; public float note1, note2 ; static int nbRecus = 0 ; public Etudiant(String a, String b, float n1, float n2) {this.nom = a ; this.prenom = b ; this.note1 = n1 ; this.note2 = n2 ; if (this.moyenne() >= 10) nbRecus++ ;} public float moyenne( ) 16 {return note1<note2 ? note2 : (note1+note2)/2 ;} } A chaque fois qu’on appelle le constructeur de la classe, la variable de classe est incrémentée. On accède ensuite à la valeur de cette variable à travers la classe, par une instruction telle que : System.out.println("Nombre d’étudiants reçus = " + Etudiant.nbRecus) ; Remarque : System est une classe ; out est une variable de cette classe, de type PrintStream ; println est une méthode définie pour les objets de type PrintStream. Cela explique la forme donnée à l’instruction d’écriture que nous avons employée jusqu’à présent. 3.3.5.2 Les constantes de classe A un nom, il peut être associé dans une classe une valeur qui ne pourra ensuite être modifiée. C’est l’attribut final qui indique que la valeur ne sera pas modifiée : public static final int annee = 2004 ; 3.3.5.3 Les méthodes de classe Il y a également des méthodes de classe qui se distinguent des méthodes d’instance. Pour le comprendre, considérons les deux méthodes définies ci-dessous à l’intérieur de la classe Personne. public class Personne{ public String nom ; public int naissance ; public Personne plusVieux(Personne x) ; {return this.naissance > x.naissance ? x : this ;} public static Personne plusVieux(Personne a, Personne b) ; {return a.naissance > b.naissance ? b : a ;} } Pour deux variables p1 et p2 de type Personne, que l’on suppose intialisées, l’appel des méthodes s’effectue comme suit : Personne p = p1.plusVieux(p2) ; Personne q = Personne.plusVieux(p1,p2) ; Les deux appels vont produire ici le même résultat. La méthode d’instance est appelée à travers une instance de la classe, la méthode de classe à travers le nom de la classe. 17 Il existe des méthodes de classe prédéfinies. Ainsi, Math.sqrt(x) va donner la racine carrée de la valeur réelle x. 3.4 3.4.1 3.4.1.1 Phénomènes d’héritage Les sous-classes L’extension d’une classe Les étudiants peuvent être considérés comme des personnes particulières. L’extension d’une classe revient à définir une sous-classe. En effet, en ajoutant des champs et des méthodes supplémentaires à une classe donnée, on construit une classe plus spécifique. Pour indiquer qu’une classe est l’extension d’une autre classe, on utilise le mot clé extends. Ainsi : public class Personne{ public String nom, prenom ; public int naissance ; public Personne() { } public Personne(String a, String b) {this.nom = b ; this.prenom = a ;} public int age(int anneeCourante) {return anneCourante - naissance ;} public boolean egal(Personne z) {if (nom.equals(z.nom) && prenom.equals(z.prenom)) return true ; else return false ;} } public class Etudiant extends Personne{ public float note1, note2 ; public float moyenne( ) {return note1<note2 ? note2 : (note1+note2)/2 ;} public void imprime( ) {System.out.println(prenom + " " + nom + ", né en " + naissance) ; if(note1 = 0 && note2 = 0) return ; System.out.println("première note : " + note1) ; System.out.println("deuxième note : " + note2) ;} } On remarque que les champs et méthodes sont indiqués au niveau adéquat de la hiérarchie, la classe Etudiant héritant des champs et des méthodes de la classe Personne. La séquence d’instructions ci-dessous est alors possible : 18 Etudiant String n Personne int nn = e = new Etudiant() ; = e.nom ; p = e; e.age(2004) ; En revanche, l’appel de p.note1 ne serait pas possible. On notera qu’en fait toute classe est sous-classe d’une autre classe. Si une classe n’est pas définie explicitement comme l’extension d’une autre classe, elle sera considérée comme sous-classe de la classe prédéfinie Object. La hiérarchie des classes selon la relation d’extension forme un arbre, dont la racine est la classe Object. Remarque : Deux champs peuvent être déclarés dans la classe et dans son extension (par exemple le champ numero peut représenter le numéro national d’identification pour la classe Personne et le numéro d’étudiant pour la classe Etudiant). En ce cas, on accède à l’un ou l’autre des champs selon le type d’objet en cause. 3.4.1.2 L’écriture des constructeurs Il est intéressant de faire appel à un constructeur de la classe pour définir le constructeur de son extension. On utilise à cette fin le mot clé super. Par exemple ci-dessous dans la définition d’un constructeur de la classe Etudiant : public class Etudiant extends Personne{ public float note1, note2 ; public Etudiant(String a, String b, int n, float n1, float n2) {super(a,b,n) ; this.note1 = n1 ; this.note2 = n2 ;} } super ne peut être utilisé qu’en tant que première instruction d’un constructeur. Si on n’utilise pas super, tout se passe en fait comme si on appelait implicitement super( ). 3.4.1.3 Le recouvrement des méthodes Quand une méthode associée à une sous-classe a le même nom, la même signature et le même type de résultat qu’une autre méthode associée à la classe qu’elle étend, il y a recouvrement des méthodes. Cela correspond à une exception à l’héritage des propriétés. Ainsi : public class Etudiant extends Personne{ public float note1, note2 ; 19 public float moyenne( ) {return note1<note2 ? note2 : (note1+note2)/2 ;} } public class EtudiantTravailleur extends Etudiant{ public float moyenne( ) {return note1<note2 ? note2 : note1 ;} } La moyenne n’est pas calculée de la manière habituelle pour les étudiants-travailleurs. Il est toutefois possible de calculer à l’intérieur de la classe EtudiantTravailleur la moyenne obtenue pour un étudiant ordinaire, en utilisant encore le mot clé super : public class EtudiantTravailleur extends Etudiant{ public float moyenne( ) {return note1<note2 ? note2 : note1 ;} public float moyenneEtudiant( ) {return super.moyenne( ) ;} } 3.4.2 3.4.2.1 Dissimulation de données Modificateurs de visibilité Les variables internes aux classes peuvent être déclarées avec l’attribut private au lieu de public. Un champ qui a l’attribut private est uniquement visible dans les méthodes définies à l’intérieur de la classe. Il existe deux autres niveaux de visibilité : - si un champ a l’attribut protetcted, il est visible à l’intérieur de la classe où le champ est défini, ainsi que dans toutes les sous-classes et dans toutes les classes qui sont dans le même paquet ; - si un champ n’a aucun des attributs private, protetcted ou public, il est visible uniquement dans le paquet où il est défini. Pas dans les sous-classes qui sont dans un autre paquet. 3.4.2.2 Méthodes d’accès aux données Pour accéder à des données qui sont dissimulées, il faut écrire des méthodes spécifiques. Si par exemple on décide de dissimuler les notes dans la classe Etudiant afin de ne pas risquer d’obtenir des valeurs hors norme, on ne peut plus initialiser ou modifier les valeurs des notes autrement que par l’intermédiaire d’un constructeur ou d’une méthode spécifique, et on ne peut connaı̂tre les notes qu’à travers des méthodes spécifiques. public class Etudiant{ 20 public String nom, prenom ; private float note1, note2 ; private boolean testNote(float n) {return n <= 20. && n >= 0. ;} //Constructeur public vérifiant la conformité des notes public Etudiant(String a, String b, float n1, float n2) {this.nom = b ; this.prenom = a ; if (testNote(n1)) this.note1 = n1 ; else this.note1 = 10. ; if (testNote(n2)) this.note2 = n2 ; else this.note2 = 10. ;} //Méthodes d’accès aux données public float quelleNote1( ) {return note1 ;} public float quelleNote2( ) {return note2 ;} //Méthodes permettant de modifier les données public void changeNotes(float n1, float n2) {if testNote(n1)) this.note1 = n1 ; if testNote(n2)) this.note2 = n2 ;} public void incrementeNotes(float inc) {note1 += inc ; if (note1 > 20.) note1 = 20. ; note2 += inc ; if (note2 > 20.) note2 = 20. ;} } Dans un programme qui fait appel à un objet de type Etudiant, pour accéder aux notes on écrit x.quelleNote1( ) au lieu de x.note1, ce qui interdit de modifier le champ concerné8 . 3.4.3 3.4.3.1 Classes abstraites et interfaces Classes abstraites Supposons que nous ayons à représenter des étudiants de maı̂trise et des étudiants de licence, et qu’il y ait à calculer une moyenne pour les uns et les autres, mais pas de la même façon. Il peut en ce cas être intéressant de définir une classe Etudiant, pour laquelle la métode moyenne soit définie “abstraitement”, c’est-à-dire sans instructions exécutables. La classe est dite classe abstraite. Elle peut contenir des méthodes abstraites et non abstraites, mais seules les classes abstraites peuvent contenir des méthodes abstraites. 8 En effet, x.quelleNote1( ) ne peut par exemple pas apparaı̂tre dans la partie gauche d’une affectation. 21 La classe abstraite Etudiant admettra alors deux sous-classes, EtudiantDeMaitrise et EtudiantDeLicence9 . Une classe abstraite ne peut être instanciée directement : seules ses sous-classes non abstraites peuvent être instanciées. 3.4.3.2 Méthodes abstraites Considérons la classe abstraite Etudiant, accompagnée de deux méthodes abstraites. Puis les sous-classes (non abstraites) EtudiantDeMaitrise et EtudiantDeLicence. public abstract class Etudiant extends Personne{ public Etudiant(String a, String b) {super(a,b) ;} public abstract float moyenne( ) ; public abstract boolean estRecu( ) ; } public class EtudiantDeMaitrise extends Etudiant{ public float cours, memoire, stage ; public EtudiantDeMaitrise(String a, String b, float c, float m, float s) {super(a,b) ; this.cours = c ; this.memoire = m ; this.stage = s ;} public float moyenne( ) {return (cours * 5 + memoire * 6 + stage * 2) / 13 ;} public boolean estRecu( ) {if (cours == 0 || memoire == 0 || stage == 0) return false ; return moyenne( ) >= 0 ? true : false ;} } public class EtudiantDeLicence extends Etudiant{ public float cours1, cours2 ; public EtudiantDeLicence(String a, String b, float c1, float c2) {super(a,b) ; this.cours1 = c1 ; this.cours2 = c2 ;} public float moyenne( ) {return (cours1 + cours2) / 2 ;} public boolean estRecu( ) {if (cours1 >= 10 && cours2 >= 10) return true ; else return false ;} 9 Une classe abstraite peut admettre des sous-classes abstraites et des sous-classes non abstraites. 22 } On peut ensuite définir un tableau d’étudiants, qui contiendra indifféremment des étudiants de maı̂trise et des étudiants de licence. Et ensuite faire appel aux méthodes moyenne et estRecu pour des éléments de ce tableau : Etudiant [ ] tab = new Etudiant[30] ; tab[0] = new EtudiantDeLicence("Anne","Dubois",12.,8.5) ; tab[1] = new EtudiantDeMaitrise("Luc","Martin",10.,14.,15.) ; ··· int nb = 0 ; for (int i = 0 ; i < tab.length ; i++) if(tab[i].estRecu( )) nb++ ; System.out.println("le nombre des reçus est " + nb) ; On notera que la sous-classe non abstraite d’une classe abstraite doit donner un corps à toutes les méthodes abstraites des classes abstraites dont elle hérite. 3.4.3.3 Interfaces En Java, il ne peut y avoir d’héritage multiple : une sous-classe ne peut être l’extension de plusieurs classes. Il existe toutefois des interfaces, qui ne sont pas vraiment des classes : contrairement aux classes abstraites, on ne peut y définir que des méthodes abstraites. Toutes les variables d’une interface doivent avoir les attributs static et final, c’està-dire que ça doit être des constantes. Une classe peut implémenter une interface, de la même manière qu’elle est l’extension d’une classe (abstraite ou non)10 . Mais la classe doit implémenter toutes les méthodes abstraites de l’interface. Par exemple : public interface Boursier {public float montantBourse( ) ;} public class EtudiantDeLicenceBoursier extends EtudiantDeLicence implements Boursier { public EtudiantDeLicenceBoursier(String a, String b, float n1, float n2) {super(a,b,n1,n2) ;} public float montantBourse( ) {return (float) (1000.+cours1*100+cours2*150) ;} } 10 Une interface peut être l’extension d’une autre interface, mais pas d’une classe. 23 public class EtudiantDeMaitriseBoursier extends EtudiantDeMaitrise implements Boursier { public EtudiantDeMaitriseBoursier(String a, String b, float c, float m, float s) {super(a,b,c,m,s) ;} public float montantBourse( ) {return (float) (1200.+cours*100+memoire*150+stage*80) ;} } On peut ensuite définir un tableau d’étudiants et un tableau de boursiers, et affecter aux cases des différents tableaux les mêmes objets, comme suit : Etudiant[ ] te = new Etudiant[20] ; Boursier[ ] tb = new Boursier[30] ; EtudiantDeLicenceBoursier elb = new EtudiantDeLIcenceBoursier("Luc","Dubois",8,14) ; te[0] = elb ; tb[0] = elb ; Ce qui permet de calculer tb[0].montantBourse( ), mais pas te[0].montantBourse( ). En revanche, on pourra calculer te[0].moyenne( ). 24 4 Compléments 4.1 4.1.1 Les exceptions Introduction Les exceptions sont des événements “anormaux” qui sont susceptibles de se produire à l’exécution d’un programme. Notamment des événements qui pourraient provoquer des erreurs à l’exécution, et qui sont indétectables à la compilation. Par exemple : une division par zéro, un indice hors des bornes d’un tableau ou la tentative d’ouvrir un fichier inexistant. Le programmeur peut déterminer quoi faire lorsqu’une exception se produit. Sinon, l’exception produira l’arrêt de l’exécution du programme. 4.1.2 La classe des exceptions Les exceptions sont des objets. Il existe une classe, la classe java.lang.Throwable, qui admet les deux sous-classes Error et Exception. Les Error sont des événements anormaux pour lesquels il n’est pas possible de définir de traitement : s’ils se produisent, l’exécution du programme s’arrête obligatoirement. On peut citer les classes d’exceptions courantes : - ArithmeticException : pour un nombre entier trop grand, ou une division entière par 0 ; - IndexOutOfBoundsException : pour un indice illégal dans un tableau ou un String. 4.1.3 Le traitement des exceptions Le traitement des exceptions s’effectue par l’instruction try/catch/finally. try délimite un bloc d’instructions dans lequel il est possible qu’une exception se produise. Le bloc try est suivi par zéro, une ou plusieurs clauses catch. La syntaxe de catch est la suivante : catch(hclassei hnomi) {hliste d’instructionsi} La classe indiquée peut être n’importe quelle sous-classe de la classe Throwable. public class Essai{ public static void main(String[ ] args) {int maj = ? ? ; int[ ] tab = {25,24,20,12,15} ; 25 try{for (int i=4 ; i>=0 ; i--) System.out.println(tab[i+maj]/i) ;} catch(ArithmeticException e) {System.out.println("faut pas diviser par zéro") ; System.out.println(e) ;} catch(Exception e) {System.out.println("il y a un problème") ; System.out.println(e) ;}} } Quand un événement anormal se produit dans un bloc try, le système cherche le catch situé dans le plus petit bloc englobant qui porte sur un événement de ce type. Les instructions définies dans catch sont exécutées, puis on sort du bloc contenant la combinaison try/catch. Ici, selon la valeur affectée à maj, on sortira du bloc try parce que l’indice du tableau sera hors des marges, ou à cause d’une division par zéro11 . Ainsi, si maj = 0, on obtient : 3 4 10 24 faut pas diviser par zéro java.lang.ArithmeticException : / by zero alors que si maj = 2, on a : il y a un problème java.lang.ArrayIndexOutOfBoundsException Les blocs catch peuvent être suivis par un bloc finally qui donne des instructions qui seront exécutées après la sortie du bloc try, quelle que soit la façon dont on sortira de ce bloc. 4.1.4 La déclaration des exceptions Certaines exceptions12 doivent être traitées, sans quoi cela produit une erreur à la compilation. Il faut donc écrire un catch qui en définit le traitement. Mais il est également possible de signaler, dans l’en-tête d’une méthode, que de telles exceptions peuvent s’y produire, afin de renvoyer le traitement de l’exception au bloc qui appelle la méthode. Cela s’effectue à l’aide du mot clé throws. Par exemple : public void ouvreFichier( ) throws IOException 11 Remarquons l’ordre des clauses catch : l’exception la plus générale est placée après la plus spécifique, sinon celle-ci ne serait jamais atteinte. 12 Mais pas les IndexOutOfBoundsException. 26 4.1.5 La définition d’exceptions Le programmeur peut définir lui-même des exceptions, comme étant des sous-classes de la classe Exception. Par exemple : public class MonException extends Exception{ public MonException( ) {super( ) ;} public MonException(String s) {super(s) ;} } L’exception définie, pour l’instant, ne réfère pas à un événement particulier. Cela est effectuée ci-dessous à l’aide de l’instruction throw. On montre également comment peut être traitée l’exception définie. public class Essai{ public static void main(String[ ] args) {int j = 1 ; try{test(j) ; System.out.println("premier passage") ; j-- ; test(j) ; System.out.println("deuxième passage") ;} catch(MonException e) {System.out.println(e) ;} System.out.println("troisième passage") ; try{j-- ; test(j) ; System.out.println("quatrième pasage")} catch(MonException e) {System.out.println(e) ;}} public static void test(int i) throws MonException {if (i==0) throw new MonException("zéro") ; if (i<0) throw new MonException("valeur négative") ;} } En ce cas, on obtiendra comme sortie du programme : premier passage MonException : zéro troisième passage MonException : valeur négative 27 4.2 4.2.1 Les entrées et sorties Introduction La classe System contient les constantes in, qui représente le flot d’entrée standard et qui est de type InputStream, et out, qui représente le flot de sortie standard et qui est de type PrintStream. Si on ne se limite pas aux instructions d’entrée et sortie tout à fait élémentaires, il faut importer le paquet java.io. 4.2.2 Les sorties La classe PrintStream contient les méthodes print(x) et println(x) où x est nécessairement de type String. La conversion des types s’effectue automatiquement pour les types primitifs. Elle fait appel à la méthode toString pour les objets, à condition que cette méthode ait été définie pour le type d’objet en question. 4.2.3 Les entrées La méthode readLine( ) est de type String. Elle s’applique à la classe BufferedReader. Elle nécessite un traitement de l’exception IOException. On lit une ligne unique, qui est placée dans une variable de type String. L’objet de type BufferedReader sera construit à partir d’un objet de type Reader, plus précisément de type InputStreamReader. Lequel sera créé à partir de l’objet System.in (de type InputStream). Cela donne : try{ BufferedReader bf = new BufferedReader(new InputStreamReader(System.in)) ; String s = bf.readLine( ) ;} catch(Exception e) {System.out.println(e) ;} Ensuite, on doit user des méthodes appropriées afin de convertir le String dans le type adéquat. Par exemple, si on veut obtenir un nombre entier, on passe par la méthode de classe Integer.parseInt, comme suit : int x = Integer.parseInt(s) ; Dans le cas où on lit plusieurs entités sur une même ligne, on utilisera la classe d’objets StringTokenizer qui est dans le paquet java.util. String x ; StringTokenizer st = new StringTokenizer(s) ; n = st.countTokens( ) ; 28 for (int i = 0 ; i < n ; i++) {x = st.nextToken( ) ; htraitement sur xi} 4.2.4 Les fichiers séquentiels On construit un objet de type BufferedReader à partir d’un objet de type FileReader, qui lui-même sera construit à partir du nom du fichier. String s try{ FileReader f = new FileReader("fichier1") ; BufferedReader br new BufferedReader(f) ; while ((s=br.readLine( )) != null) system.out.println(s) ;} catch(Exception e){System.out.println(e) ;} Cette fraction de programme lit les unes à la suite des autres toutes les lignes d’un fichier, et les affiche à l’écran. Inversement, le programme ci-dessous va écrire sur un fichier des caractères donnés sous la forme d’un tableau de codes ASCII. import java.io.* ; import java.util.* ; public class Essai {public static void main(String[ ] args) {byte b[ ] = {45,52,63,74,85} ; try{FileOutputStream g = new FileOutputStream("baba") ; g.write(b) ; byte c[ ] = {10,13,90,91,92,93} ; g.write(c) ;} catch(Exception e){System.out.println(e) ;} } } 29