Programmation avancée
Le projet ci-après illustre la structure d'un programme java en ligne de commande qui traite les arguments de la ligne de commande comme paramètres du point d'entrée du programme. Il comprend :
- des classes ;
- des paquets (pour contenir les classes) ;
- une méthode main (point d'entrée du programme).
Les notions de classe et d'objet sont explicitées ci-après. La qualification de static de la méthode main est expliquée plus loin (méthode de classe). Le tableau de chaînes de caractères en paramètre de la méthode permet de prendre en compte les arguments de la ligne de commande.
Essayer : java Hello Luc Maître:Yoda
Principes de la programmation par objets
Un objet fusionne des données (caractéristiques structurelles ou variables) et des traitements (caractéristiques comportementales ou opérations) dans un tout dont le type est une classe (class). C'est la classe qui déclare les données et les opérations dont elle explicite (pour certaines) la méthode (la manière de procéder).
class Compteur { int valeur; Compteur() { valeur = 0; } void augmente() { valeur += 1; } void remetAZéro() { valeur = 0; } }
Pour créer un objet, on crée un exemplaire de sa classe par une opération appelée instanciation (avec l'opérateur new). On peut alors accéder aux données de l'objet et/ou déclencher sur cet objet les méthodes définies dans sa classe.
Compteur cpt = new Compteur(); System.out.println("Le compteur est à " + cpt.valeur); cpt.augmente(); System.out.println("Le compteur est à " + cpt.valeur); cpt.valeur += 1; System.out.println("Le compteur est à " + cpt.valeur); cpt.valeur -= 4; System.out.println("Le compteur est à " + cpt.valeur); cpt.remetAZéro(); System.out.println("Le compteur est à " + cpt.valeur);
On peut cacher certaines caractéristiques structurelles ou comportementales (des sous-programmes utiles pour définir les opérations exposées) (private). Cela empêche l'accès direct aux données cachées des objets ou le déclenchement des méthodes cachées. Les caractéristiques exposées (donc visibles et accessibles) sont qualifiées de public. On parle de masquage (de l'implantation).
class Compteur { private int valeur; public Compteur() { valeur = 0; } public void augmente() { valeur += 1; } public int valeur() { return valeur; } public void remetAZéro() { valeur = 0; } }
Compteur cpt = new Compteur(); System.out.println("Le compteur est à " + cpt.valeur); //n'est plus possible System.out.println("Le compteur est à " + cpt.valeur()); cpt.augmente(); System.out.println("Le compteur est à " + cpt.valeur()); cpt.valeur += 1; //n'est plus possible System.out.println("Le compteur est à " + cpt.valeur()); cpt.remetAZéro(); System.out.println("Le compteur est à " + cpt.valeur());
Si la valeur des données ne peut être obtenue que par une méthode de consultation (un informateur ou getter) et modifiée que par une méthode de mise à jour (transformateur ou setter), on parle d'encapsulation. Cela permet de garantir la validité des données d'un objet (valeur dans leur intervalle de définition par exemple). Dans le cas de notre compteur, il ne peut plus avoir une valeur négative.
L'encapsulation garantit aussi la cohérence des données entre elles, comme dans l'exemple de cette classe Heure.
class Heure { private int h, m; public Heure() { h = 12; m = 0; } public void augmenteDe(int heures, int minutes) { if (m + minutes > 59) heures++; m = (m + minutes) % 60; h = (h + heures + (minutes / 60)) % 24; } public int heures() { return h; } public int minutes() { return m; } public String enTexte() { String texte = ""; if (h < 10) texte += "0"; texte += h; texte += ":"; if (m < 10) texte += "0"; texte += m; return texte; } }
Cela permet aussi de changer de représentation par exemple, sans incidence sur le programme qui utilise la classe d'objets concernée.
class Heure { private int m; public Heure() { m = 720; } public void augmenteDe(int heures, int minutes) { m = (m + heures * 60 + minutes) % 1440; } public int heures() { return m / 60; } public int minutes() { return m % 60; } public String enTexte() { String texte = ""; if (heures() < 10) texte += "0"; texte += heures(); texte += ":"; if (minutes() < 10) texte += "0"; texte += minutes(); return texte; } }
Le projet ci-après illustre ces notions avec les classes Time et Date. Il met aussi en oeuvre une classe de tests unitaires et différentes structures de contrôles (if, if-else, switch-case-default, for) et le mécanisme d'exceptions du langage java (Exception, try-catch, throws et throw).
Une interface (interface) permet de spécifier une API (Application Programming Interface).
On peut aussi définir des constantes et une classe peut définir des méthodes qui ne s'appliquent pas à leurs représentants ou objets mais à la classe elle-même. On appelle ces méthodes des méthodes de classe et on les qualifie par le mot-clé static). On peut de même définir des variables dont la valeur est partagée par toutes les instances d'une classe parce que ces variables sont portées par la classe elle-même. On les appelle variables de classe (mot-clé static).
L'héritage enfin complète l'outillage de programmation par objets en offrant un mécanisme de factorisation des caractéristiques (données et méthodes) communes à plusieurs classes en définissant une classe de base qui transmet ses caractéristiques à des classes dérivées qui peuvent compléter (mot-clé extends) la définition par d'autres données et/ou méthodes. Une classe qui sert de base à un héritage peut ne pas correspondre à la définition d'objets réels. On dit alors que cette classe est non instanciable et on la déclare abstraite (avec le mot-clé abstract). Dans une classe abstraite, on peut spécifier des opérations sans définition (sans méthode) en les qualifiant de abstract.
Le projet ci-après illustre ces notions avec la classe abstraite BaseTime, l'interface Clock et les classes Time et Duration. 5
Le projet ci-après illustre la notion de méthode de classe avec la classe Date et ses opérations leapYear(int year) et numberOfDaysForMonth(int month, int year).
Une classe peut aussi définir des objets qui rassemblent plusieurs objets de classes différentes. On parle d'agrégation quand les éléments de l'ensemble ont une "vie" indépendante de celle de l'ensemble (exemple des personnes membres d'une ou plusieurs équipes) ou de composition quand les éléments de l'ensemble ont une vie liée à celle de l'unique ensemble auquel ils appartiennent (exemple des appartements d'un immeuble). Cela a une incidence sur la manière de définir les constructeurs et les transformateurs (setters) de la classe.
Une classe peut aussi être définie pour représenter une collection d'objets. Les opérations réalisables sur une collection étant communément admises (ajout, recherche, suppression …), on les spécifie dans une interface. Cette interface pouvant être implantée de différentes manières (tableau, structures chaînées, fichier…), on peut définir plusieurs classes qui implantent (mot clé implements) l'interface. Un programme utilisant une collection pourra déclarer la variable servant au stockage des objets en utilisant l'interface comme type (principe de substitution de Liskov, voir principes SOLID).
Le projet ci-après illustre ces notions avec la classe List. Cette classe définit une classe imbriquée (ListElement). La définition d'une classe imbriquée est le seul cas où l'on peut cacher une classe (mot-clé private ou protected).
Collections et parcours de collections
Lorsqu'un traitement doit être effectué sur chacun des éléments d'une collection, il faut parcourir cette collection, ce qui peut être fait de différentes façons. Dans le cas d'une liste chaînée, le parcours avec une boucle for est inefficace parce que l'accès à un élément par son indice est long puisqu'il se fait en parcourant la liste de son premier élément jusqu'à l'élément voulu. Avec une collection de n (n > 1) éléments, le premier élément est parcouru n fois, le deuxième n-1 fois…
Tous les parcours sont décomposables en 3 opérations : initialisation, test de fin de parcours, obtention du prochain élément. Cela peut être défini dans une classe au moyen de 3 méthodes. On appelle itérateur une telle classe.
Les itérateurs peuvent être mis en oeuvre explicitement (déclaration et instanciation d'objet itérateur puis utilisation des méthodes de l'itérateur - hasNext() et next()) ou implicitement avec la structure de contrôle for(typeElement e : collection).
La version suivante illustre la notion d'itérateur en montrant qu'il est possible de définir différentes façons de parcourir une collection. Elle définit une méthode pour obtenir un itérateur ou un autre (on parle de fabrique).
La version suivante illustre la possibilité de passer une méthode comme argument d'une opération. Elle définit pour cela la méthode dans une classe dédiée à ce seul objectif : c'est la réification (voir réification en informatique). Elle transmet enfin un objet de cette classe comme paramètre pour faire appliquer cette opération. Pour éviter de définir une classe (nommée ou anonyme), le projet ci-après utilise une expression anonyme (ou expression lambda ou lambda).
Patron Stratégie
On parle de stratégies lorsqu'il est possible de réaliser une opération sur un même objet de différentes manières. On peut par exemple afficher tous les éléments d'une collection horizontalement - les uns à la suite des autres, ou verticalement - les uns en dessous des autres. On peut aussi trier une collection en utilisant différentes méthodes de tri (tri bulle, tri sélection, tri fusion, tri rapide …).
Pour cela, on définit les différentes méthodes de traitement sous la forme d'une méthode execute() dans différentes classes qui implantent chacune l'interface Strategy, qui spécifie ces classes de traitement.
L'objet sur lequel on applique le traitement peut être passé comme argument du constructeur de la classe implantant la stratégie.
La version suivante illustre le patron de conception stratégie en montrant qu'il est possible de définir différentes manières de réaliser un traitement et d'appliquer à l'exécution l'une des stratégies possibles (en fonction du contexte par exemple). Elle propose deux stratégies d'affichage et une méthode de tri. Comme les deux opérations à la base du tri sont la comparaison et l'échange de deux éléments, il faut pour pouvoir trier une collection que ses éléments soient comparables entre eux. L'interface Comparable est utilisée ici pour typer les éléments de la collection ainsi que pour spécifier les classes dont les instances peuvent être comparées entre elles (Time, Alarm…).
Exercice à rendre
Implanter le tri sélection (défini dans la méthode sort() de la classe List comme une stratégie et définir une autre stratégie pour le tri bulle. Modifier la méthode sort() pour qu'elle applique aléatoirement (voir la méthode random() de la classe Math) l'une des 2 stratégies de tri ainsi définies.
Interface utilisateur graphique
Exercice à rendre
Modifier la classe Alarm pour qu'elle utilise un objet de la classe Time plutôt qu'une chaîne de caractères. Modifier toutes les classes du programme affectées par cette modification.
Modifier l'IHM pour contrôler la saisie et la modification de l'heure d'une alarme. Cela peut se faire en remplaçant la zone de texte txtTime par deux zones de texte distinctes pour la saisie des heures et minutes. Cette solution permet de se familiariser avec les agenceurs de composants de Swing.
Que l'interface conserve une seule zone de texte de saisie de l'heure ou en adopte deux distinctes, il conviendra de définir un ou des écouteurs (à placer dans le paquet controler) qui réagissent au changement de valeur saisie (utiliser addKeyListener sur un JTextField) pour vérifier que le texte saisi correspond à ce qui est attendu. Le résultat du test de correspondance à une expression régulière (ou expression rationnelle) permet ainsi, lorsque la valeur saisie est correcte, d'écrire cette valeur en noir et d'activer le bouton de modification, ou lorsque la valeur saisie est incorrecte, d'écrire la valeur saisie en rouge et de désactiver le bouton de modification.
Patron Décorateur
Décorateur est un patron de conception de structure qui permet d’affecter dynamiquement de nouveaux comportements à des objets en les plaçant dans des emballeurs qui implantent ces comportements. L'idée est la même que pour les systèmes en couches dont les services offerts s'appuient sur les services offerts par les couches inférieures.
L'exemple suivant illustre ce principe en proposant dans une classe KeyboardForBoundedIntegers une méthode int getIntBetween(int, int) qui utilise la méthode int getInt() définie dans la classe KeyboardForIntegers. Cette méthode utilise la méthode String getString() définie dans la classe Keyboard. La couche la plus basique (Keyboard) fournit un service (getString) utilisé par la couche au dessus d'elle, qui peut fournir un service de plus haut niveau (getInt). Cette couche fournit un service utilisé par la couche au dessus d'elle, qui peut fournir un service de plus haut niveau (getIntBetween). La couche de plus haut niveau (KeyboardForBoundedIntegers) est un décorateur de la couche inférieure (KeyboardForIntegers), qui est elle-même un décorateur pour la couche inférieure (Keyboard).
un exemple de couches de décorateurs
Décorateur répond aux problèmes suivants :
- comment ajouter des responsabilités à un objet dynamiquement au moment de l'exécution ?
- comment ajouter des options de manière flexible, sans devoir multiplier les classes par héritage ?
Ce second exemple illustre la réponse à ces problèmes.
un exemple de décorateurs pour de multiples options
Les entrées-sorties en java sont supportées par des classes du paquet java.io qui mettent en oeuvre ce patron de conception.
Le support de cours sur les entrées-sorties en Java.
Patron Singleton
Singleton est un patron de conception qui limite à un seul exemplaire le nombre d'instances d'une classe. Pour y parvenir, il déclare privé le constructeur de la classe et propose une méthode de classe getInstance qui instancie la classe si ce n'est pas déjà fait, retourne l'instance créée dans tous les cas.
Patron Visiteur
On peut vouloir ajouter une méthode m à une classe A pour traiter des objets d'une classe B sans le pouvoir. Par exemple, ajouter à la classe DataOutputStream une méthode writeTime(Time t) alors qu'on n'a pas le code source de la classe DataOutputStream. L'héritage offre cette possibilité en définissant une classe A', qui hérite de A et y ajoute la méthode voulue. Une autre solution consiste à définir une méthode m' dans la classe B pour que le traitement se fasse sur une instance de la classe A. L'idée est que si l'on ne peut pas rendre possible a.m(b) avec a instance de A et b instance de B, on peut rendre possible b.m'(a). Il n'y a qu'à examiner le code de la méthode saveInto(DataOutputStream s) de la classe Alarm :
s.writeUTF(name); time.saveInto(s);
dans laquelle time.saveInto(s) pourrait s'écrire s.writeTime(this).
Il se peut aussi que la méthode m à ajouter corresponde à une opération qu'il est possible de faire de différentes façons. C'est le cas par exemple de l'enregistrement dans un fichier, qui peut se faire en texte brut, au format XML ou JSON… Il faut alors définir autant de méthodes qu'il y a de manières de réaliser l'opération… et ajouter une nouvelle méthode à chaque nouvelle manière de faire l'opération… en rendant la classe ainsi définie particulièrement chargée en méthodes, sans réelle valeur conceptuelle.
La solution apportée par le patron visiteur consiste à déporter la méthode à ajouter dans une classe distincte dont le nom serait celui de la manière de réaliser l'opération. La mise en oeuvre de ce patron de conception s'appuie sur 2 interfaces :
public interface Operable { //visitable void performs(Operation op); } public interface Operation { //visiteur void doItOn(B b); }
Pour ajouter à la classe B une méthode m sans changer la classe B, il suffit que B implante l'interface Operable. On peut alors définir la méthode m en définissant une classe M qui implante l'interface Operation et qui définit les traitements à réaliser dans sa méthode doItOn. S'il y a plusieurs manière m1, m2, m3 de faire m, on définit autant de classes Operation que nécessaire (M1, M2, M3). Si d'autres manières de faire m doivent être ajoutées plus tard, on peut le faire sans rien changer à la classe B.
Dans l'exemple de la méthode save, il est question d'enregistrer une heure (instance de la classe Time) dans un fichier. Cela peut se faire de différentes manières : en texte brut (.txt), en XML (.xml), au format JSON (.json), en binaire. Et il faudra sans visiteur ajouter à la classe Time (en fait Clock dans notre cas) autant de nouvelles méthodes (saveTxt, saveXml, saveJson…).
Patron Observateur
Le projet suivant propose différents types de vue sur les alarmes et permet d'ouvrir plusieurs fenêtres pour voir une alarme.
AlarmList avec différents types de vue sur les alarmes
Malheureusement, un changement sur une alarme affichée une ou plusieurs fois ne se répercute pas sur les vues de cette alarme.
Le patron de conception Observateur fournit une solution au problème de la mise à jour d'objets dits observateurs qui exploitent un objet dit observé (ou observable ou sujet) en permettant à ce dernier de notifier aux observateurs son changement d'état.
Ce patron peut être mis en oeuvre dans une application graphique qui propose plusieurs vues sur un même objet.
Patron Commande
Le patron de conception Commande transforme une action à effectuer en un objet autonome (réification) qui contient tous les détails de cette action. Cette transformation permet de paramétrer des méthodes avec différentes actions, planifier leur exécution, ou les mettre dans une file d’attente. Ce patron permet de séparer complètement le code initiateur de l'action, du code de l'action elle-même. Il est souvent utilisé dans les interfaces graphiques où, par exemple, un item de menu et un bouton sont connectés à la même commande.
Patron Mémento
Le patron de conception Mémento permet de conserver l'état d'un objet pour le restaurer après modification si nécessaire….
Exercice à rendre
Ajouter au programme Alarmes un menu Edition contenant les options Modifier et Supprimer (déjà proposées sous forme de bouton) ainsi qu'une option Annuler.
Consulter la page WikiLivres du patron de conception Memento et implanter l'option annuler en utilisant ce patron.
XML, SAX et jDOM
exemples d'utilisation de jDOM
Alarmes avec fichier XML de préférences
Exercice à rendre
Le programme lit les préférences mais ne change pas les variables soundName et quitConfirm en fonction des préférences lues. Modifier le code de la classe Alerte pour cela soit fait.
Patron Procuration
Le patron de conception Procuration interpose un intermédiaire M (ou mandataire ou Proxy) entre une classe cliente C et une classe fournisseur F (objet du mandat). Au lieu d'utiliser un fournisseur de la classe F, la classe cliente utilise un objet de la classe M qui a la même API. Le mandataire relaie (ou pas) tous les appels de méthode à l'objet du mandat de la classe F mais peut effectuer d'autres traitements : mémorisation des résultats pour ne pas les redemander au fournisseur (proxy cache), bloquer certains appels (contrôle d'accès), journalisation…
Patron Adaptateur
Le patron de conception Adaptateur permet d'utiliser des objets d'une classe présentant des méthodes différentes de celles qu'on voudrait avoir tout en fournissant le service voulu. Il faut donc passer par un intermédiaire qui se charge de l'adaptation des API (pas obligatoirement le même nombre de méthodes).
Patron Composite
Le patron de conception Composite …
Introspection (réflexivité)
L'introspection pour un langage de programmation est la capacité à traiter non seulement du domaine dans lequel on écrit un programme mais aussi à traiter du programme lui-même. Elle confère aux langages qui en sont capables des possibilités inédites.
L'introspection est rendue possible par les méthodes proposées dans les classes Object, Class, Field et Method du paquetage java.lang.reflect, dont les principales sont présentées dans ce document.
Injection de dépendances
L'injection de dépendances applique le principe d'inversion de contrôle pour créer dynamiquement (injecter) les dépendances entre les différents objets en s'appuyant sur une description (fichier de configuration ou métadonnées) ou par programme.
Elle est utile pour remplacer une classe par une autre, notamment pour la réalisation de tests unitaires avec des objets factices (easymock, mockito).
L'injection de dépendances est expliquée dans l'article de Martin Fowler qui évoque les frameworks permettant d'en faire : picoContainer et Spring.
Références
Contrôle de connaissances
L'Unité d'Enseignement est évaluée par un examen écrit noté sur 20 auquel s'ajoute un bonus pouvant aller jusqu'à 3 points pour les exercices rendus.