Contents 1 L’orienté objet et l’héritage 1.1 Introduction . . . . . . . . . . . . . . . . . . . . 1.2 Convention de nommage des classes et variables 1.3 Une première classe . . . . . . . . . . . . . . . 1.4 Les paquets . . . . . . . . . . . . . . . . . . . . 1.5 Interaction avec l’utilisateur . . . . . . . . . . . 1.6 Java et pointeurs . . . . . . . . . . . . . . . . . 1.7 Constructeurs : initialisation d’objets de classe 1.8 Setters, getters et fonction toString() . . . . . 1.9 Gestion des tableaux . . . . . . . . . . . . . . . 1.10 Héritage . . . . . . . . . . . . . . . . . . . . . . 1.11 Portée des attributs et méthodes de classe . . . 1.12 Mot-clé final . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 3 4 4 5 5 6 8 10 10 12 12 2 Polymorphisme : superclasses abstraites et 2.1 Introduction au polymorphisme . . . . . . . 2.2 Supeclasses abstraites . . . . . . . . . . . . 2.3 Interface . . . . . . . . . . . . . . . . . . . . 2.4 Superclasses abstraites VS interfaces . . . . interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 13 13 15 16 3 Traitement des exceptions 3.1 Introduction . . . . . . . . . . . . 3.2 Capturer une erreur . . . . . . . 3.3 Les clause throws et throw . . . 3.4 La fonction printStackTrace() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 17 18 19 20 4 Fichiers et flots de données 4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Fichiers et flux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3 Écriture et lecture à partir d’un fichier à accès séquentiel . . . . . . . . . . . . . . 21 21 21 22 . . . . . . . . . . . . . . . . -1- . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . -2- Chapter 1 L’orienté objet et l’héritage 1.1 Introduction Avec le langage de programmation Java, l’orienté objet est un concept primordial étant donné que tout y est objet. En C/C++, l’emploi des objets n’est pas obligatoire. L’utilisateur déclare une fonction main() qui est appelée au lancement du programme et qui contient l’ensemble des instructions du programme. En Java, cette fonction main() est incluse en tant que fonction static (opération de classe) au sein d’une classe. Il n’est donc pas nécessaire de créer une instance d’objet pour appeler cette fonction, mais il n’est pas possible de déclarer des fonctions ou des variables en dehors des classes. Ce chapitre rappellera donc les notions relatives à l’orienté objet. 1.2 Convention de nommage des classes et variables Avant d’aller plus loin dans la présentation des concepts relatifs à l’orienté objet, il faut présenter les conventions de nommage des classes et des variables au sein de Java. Afin d’améliorer la lisibilité et la compréhensibilité du code, la convention de nommage CamelCase a été adoptée pour nommer les classes et variables. Elle n’est pas obligatoire, mais fortement suggérée et communément adoptée par la communauté des développeurs. Elle est dès lors imposée au cours des séances de laboratoire. Cette convention amène les règles suivantes : Le nom d’une classe commence par une majuscule. Le nom d’une variable ou d’une fonction commence par une minuscule. Lorsque que le nom d’une classe, d’une variable ou d’une fonction est composé de plusieurs mots, l’initial de chaque mot est mise en majuscule (à l’exception du premier mot s’il s’agit du nom d’une variable, afin de respecter la règle précédente). L’emploi du caractère ” ” pour séparer les mots et donc contre-indiqué. Exemples : public class EtudiantMaster int anneeNaissance float calculerMoyenne() -3- 1.3 Une première classe Le listing 1.1 présente une première classe assez simple qui permet d’afficher le texte ”Hello World” au terminal. En Java, chaque classe est impérativement programmée dans un fichier distinct qui porte le nom de la classe et dont l’extension est ”.java”. Il y a ainsi un fichier nommé HelloWorld.java qui contient le code de la classe HelloWord. Listing 1.1: La classe HelloWorld 1 public class HelloWorld { 2 /* * * @param args the command line arguments */ public static void main ( String [] args ) { System . out . println ( " Hello World " ); } 3 4 5 6 7 8 9 10 } La fonction main() est nécessairement déclarée comme étant publique (public) et statique (static) afin de pouvoir être appelée sans devoir instancier la classe. Les méthodes et les variables statiques sont appelées méthodes et variables de classe. Les variables de classes sont partagées entre les instances de la classe. Si une instance modifie la valeur d’une variable de classe, cette modification sera d’application pour toutes les instances de la classe considérée. les méthodes de classe spécifient que l’action réalisée n’est pas liée à une instance particulière de cette classe. Il n’est donc pas nécessaire d’instancier la classe, de créer un objet, pour utiliser la méthode. Pour appeler une telle méthode, il suffit de faire directement référence au nom de la classe. Par exemple, pour appeler la fonction main() de la classe HelloWorld : HelloWorld.main(null); Comme la fonction main() de la classe HelloWorld ne renvoie rien, le mot-clé void est placé avant le nom de la fonction. Pour afficher du texte dans la console, HelloWorld utilise les outils fournit par la classe System. La classe System met à disposition des objets de flux qui fournissent des canaux de communication entre un programme et un périphérique bien déterminé. En l’occurrence, System.out permet d’afficher des données dans la console. Il fournit plusieurs méthodes à cet escient. La méthode utilisée, println(), est un raccourci pour print line. Cette méthode affiche dans la console la chaı̂ne de caractère passée en paramètre et insère un retour à la ligne. Il est possible d’afficher une chaı̂ne de caractères sans retour à la ligne avec la méthode print(). 1.4 Les paquets En Java, les classes sont réunies dans des paquets qu’il faut importer pour pouvoir les employer, à l’instar des fichiers d’en-tête en C++ qui fournissent les définitions de fonctions et de classes. Dans l’exemple du listing 1.1, la classe System est issue du package java.lang. Celui-ci n’est pas importé par une instruction spécifique au début du programme, car il est automatiquement importé dans tous les programmes Java. Le paquet java.lang est le seul paquet à être importé automatiquement. Tout autre paquet doit être importé avec le mot-clé import. Par exemple, l’emploi de la classe JOptionPane, qui sera présenté plus loin, nécessite l’importation du paquet javax.swing. Il y a deux manières d’accéder à une classe placée dans un paquet. Soit en important la classe elle-même avec une ligne de code comme : import javax.swing.JOptionPane; -4- soit en important l’entièreté du paquet qui la contient à l’aide du signe ’*’ : import javax.swing.*; 1.5 Interaction avec l’utilisateur Nous avons déjà vu comment afficher une chaı̂ne de caractères dans la console à l’aide de la classe System. Pour saisir des informations au clavier, c’est la classe JOptionPane qui est employée. Cette classe fait partie du package javax.swing et fournit un ensemble de méthodes qui permettent à l’utilisateur d’interagir via des boı̂tes de dialogue. Pour saisir du texte via une boı̂te de dialogue, il faut utiliser la fonction showInputDialog. Cette fonction renvoie un pointeur vers la chaı̂ne de caractères saisie par l’utilisateur. String nom = JOptionPane.showInputDialog("Veuillez entrer votre nom"); Pour afficher du texte au sein d’une boı̂te de dialogue, il suffit d’utiliser la fonction showMessageDialog() : JOptionPane.showMessageDialog(null, "Votre nom est " + nom); Pour manipuler autre chose que des chaı̂nes de caractères, il est nécessaire d’utiliser les classes des types primitifs. Java fournit des classes pour chaque type primitif (int, float ...) qui encapsulent une instance du type primitif considéré et qui offrent un ensemble de méthodes pour manipuler les données. Ainsi, si un utilisateur souhaite saisir un entier, il peut utiliser la fonction Integer.parseInt() qui prend en argument une chaı̂ne de caractères et qui renvoie un entier : int age = Integer.parseInt(JOptionPane.showInputDialog("Veuillez entrer votre age")); ^ Le même genre de méthode peut être trouvé pour les autres types primitifs. 1.6 Java et pointeurs Pour rappel, un pointeur est une variable qui contient l’adresse à laquelle se trouve une donnée. Dans Java, tous les objets sont accédés par l’intermédiaire de pointeurs. À chaque fois qu’une déclaration d’objet est faite, il s’agit en fait de la déclaration d’un pointeur. Par exemple, les chaı̂nes de caractères sont gérées en Java par la classe String. Une chaı̂ne de caractères est donc déclarée comme toute variable primitive : String monNom; A ce stade cependant, il n’est pas encore possible de manipuler de chaı̂nes de caractères. La variable monNom est un pointeur et ne permet que de stocker l’adresse de la chaı̂ne de caractères. Il est donc nécessaire d’allouer de la place pour celle-ci et de stocker l’adresse de cette espace mémoire dans la variable monNom. Cette opération se fait à l’aide de l’instruction new : monNom = new String(); Les fonctions ne reçoivent pas de copies locales des objets, mais directement les adresses des objets passés en paramètres. Toute modification apportée sur un objet au sein d’une fonction est donc aussi effective en dehors de cette fonction. -5- 1.7 Constructeurs : initialisation d’objets de classe Le constructeur est une fonction qui est automatiquement appelée lors de l’instanciation d’un objet. Il porte le même nom que la classe et ne spécifie pas de type de retour. Il est possible de tout y mettre, mais son but premier est d’initialiser les membres d’un objet pour éviter des erreurs lors de la manipulation de l’objet. Par exemple, les classes présentées aux listings 1.2 et 1.3 présentent la différence entre un constructeur mal implémenté et un constructeur correct. Ces classes sont testées par la classe TestProduit présentée au listing 1.4. Dans le cas du constructeur incomplet, le pointeur contenant le nom du produit n’est jamais initialisé. Il vaut donc null et le code de la classe TestProduit va entraı̂ner une erreur de type NullPointerException. Si le constructeur est bien implémenté, comme c’est le cas au listing 1.3, il initialise le pointeur afin d’éviter ce genre d’erreur. Cette initialisation est obligatoire uniquement si son absence peut entraı̂ner une erreur lors de la manipulation d’instances de la classe. Par exemple, la mise à 0 de la variable quantite n’est pas ici obligatoire, il s’agit surtout d’une bonne pratique de programmation. Par contre, si une fonction ajouter() permet d’augmenter la quantité d’un produit et que la variable quantite n’est pas initialisée, le résultat de cette opération sera indéterminé. Listing 1.2: La classe Produit avec un constructeur mal implémenté 1 public class Produit { 2 String nom ; 3 int quantite ; 4 5 public Produit () { 6 } 7 public String getNom () { return nom ; 8 9 } 10 11 14 public void setNom ( String nom ) { if ( nom != null ) this . nom = nom ; 15 } 12 13 16 18 public int getQuantite () { return quantite ; 19 } 17 20 public void setQuantite ( int quantite ) { if ( quantite > -1) this . quantite = quantite ; 21 22 23 } 24 25 } Listing 1.3: La classe Produit avec un constructeur correctement implémenté 1 public class Produit { 2 String nom ; 3 int quantite ; 4 5 6 public Produit () { nom = new String (); quantite = 0; 7 8 } 9 10 public String getNom () { -6- return nom ; 11 } 12 13 16 public void setNom ( String nom ) { if ( nom != null ) this . nom = nom ; 17 } 14 15 18 20 public int getQuantite () { return quantite ; 21 } 19 22 public void setQuantite ( int quantite ) { if ( quantite > -1) this . quantite = quantite ; 23 24 25 } 26 27 } Listing 1.4: Application testant la classe Produit 1 public class TestProduit { 2 /* * * @param args the command line arguments */ public static void main ( String [] args ) { Produit unProduit = new Produit (); System . out . println ( " Hello " + unProduit . getNom (). toUpperCase ()); } 3 4 5 6 7 8 9 10 11 } 12 13 14 15 16 run : Exception in thread " main " java . lang . NullPointerException at javaapplication3 . TestProduit . main ( TestProduit . java :15) Java Result : 1 Une classe peut avoir plusieurs constructeurs s’ils sont surchargés. Surcharger un constructeur revient à créer un nouveau constructeur qui comprend un ensemble de paramètres dont le nombre ou le type est différent des paramètres des constructeurs déjà existant. Par exemple, le listing 1.5 présente une version de la classe Produit qui fournit un constructeur permettant de donner le nom et la quantité du produit à la création de l’objet. L’emploi de ce constructeur est présenté au listing 1.6 Listing 1.5: Exemple de constructeur surchargé avec la classe Produit 1 public class Produit { 2 String nom ; 3 int quantite ; 4 5 6 public Produit () { nom = new String (); quantite = 0; 7 8 } 9 12 public Produit ( String nom , int quantité ) { this . nom = nom ; this . quantite = quantite ; 13 } 10 11 -7- 14 public String getNom () { return nom ; 15 16 } 17 18 21 public void setNom ( String nom ) { if ( nom != null ) this . nom = nom ; 22 } 19 20 23 25 public int getQuantite () { return quantite ; 26 } 24 27 public void setQuantite ( int quantite ) { if ( quantite > -1) this . quantite = quantite ; 28 29 30 } 31 32 } Listing 1.6: Application utilisant le constructeur surchargé de la classe Produit 1 public class TestProduit { 2 /* * * @param args the command line arguments */ public static void main ( String [] args ) { Produit unProduit = new Produit ( " Paquet de pommes " , 25); System . out . println ( unProduit . getNom () + " est présent en " + unProduit . getQuantite () + " exemplaires " ); } 3 4 5 6 7 8 9 10 11 12 } 13 14 15 run : Paquet de pommes est présent en 25 exemplaires 1.8 Setters, getters et fonction toString() Le programmeur est libre de mettre les fonctions qu’il désire dans les classes qu’il crée, mais il existe un ensemble de fonctions récurrentes qui facilitent et systématisent la manipulation des classes. Dans le contexte de la programmation orientée objet, il est recommandé de rendre privés les attributs de classes, comme c’est le cas pour le nom et l’âge de la classe Produit présentée au listing 1.5. Cette pratique force l’emploi de méthodes de classe sécurisées pour accéder à ces paramètres. Ces méthodes de classe peuvent vérifier que la manipulation des attributs ne violent pas certaines règles (exploitation d’un pointeur null, attribution d’un entier négatif à une variable représentant l’âge d’une personne ...). Pour accéder aux attributs de classes, on utilise des fonctions nommées accesseurs. Il s’agit de fonctions similaires à toutes autres fonctions, mais qui commencent par get ou set et qui permettent d’accéder aux attributs de classes respectivement en lecture et en écriture. Les setters se nomment set + ’nom de l’attribut’. Les getters se nomment get + ’nom de l’attribut’. Un exemple de telles fonctions se trouve au listing 1.7. -8- Listing 1.7: Classe Produit completée avec les setters, les getters et la fonction toString() 1 public class Produit { 2 String nom ; 3 int quantite ; 4 5 public Produit () { 6 } 7 10 public Produit ( String nom , int quantite ) { this . nom = nom ; this . quantite = quantite ; 11 } 8 9 12 14 public String getNom () { return nom ; 15 } 13 16 19 public void setNom ( String nom ) { if ( nom != null ) this . nom = nom ; 20 } 17 18 21 23 public int getQuantite () { return quantite ; 24 } 22 25 28 public void setQuantite ( int quantite ) { if ( quantite > -1) this . quantite = quantite ; 29 } 26 27 30 31 @Override 32 public String toString () { return " Produit { " + " nom = " + nom + " , quantite = " + quantite + ’} ’; 33 } 34 35 } Une autre fonction intéressante présentée dans le listing 1.7 est la fonction toString(). Cette fonction est appelée automatiquement à chaque fois qu’un objet est passé à une fonction là où celle-ci attend une chaı̂ne de caractère. Ainsi, si vous passez un objet à la fonction println() : System.out.println(aProduct);, la fonction va afficher la chaı̂ne de caractères renvoyée par la fonction toString(). Un exemple est présenté au listing 1.8. Listing 1.8: Application utilisant la fonction toString() de la classe Personne 1 2 3 public class TestProduit { public static void main ( String [] args ){ Produit unProduit = new Produit ( " Paquet de pommes " , 25); System . out . println ( unProduit ); 4 } 5 6 } 7 8 9 run : Produit { nom = Paquet de pommes , quantite =25} -9- 1.9 Gestion des tableaux Pour créer un tableau en Java, il faut d’abord déclarer un pointeur puis définir la taille du tableau qui lui est assigné : int unTableau[] = new int[10]; Le parcours du tableau peut se faire de deux manières : 1. En utilisant une variable identifiant l’indice de l’élément du tableau qui doit être accédé. 2. En utilisant un pointeur qui pointe successivement les différents éléments du tableau. Ces deux méthodes sont illustrées au listing 1.9. Il n’est pas nécessaire de stocker la taille du tableau dans une variable séparée, car la taille est un attribut du tableau. Listing 1.9: Emploi des tableaux en Java 1 2 3 4 public class TestTableau { public static void main ( String [] args ){ int unTableau [] = new int [3]; for ( int i =0; i < unTableau . length ; ++ i ){ unTableau [ i ]= i ; 5 6 } 7 for ( int element : unTableau ){ System . out . println ( element ); 8 } 9 } 10 11 } 12 13 14 15 16 run : 1 2 3 Pour les objets, l’exploitation d’un tableau est légèrement différente. Comme les objets sont gérés à l’aide de pointeurs, l’allocation de l’espace nécessaire pour un tableau d’objets se déroule en deux étapes. Premièrement, il faut allouer de la place pour accueillir les adresses des objets : Produit unTableau[] = new Produit[10]; Ensuite, il est faut allouer de la place pour chaque élément du tableau : unTableau[i] = new Produit(); Une fois ces deux étapes accomplies, la manipulation d’un tableau d’objet est similaire à l’exploitation d’un tableau d’éléments de type de bases. 1.10 Héritage Lorsqu’une classe doit réutiliser la plupart des fonctionnalités d’une autre classe, mais en modifier certaines et/ou en ajouter d’autres, elle peut utiliser la notion d’héritage. L’héritage permet à la sous-classe (la classe qui hérite) d’avoir accès à toutes les méthodes de la classe parente (superclasse) qui sont déclarées comme étant public ou protected. En Java, l’héritage multiple est interdit. Une classe ne peut hériter que d’une seule autre classe. Le mot-clé extends est utilisé à la déclaration de la sous-classe pour indiquer qu’elle hérite d’une super-classe : - 10 - public class Livre extends Produit La classe Livre hérite alors de tous les attributs et fonctions déclarés comme étant public ou protected de la classe Produit. La sous-classe peut accéder aux méthodes de la super-classe à l’aide du mot-clé super : super.toString(); Au sein du constructeur de la sous-classe, ce mot-clé sert aussi à appeler les constructeurs de la super-classe : super(); Les constructeurs de la sous-classe doivent toujours pouvoir appeler un constructeur de la superclasse. Le constructeur de la superclasse ne prenant aucun argument est implicitement employé si aucun constructeur n’est explicitement appelé dans le constructeur de la sous-classe. Si un autre constructeur doit être employé, son appel spécifique doit être la première instruction du constructeur de la sous-classe. Si la sous-classe déclare une fonction avec le même en-tête que la superclasse, elle redéfinit la fonction. C’est alors la fonction de la sous-classe qui sera appelée et non celle de la superclasse. Un exemple de sous-classe est présenté au listing 1.10. Un exemple d’utilisation de cette sousclasse est présenté au listing 1.11. Listing 1.10: Classe Livre, sous-classe héritant de la classe Produit. 1 public class Livre extends Produit { 2 String auteur ; 3 int nombrePage ; 4 5 6 public Livre () { super (); nombrePage = 0; auteur = new String (); 7 8 9 } 10 14 public Livre ( String nom , int quantite , String auteur , int nombrePage ) { super ( nom , quantite ); this . nombrePage = nombrePage ; this . auteur = auteur ; 15 } 11 12 13 16 18 public int getNombrePage () { return nombrePage ; 19 } 17 20 22 public void setNombrePage ( int nombrePage ) { this . nombrePage = nombrePage ; 23 } 21 24 26 public String getAuteur () { return auteur ; 27 } 25 28 30 public void setAuteur ( String auteur ) { this . auteur = auteur ; 31 } 29 32 33 @Override 34 public String toString () { return super . toString () + " \ nLivre { " + " auteur = " + auteur + " , 35 - 11 - Portée public protected pas de portée explicite private Classe Oui Oui Oui Oui Paquet Oui Oui Oui Non Sous-classe Oui Oui Non Non Monde Oui Non Non Non Table 1.1: Accessibilité des attributs et fonctions de classe en fonction de leur portée. nombrePage = " + nombrePage + ’} ’; 36 } 37 38 } Listing 1.11: Application utilisant la fonction toString() de la classe Personne 1 2 3 4 5 public class TestProduit { public static void main ( String [] args ){ Produit desProduits [] = new Produit [2]; desProduits [0] = new Produit ( " Paquet de pommes " , 25); desProduits [1] = new Livre ( " Comment programmer en Java " , 10 , " Deitel & Deitel " , 1546); 6 for ( Produit unProduit : desProduits ) 7 System . out . println ( unProduit ); 8 } 9 10 } 11 12 13 14 15 run : Produit { nom = Paquet de pommes , quantite =25} Produit { nom = Comment programmer en Java , quantite =10} Livre { auteur = Deitel & Deitel , nombrePage =1546} 1.11 Portée des attributs et méthodes de classe Les attributs et les méthodes de classe peuvent avoir différentes portées déterminées par les mots-clés public, protected et private. Leur portée respective est rappelée au Tableau 1.1. 1.12 Mot-clé final Le mot-clé final est utilisé pour identifier un attribut, une méthode ou une classe qui ne pourra plus être modifié après sa déclaration. Une classe finale (public final class) ne pourra jamais devenir une superclasse. Aucune classe ne pourra en hériter et redéfinir ses fonctions. Une fonction finale (portée final typeRetour) ne pourra jamais être redéfinie. Une sousclasse héritant de la classe contenant la fonction finale devra l’employer telle quelle sans possibilité de modification. La valeur d’un attribut final (portée final typeAttribut) ne peut pas être modifiée. Elle est attribuée à la déclaration de l’attribut et ne changera jamais. - 12 - Chapter 2 Polymorphisme : superclasses abstraites et interfaces 2.1 Introduction au polymorphisme Le polymorphisme, selon Wikipedia, est le concept consistant à fournir une interface unique à des entités pouvant avoir différents types. Le polymorphisme qui sera étudié ici est le polymorphisme par sous-typage : des classes différentes héritent de la même interface, des mêmes noms de fonctionnalités, mais ces fonctionnalités peuvent être implémentées de manières différentes par chacune de ces classes. Les superclasses abstraites et les interfaces définissent un ensemble de fonctions qui ne sont pas nécessairement implémentées. Il s’agit d’un contrat passé avec les classes qui héritent des superclasses abstraites ou qui implémentent les interfaces. Ces contrats garantissent que les sousclasses fourniront un ensemble d’opérations identifiées. Par exemple, une interface Vendable pourrait imposer aux classes qui l’implémentent de fournir une fonction getPrix() qui renvoie le prix de l’objet. Ceci permet de traiter de manière homogène des objets instanciant des classes différentes. Ceci facilite l’extension des systèmes informatiques. Le système manipule des objets Vendable sans se soucier de savoir s’il s’agit d’instance de la classe Livre ou de la classe JeuxVideo. S’il faut ajouter une classe CompactDisc au système, il suffit que cette nouvelle classe implémente l’interface Vendable pour qu’elle soit directement manipulable par le système sans modification supplémentaire. Ce chapitre présente deux manières de gérer le polymorphisme par sous-typage : les superclasses abstraites et les interfaces. 2.2 Supeclasses abstraites Une superclasse abstraite est une classe qui ne peut pas être implémentée, à la différence des classes concrètes qui peuvent instancier des objets. Une superclasse abstraite est identifié grâce à l’emploi du mot-clé abstract dans sa définition. Cette classe contient au moins une opération dont l’implémentation est différée et qui est aussi identifiée par le mot clé abstract. Ces fonctions ne possèdent pas d’implémentations, ce sont les sous-classes de la classe abstraite qui doivent fournir leurs implémentations. Un exemple est présenté au listing 2.1 Listing 2.1: Exemple de l’emploi des superclasses abstraites. L’implémentation de la fonction manger est différée jusqu’à la connaissance du régime alimentaire des animaux. 1 2 public abstract class Animal { String nom ; - 13 - int poids ; 3 4 5 public Animal () { 6 } 7 10 public Animal ( String nom , int poids ) { this . poids = poids ; this . nom = nom ; 11 } 8 9 12 14 public int getPoids () { return poids ; 15 } 13 16 18 public void setPoids ( int poids ) { this . poids = poids ; 19 } 17 20 22 public String getCouleur () { return nom ; 23 } 21 24 26 public void setCouleur ( String nom ) { this . nom = nom ; 27 } 25 28 public abstract void manger (); 29 30 31 @Override 32 public String toString () { return " Animal { " + " nom = " + nom + " , poids = " + poids + ’} ’; 33 } 34 35 } 36 37 public class Carnivore extends Animal { 38 40 public Carnivore ( String nom , int poids ) { super ( nom , poids ); 41 } 39 42 43 @Override 44 public void manger () { System . out . println ( " Je mange de la viande " ); 45 } 46 47 48 } 49 50 public class Vegetarien extends Animal { 51 53 public Vegetarien ( String nom , int poids ) { super ( nom , poids ); 54 } 52 55 56 @Override 57 public void manger () { System . out . println ( " Je mange des végétaux . " ); 58 } 59 60 61 } 62 63 64 65 public class TestAnimal { public static void main ( String [] args ){ Animal zoo [] = new Animal [2]; - 14 - zoo [0] = new Carnivore ( " Lion " , 190); zoo [1] = new Vegetarien ( " Lapin " , 2); for ( int i =0; i < zoo . length ; ++ i ){ System . out . println ( zoo [ i ]); zoo [ i ]. manger (); } 66 67 68 69 70 71 } 72 73 } 74 75 76 77 78 79 run : Animal { nom = Lion , poids =190} Je mange de la viande Animal { nom = Lapin , poids =2} Je mange des végétaux . Dans l’exemple, on comprend l’utilité de déclarer la fonction manger comme étant abstraite. Le régime alimentaire est spécifique aux animaux et ne pourrait être implémenté de manière générale. Dès lors, il faut utiliser le mot-clé abstract pour spécifier que la fonction ne peut être implémentée à ce niveau d’abstraction, mais qu’elle devra l’être lorsque les classes héritant de cette superclasse abstraite deviennent concrètes. 2.3 Interface Une interface est une structure qui ne comprend que des fonctions publiques et abstraites (public abstract) et des données publiques, statiques et finales (public static final). Le mot-clé final indique que la valeur de la donnée ne peut être changée. La donnée doit donc être initialisée lors de sa déclaration. La déclaration d’une interface est similaire à celle d’une classe. Le mot-clé class est simplement remplacé par le mot-clé interface. Pour utiliser une interface, une classe doit spécifier qu’elle l’implémente avec le mot-clé implements et elle doit implémenter toutes les méthodes de l’interface (ou être déclarée comme étant une classe abstraite). L’interface est un ”contrat” à remplir par les classes l’implémentant. Elle garantit ainsi la présence de tout un ensemble de méthodes au sein de ces classes. À nouveau, cela permet de manipuler un ensemble de classes différentes via une interface commune. Par convention, les interfaces sont nommées avec le suffixe ”-able”. Une interface Vendable fournira toutes les méthodes jugées nécessaires pour qu’un objet soit ”vendable” comme une méthode retournant le prix du produit, la quantité vendue... Un exemple d’interface est présenté au listing 2.2. Listing 2.2: Exemple de l’emploi des interfaces. Toute classe ”Vehiculable” doit fournir une méthode qui permet d’accélérer, et une méthode qui permet de freiner. 3 public interface Vehiculable { public abstract void accelerer (); public abstract void freiner (); 4 } 1 2 5 6 public class Velo implements Vehiculable { 7 8 @Override 9 public void accelerer () { System . out . println ( " Je pédale plus vite !! " ); 10 11 } 12 13 @Override 14 public void freiner () { - 15 - System . out . println ( " Je serre les freins !! " ); 15 } 16 17 18 } 19 20 public class Voiture implements Vehiculable { 21 22 @Override 23 public void accelerer () { System . out . println ( " J ’ appuie sur le champignon !! " ); 24 } 25 26 27 @Override 28 public void freiner () { System . out . println ( " J ’ appuie sur la pédale de frein !! " ); 29 } 30 31 32 } 33 34 35 36 37 38 39 public class TestVehiculable { public static void main ( String [] args ) { Vehiculable desVehicules [] = new Vehiculable [2]; desVehicules [0] = new Velo (); desVehicules [1] = new Voiture (); for ( Vehiculable unVehicule : desVehicules ){ unVehicule . accelerer (); unVehicule . freiner (); 40 41 } 42 } 43 44 } 45 46 47 48 49 50 run : Je pédale plus vite !! Je serre les freins !! J ’ appuie sur le champignon !! J ’ appuie sur la pédale de frein !! 2.4 Superclasses abstraites VS interfaces Au vu du polymorphisme, les superclasses abstraites et les interfaces permettent toutes deux de manipuler un ensemble de classes différentes via une interface commune. Cependant, elles ont chacune leurs avantages et inconvénients. Voici un comparatif non exhaustif des deux solutions : Les interfaces peuvent être utilisées pour remplacer l’héritage multiple. Elles ne permettent pas l’héritage de fonctions implémentées, mais elles permettent à une classe d’”hériter” de diverses interfaces. Les superclasses abstraites permettent l’héritage de fonctions implémentées mais empêchent l’héritage d’autres classes. Les superclasses abstraites permettent la déclaration de membres non publiques, ce qui n’est pas possible avec les interfaces. Si de nouvelles méthodes doivent être ajoutées après le développement d’un projet, il est possible de les ajouter dans les superclasses abstraites sans pour autant devoir modifier les sous-classes en héritant (tant que ces méthodes ne sont pas abstraites). Dans le cas des interfaces, l’ajout de nouvelles méthodes entraı̂ne d’office la nécessité d’implémenter ces méthodes dans chaque classe implémentant ces interfaces. - 16 - Chapter 3 Traitement des exceptions 3.1 Introduction Il arrive que des erreurs arrivent au cours de l’exécution des programmes lorsque la sécurité de ceux-ci est trop faible, lorsqu’ils ont été mal conçus ou lorsque l’utilisateur utilise mal le programme. Ainsi, une fonction division qui ne contrôle pas la valeur du dénominateur pourrait entraı̂ner une erreur si celui-ci vaut zéro. Aussi, une exception sera levée si un utilisateur saisi du texte dans une zone de texte qui est vouée à être convertie en chiffres. Par défaut, ces exceptions vont interrompre le programme en imprimant la stack trace, ou pile d’appels en français. La pile d’appels contient la succession d’appels de fonctions qui a mené à la levée de l’exception. Ces appels identifient les classes où se situent ces fonctions, les numéros de lignes où les appels ont lieu et le numéro de ligne de l’instruction fautive. Un exemple d’exception levée lors du dépassement des limites d’un tableau est présentée au listing 3.1. Plutôt que de laisser l’exception arrêter brutalement l’exécution du programme, il est possible de l’attraper et d’adapter l’exécution du programme à la levée de l’exception. Par exemple, dans le cas de la saisie d’un texte au lieu de chiffres, il est possible de demander à l’utilisateur de corriger sa saisie. Ce chapitre présente les méthodes qui permettent de programmer de tels comportements. Listing 3.1: Exemple d’erreur dûe au dépassement des bornes d’un tableau. 1 public class Panier { 2 Produit [] lePanier ; 3 int indice ; 4 public Panier () { lePanier = new Produit [3]; 5 6 indice = 0; 7 } 8 9 public void addProduit ( Produit newProduit ){ 10 lePanier [ indice ++]= newProduit ; 11 } 12 13 } 14 15 16 17 18 19 20 21 public class TestExceptions { public static void main ( String [] args ) { Panier monPanier = new Panier (); monPanier . addProduit ( new Produit ( " One Piece vol . 80 " , 1)); monPanier . addProduit ( new Produit ( " The Dark Knight " , 1)); monPanier . addProduit ( new Produit ( " Game of Thrones Season 6 " , 1)); monPanier . addProduit ( new Produit ( " The Hypnoflip Invasion " , 1)); } 22 23 } - 17 - 24 25 26 27 28 29 run : Exception in thread " main " java . lang . A r r a y I n d e x O u t O f B o u n d s E x c e p t i o n : 3 at javaapplication3 . Panier . addProduit ( Panier . java :22) at javaapplication3 . TestExceptions . main ( TestExceptions . java :18) Java Result : 1 3.2 Capturer une erreur Pour capturer une erreur, il faut place les instructions susceptibles de causer l’erreur à l’intérieur d’un bloc try. Ce bloc try sera directement suivi d’un bloc catch qui identifie les erreurs qui doivent être attrapées et gérées par le programme. Il peut y avoir plusieurs bloc catch pour capturer plusieurs erreurs différentes. Les bloc catch peuvent aussi avoir un ordre hiérarchique du plus spécifique au plus général (ArrayIndexOutOfBoundsException → IndexOutOfBoundsException → RuntimeException → Exception). Lorsqu’une erreur identifiée par le bloc catch est déclenché dans le bloc try, le contrôle de programme quitte le bloc try et poursuit au premier bloc catch correspondant à l’erreur. Un exemple est présenté au listing 3.2. L’erreur ArrayIndexOutOfBoundsException y est explicitement capturée et un message invite le programmeur à créer un tableau de plus grande taille. Il est aussi possible de placer un bloc finally qui sera exécuté en toutes circonstances, qu’il y ait eu erreur ou pas, à la fin de l’exécution du bloc try ou du bloc catch. Le listing 3.2 indique comment la classe Panier peut utiliser la gestion d’erreur pour éviter de faire crashe le programme en cas d’erreur dans la fonction addProduit(). Listing 3.2: Exemple de traitement d’erreur à l’aide du bloc try/catch 1 public class Panier { 2 Produit [] lePanier ; 3 int indice ; 4 public Panier () { lePanier = new Produit [3]; 5 6 indice = 0; 7 } 8 9 public void addProduit ( Produit newProduit ){ try { 10 11 lePanier [ indice ++]= newProduit ; 12 13 } 14 catch ( A r r a y I n d e x O u t O f B o u n d s E x c e p t i o n e ){ System . out . println ( " Le tableau n ’ est pas assez grand , veuillez agrandir sa taille . " ); 15 16 17 } 18 catch ( Exception e ){ System . out . println ( " Une autre exception a été levée . " ); 19 20 } 21 finally { System . out . println ( " Et voilà , on sort de la fonction . " ); 22 } 23 } 24 25 } 26 27 28 29 30 31 public class TestExceptions { public static void main ( String [] args ) { Panier monPanier = new Panier (); monPanier . addProduit ( new Produit ( " One Piece vol . 80 " , 1)); monPanier . addProduit ( new Produit ( " The Dark Knight " , 1)); - 18 - monPanier . addProduit ( new Produit ( " Game of Thrones Season 6 " , 1)); monPanier . addProduit ( new Produit ( " The Hypnoflip Invasion " , 1)); 32 33 } 34 35 } 36 37 38 39 40 41 42 run : Et voilà , on sort de Et voilà , on sort de Et voilà , on sort de Le tableau n ’ est pas Et voilà , on sort de 3.3 la fonction . la fonction . la fonction . assez grand , veuillez agrandir sa taille . la fonction . Les clause throws et throw La clause throws reprend une liste des exceptions qu’une méthode est susceptible de lancer. Cette liste peut être non exhaustive. Java effectue une distinction importante entre les Exception vérifiées, les RuntimeException non vérifiées et les Error. Les RuntimeException sont des exceptions qui peuvent être levée à n’importe quel moment lors de l’exécution du programme, comme ArrayIndexOutOfBoundsException. La tentative d’utilisation d’un pointeur null va également lever une RuntimeException de type NullPointerReference. Comme il serait fastidieux pour le programmeur de citer toutes ces exceptions dans la clause throws, il n’est pas obligatoire toutes les reprendre. C’est pourquoi elles sont qualifiées de non vérifiées. Il en est de même pour les Error (débordement de la pile d’appel, espace mémoire insuffisant ...). Les autres erreurs (IOException, IllegalAccessException ...) doivent soit être capturées au sein d’un bloc try/catch, soit être reprise dans la liste de la clause throws. Ces exceptions sont ainsi vérifiées par le compilateur. La clause throws se place après le nom de la méthode : int nomMéthode(listeParamètres) throws TypeException1, TypeException2, TypeException3, ... Le listing 3.3 reprend l’exemple de la classe Panier où est indiquée une RuntimeException dans la clause throws, ce qui n’est donc pas obligatoire. Listing 3.3: Exemple d’utilisation de la clause throws. 1 public class Panier { 2 Produit [] lePanier ; 3 int indice ; 4 public Panier () { lePanier = new Produit [3]; 5 6 indice = 0; 7 } 8 9 public void addProduit ( Produit newProduit ) throws A r r a y I n d e x O u t O f B o u n d s E x c e p t i o n { 10 11 lePanier [ indice ++]= newProduit ; 12 } 13 14 } La clause throw est utilisée par l’utilisateur pour lancer explicitement une exception : throw new Exception("Tu as fait une erreur ici !!"); - 19 - 3.4 La fonction printStackTrace() Les exceptions fournissent des méthodes indiquant des informations concernant l’origine de l’erreur. Ainsi la fonction printStackTrace() imprime la liste des appels de fonctions qui ont mené à l’erreur, ainsi que leur localisation dans le code source. Le listing 3.4 illustre l’emploi de cette fonction. Listing 3.4: Exemple d’utilisation de la fonction printStackTrace(). 1 public class Panier { 2 Produit [] lePanier ; 3 int indice ; 4 public Panier () { lePanier = new Produit [3]; 5 6 indice = 0; 7 } 8 9 public void addProduit ( Produit newProduit ){ try { 10 11 lePanier [ indice ++]= newProduit ; 12 13 } 14 catch ( A r r a y I n d e x O u t O f B o u n d s E x c e p t i o n e ){ e . printStackTrace (); 15 16 } 17 finally { System . out . println ( " L ’ exécution continue !! " ); 18 } 19 } 20 21 } 22 23 24 25 26 27 28 29 30 run : L ’ exécution continue !! L ’ exécution continue !! L ’ exécution continue !! java . lang . A r r a y I n d e x O u t O f B o u n d s E x c e p t i o n : 3 at javaapplication3 . Panier . addProduit ( Panier . java :23) at javaapplication3 . TestExceptions . main ( TestExceptions . java :19) L ’ exécution continue !! - 20 - Chapter 4 Fichiers et flots de données 4.1 Introduction Les données contenues dans les variables des programmes ne sont pas permanentes. Elles peuvent être écrasées une fois que le contrôle du programme sort du bloc où elles ont été déclarées ou que le programme se termine. Pour rendre les données permanentes, il faut les placer dans un fichier. Ce chapitre présente comment placer des données dans un fichier en Java. 4.2 Fichiers et flux En informatique, les données sont représentées par des bits qui peuvent valoir 0 ou 1. Pour pouvoir représenter plus que deux valeurs, les bits sont rassemblés en groupes de 8 bits appelés octets. Un octet peut représenter 256 valeurs différentes. Deux octets sont nécessaires pour représenter les caractères à l’aide de la norme Unicode. Pour les données plus complexes, comme les chiffres, les caractères sont regroupés pour former des champs. Un entier nécessite ainsi deux caractères, ou quatre octets. Lorsque plusieurs champs sont regroupés, on parle alors d’ enregistrements. Finalement, un fichier est destiné à accueillir un ensemble d’enregistrements. Pour Java, chaque fichier est un flux séquentiel d’octets. La fin d’un fichier peut être connue soit à l’aide d’une marque spécifique, soit à l’aide d’un nombre d’octets précis consigné dans la structure de données administrative maintenue par le fichier. En programmation, la fin d’un fichier est indiquée soit par la levée d’une exception, soit par une valeur particulière retournée par la fonction de lecture. Au démarrage d’un programme quelconque, Java crée à priori trois objets de flux qu’il associe aux périphériques au début de son exécution : System.in, System.out et System.err. L’objet System.in permet à un programme de récupérer les octets saisis au clavier, l’objet System.out permet de sortir les données à l’écran et l’objet System.err permet de sortir des messages d’erreurs à l’écran. Chaque flux peut être redirigé. Ainsi, il serait possible de rediriger System.err afin qu’il enregistre les messages d’erreur dans un fichier plutôt que de les afficher à l’écran. Ainsi, lorsqu’un programme plante, les informations relatives aux erreurs qui ont mené à l’arrêt brutal du programme sont conservées dans un fichier. Plusieurs classes sont employées pour rediriger les sorties appropriées vers des fichiers. Les classes abstraites InputStream et OutputStream définissent les méthodes qui prennent respectivement en charge des entrées et des sorties d’octets. Les classes FileInputStream et FileOuptuStream implémentent ces classes abstraites et permettent de faire des entrées/sorties d’octets à partir de fichiers. Pour convertir les classes en flux d’octets, ou inversement, il faut utiliser les classes ObjectInputStream et ObjectOutputStream. Ces classes redirigent les flux d’octets vers des InputStream ou des OutputStream. Il faut donc les chaı̂ner avec des instances des classes FileInputStream et FileOutputStream pour lire et écrire des classes dans - 21 - des fichiers. Pour être compatible avec les classes ObjectInputStream et ObjectOutputStream, une classe doit implémenter l’interface Serializable. Cette interface ne possède ni méthode, ni champs. Elle sert juste à baliser les classes comme étant Serializable. Toutes les variables d’instances de la classe doivent être de type Serializable. C’est le cas pour les types de base fournis par Java (float, int, String ...). Il s’agira surtout de vérifier que vos classes dont des instances sont contenues dans une classe Serializable soient elles aussi Serializable. 4.3 Écriture et lecture à partir d’un fichier à accès séquentiel La classe JFileChooser du package javax.swing fournit tout un ensemble de méthodes permettant d’accéder à des fichiers à accès séquentiel. La fonction showSaveDialog() permet de se déplacer dans l’arborescence de fichier de la machine et de créer un nouveau chemin de fichier. La fonction showOpenDialog() permet de récupérer le chemin d’un fichier existant. Ces deux fonctions retournent un entier indiquant le résultat de l’opération. Si cet entier vaut JFileChooser.CANCEL OPTION, cela indique que l’utilisateur a annulé la saisie du chemin et qu’il est inutile d’essayer d’exploiter le résultat de la fonction. Si tout s’est bien passé, la fonction getSelectedFile() de la classe JFileChooser permet de créer ou d’ouvrir le fichier identifié par le chemin saisi par l’utilisateur. Le retour de cette fonction est une instance de la classe File qui permet d’accéder au fichier. Ce fichier doit alors être utilisé comme argument du constructeur des chaı̂nes de classes FileInputStream → ObjectInputStream ou FileOutputStream → ObjectOutputStream pour ouvrir des flux qui permettront respectivement de lire ou d’écrire dans le fichier. Dans le cas de l’écriture, toutes les données contenues a priori dans le fichier seront écrasées. Pour ajouter du contenu dans un fichier existant, il faut d’abord l’ouvrir en lecture, récupérer son contenu, ouvrir le fichier en écriture, réécrire le contenu du fichier et ajouter les nouvelles données. L’écriture et la lecture sont effectués avec respectivement les fonctions writeObject et readObject des classes FileOutputStream et FileInputStream. Concernant la fonction readObject, elle renvoie un objet de type Object; il faut donc le transtyper suivant le type de l’objet lu. Un exemple d’écriture et de lecture d’un objet est présenté au listing 4.1. Listing 4.1: Exemple de lecture et d’écriture d’instances de classe dans un fichier à accès séquentiel. 1 public class Produit implements Serializable { 2 String nom ; 3 int quantite ; 4 5 public Produit () { 6 } 7 10 public Produit ( String nom , int quantite ) { this . nom = nom ; this . quantite = quantite ; 11 } 8 9 12 14 public String getNom () { return nom ; 15 } 13 16 19 public void setNom ( String nom ) { if ( nom != null ) this . nom = nom ; 20 } 17 18 21 - 22 - 23 public int getQuantite () { return quantite ; 24 } 22 25 28 public void setQuantite ( int quantite ) { if ( quantite > -1) this . quantite = quantite ; 29 } 26 27 30 31 @Override 32 public String toString () { return " Produit { " + " nom = " + nom + " , quantite = " + quantite + ’} ’; 33 } 34 35 } 36 37 38 39 40 public class TestGestionFichier { public static void main ( String [] args ) { JFileChooser choixFichier = new JFileChooser (); int resultat = choixFichier . showSaveDialog ( null ); 41 42 if ( resultat == JFileChooser . CANCEL_OPTION ){ 43 System . out . println ( " Création du fichier annulée . " ); 44 return ; 45 } 46 47 File nomFichier = choixFichier . getSelectedFile (); 48 if ( nomFichier == null || nomFichier . getName (). equals ( " " )){ 49 System . out . println ( " Nom de fichier incorrect . " ); 50 return ; 51 } 52 53 Produit oeuf = new Produit ( " Oeuf " , 6); 54 try { ObjectOutputStream sortie = new ObjectOutputStream ( new FileOutputStream ( nomFichier )); sortie . writeObject ( oeuf ); sortie . close (); 55 56 57 58 59 } 60 catch ( Exception e ){ e . printStackTrace (); 61 62 } 63 64 65 resultat = choixFichier . showOpenDialog ( null ); if ( resultat == JFileChooser . CANCEL_OPTION ){ 66 System . out . println ( " Création du fichier annulée . " ); 67 return ; 68 } 69 70 nomFichier = choixFichier . getSelectedFile (); 71 if ( nomFichier == null || nomFichier . getName (). equals ( " " )){ 72 System . out . println ( " Nom de fichier incorrect . " ); 73 return ; 74 } 75 76 ObjectInputStream entree = null ; 77 try { entree = new ObjectInputStream ( new FileInputStream ( nomFichier )); while ( true ){ 78 79 Produit unProduit = ( Produit ) entree . readObject (); System . out . println ( unProduit ); 80 81 } 82 83 } 84 catch ( java . io . EOFException e ){ - 23 - System . out . println ( " Fin de la lecture du fichier . " ); 85 86 } 87 catch ( Exception e ){ e . printStackTrace (); 88 89 } 90 finally { try { 91 entree . close (); 92 93 } 94 catch ( Exception e ){ e . printStackTrace (); 95 } 96 } 97 } 98 99 100 } 101 102 103 104 run : Produit { nom = Oeuf , quantite =6} Fin de la lecture du fichier . - 24 -