Initiation à la programmation objet avec Java

Olivier Capuozzo

03 Janvier 2012

Résumé

Ceci est un support de cours DAIGL (Développement d'Applications Informatiques et Génie Logiciel) à destination d'étudiants en STS IG (Section de Technicien Supérieur en Informatique de Gestion).

Il fait suite à 4 mois d'initiation à la programmation avec PHP.

Ce support est basé sur le langage Java, et mis en oeuvre sous GNU/Linux, mais peut l'être sur d'autres environnements non ouverts, non libres, comme MS-Windows, sans aucun problème particulier.

Ce document a été réalisé sous GNU/Linux avec vim , au format docbook , mis en page avec le processeur XSLT saxon développé par Michael Kay et les feuilles de styles de Norman Walsh .

Ce document est placé sous la Licence GNU Free Documentation License, Version 1.1 ou ultérieure publiée par la Free Software Foundation.


Table des matières

1. Intro
1.1. Kit de développement Java
1.2. Installation du jdk
2. Premier programme
2.1. Compilation
2.2. Exécution
2.3. Modification
2.4. Premières comparaisons avec PHP
2.5. Exploitation des données reçues en ligne de commande
2.6. Exercices
2.6.1. Solution
3. Types primitifs
3.1. Tout n'est pas objet
3.1.1. Types primitifs
3.1.2. Exemple de conversions
3.2. Exercices
4. Interaction utilisateur <--> application
4.1. Exemple de fonctions d'interaction avec l'utilisateur (en MODE GRAPHIQUE)
4.1.1. Afficher des informations en mode Graphique
4.1.2. Lire des informations entrées au clavier en mode Graphique
4.2. Exemple de fonctions d'interaction avec l'utilisateur (en MODE CONSOLE)
4.2.1. Afficher des informations en mode Console
4.2.2. Lire des informations entrées au clavier en mode Console
4.3. Exercice
5. Concepts objet
5.1. Caractéristiques principales d'un objet
5.2. Classe
6. Utilisation d'objets
6.1. Introduction
6.2. Cycle de vie d'un objet
6.2.1. Ce qu'il faut retenir
6.3. Variable référence
6.4. Exercices
7. Utilisation de collections d'objets (tableau)
7.1. Tableau
7.2. Exercices
8. Interface et classes d'implémentation
8.1. Introduction
8.2. Interface
8.3. Modification du programme principal
8.4. Classe d'implémentation À COMPLÉTER
8.5. Collection
8.6. Exercices
9. Une série d'exercices en conception/utilisation d'objets
9.1. classe Adresse
9.2. classe Personne
9.3. classe Personne ayant au moins une Adresse
10. Les concepts fondamentaux de l'objet
10.1. Intro
10.2. Héritage
10.3. Vocabulaire et définition
10.4. Héritage sous contrôle
10.4.1. Contrôle côté parent - exposer ses caractéristiques
10.4.2. Contrôle côté enfants - que faire de son héritage ?
10.5. Héritage et constructeurs
10.6. Un zoo
10.7. static : lorsque la classe se prend pour un objet
10.8. Exercices
11. Exercices d'entrainement
11.1. Objectifs
11.2. Conception d'une hiérarchie de classes et gestion d'une liste d'objets
11.3. Poursuite d'un implémentation, utilisation en ligne de l'API Java
12. Exercices de révision
12.1. Question I - Truc : Valeur par défaut et constructeur (7 points)
12.2. Question II - Voiture : Activité de conception (4 points)
12.3. Question III - Voiture : Paramétrage du nombre de vitesses de la boîte ( 5 points )
12.4. Question IV - Voiture : Ajout d'une méthode, et test unitaire ( 4 points )
13. Programmer des interfaces graphiques
13.1. Intro
13.2. Étape 1 : La maquette papier
13.3. Étape 2 : Conception de la fenêtre et de ses composants
13.4. Étape 3 : Placer les composants dans un conteneur
13.5. Étape 4 : Gérer la position du composant dans la vue
13.6. Étape 5 : Associer un gestionnaire d'événement
13.7. Étape 6 : Coder la logique de traitement
13.8. L'exemple complet
13.9. Résumé
13.10. Exercices
14. Jeu du pendu en mode graphique
14.1. Introduction
14.2. Gestionnaire de positionnement
14.3. Affichage d'une image
14.4. Analyse de la maquette du jeu
14.5. Programmation de l'application
14.6. Annexe : diagramme de classes du jeu dans ses versions texte et graphique
15. Gestion de fichiers
15.1. Intro
15.2. Java et la gestion des fichiers
15.3. Exploitation en lecture d'un fichier texte
15.4. Exploitation en écriture d'un fichier texte
15.5. Exercice
15.6. Gestion de fiches Contact
15.7. Exercices
16. Boîte de dialogue
16.1. Introduction
16.2. Item de menu
16.3. Logique de l'événement
16.4. La boîte de dialogue
16.5. Code de fermeture de la boîte de dialogue
16.6. Conclusion
16.7. Exercices
17. Interaction avec un SGBDR
17.1. Introduction
17.2. Pilotes JDBC
17.3. I - Chargement du pilote dans la JVM ( Java Virtual Machine )
17.4. II - Etablissement de la connexion
17.5. III - Exécution d'une requête SQL
17.6. IV - Exploitation des résultats
17.6.1. Requête d'interrogation avec l'ordre SELECT
17.6.2. Requête de mise à jour (UPDATE, INSERT, DELETE)
17.7. Requêtes paramétrées
17.8. Exemple de programme
17.9. Type Java et SQL
17.10. Requête de modification avec les méthodes updateXXX
17.11. Transaction
18. Prise en main d' HSQLDB (Hypersonic SQL)
18.1. Introduction à HSQLDB
18.2. Installation
18.3. Démarrage d'hsqldb
18.4. Outil Database Manager
19. Utilisation de HSQLDB en mode serveur, avec deux clients
19.1. Conclusion
20. TP avec JDBC et HypersonicSQL
20.1. Projet à rendre

1. Intro

Les concepts de la programmation orientée objet datent des années 60 avec le langage Algol puis Simula. Le langage Smalltalk est une référence en la matière et plus récemment Eiffel (fin de année 80 début année 90).

Objectifs principaux de l'approche objet.

  • Produire des logiciels fiables, garants d'une certaine robustesse.

  • Rechercher les qualités de réutilisabilité et d'extensibilité.

  • Faciliter le déploiement et la maintenabilité.

C'est, en d'autres mots, répondre aux exigences du génie logiciel dont l'objectif est de produire du logiciel de qualité.

Java est un langage de programmation qui permet une certaine approche objet des solutions.

Comme tout langage de programmation, Java dispose de bibliothèques spécialisées sous forme de modules fonctionnels, comme en PHP, et de classes d'objets. Nous aurons l'occasion d'étudier ces deux concepts, car ils sont, avec le concept d'interface, centraux en programmation objet.

C'est un changement de paradigme par rapport à la programmation structurée (basée sur la notion de fonction). La plupart des langages issus de l'approche structurée ont évolué vers l'objet : C -> C++, Pascal -> Pascal Object/Delphi, VB -> VB.NET, Ada, PHP ... D'autres langages sont directement conçus dans une approche objet, après Simula dès 1966 : Eiffel, Smalltalk, Java, C#...

Java est le fruit d'un travail initié par une équipe d'ingénieurs de chez SUN Microsystems. Java s'est fortement inspiré du C++ dont il a éliminé certaines caractéristiques comme les pointeurs et limité d'autres comme l'héritage multiple.

Quelques caractéristiques remarquables de Java

  • Fortement typé.

    Distinction forte entre les erreurs survenant à la compilation (erreur de syntaxe, de non concordance de type, de visibilité...) et les erreurs d'exécution (chargement et liaison des classes, génération de code, optimisation...).

  • Indépendance des plates-formes d'exécution.

    Dans la mesure où celle-ci dispose d'une machine virtuelle Java (JVM)

  • Sûreté de fonctionnement.

    Le système d'exécution Java, inclus dans la JVM, vérifie que le code n'est pas altéré avant son exécution (absence de virus et autre modification du code).

  • Gestion automatique de la mémoire.

    Libération automatique de la mémoire réalisée par un garbage-collector (ramasse-miettes). Mais ordre de création des objets à l'initiative du développeur (opérateur new)

  • Orienté Objet.

    Diminution du temps de développement grâce à la réutilisation de composants logiciels .

  • Réalisation d'interface utilisateur graphique.

    Une hiérarchie de classe (Java Fundation Classes - jfc) connue sous le nom de Swing, permet de construire des IHM de qualité.

  • Interprété et multitâches.

    Le code est d'abord compilé, traduit en Byte code. C'est ce byte code qui sera interprété par la JVM.

    Le langage supporte le multithreading avec primitives de synchronisation.

  • Robuste.

    La gestion des exceptions est intégrée au langage.

  • Orienté réseau et client/serveur.

    Client/serveur traditionnel : Application classique, gestion de protocoles réseaux, pont d'accès aux bases de donnée (jdbc), RMI. . .

    Client/serveur Web : Applet côté client et Servlet côté serveur.

  • Déploiement facilité.

    Les classes peuvent être assemblées en unités d'organisation physique et logique (package). Présence d'une API d'internationalisation.

  • Gratuit et "participatif".

    De grandes entreprises participent avec SUN à la réussite de Java, et croient à l'avenir de ce langage : IBM, ORACLE, BORLAND...

    Dispose d'une grosse bibliothèque de base (plus 1600 classes et interfaces pour le JDK 1.2.2).

1.1. Kit de développement Java

Les principales Versions du jdk

  • jdk 1.0 : première version, orienté développement Web (applet)

  • jdk 1.1 : version plus costaud, destiné aussi aux applications d'entreprises.

  • jdk 1.2 : améliore et enrichie la 1.1 : collections, interface utilisateur swing etc.

  • jdk 1.3 : de nouvelles performances et de nouvelles classes (3D . . . )

  • jdk 1.4 : amélioration de la vitesse d'exécution, de nouvelles fonctionnalités (XML, String...)

  • jdk 1.5 et sup : collections typées, encapsulation automatique des types primitifs...

1.2. Installation du jdk

Pour développer en java, vous devez disposer d'un JDK ( Java Developement Kit ), un kit de développement, ainsi qu'une documentation associée et un éditeur.

Pour installer un JDK sur votre machine, vous pouvez vous référer ici http://www.linux-france.org/prj/edu/archinet/DA/install-java. Vous y trouverez une procédure détaillée, contenant en fin de document un lien vers une ressource d'installation de java pour d'autres systèmes (Mac, Windows).

Parmi les sous-répertoires du jdk, on trouve

  • bin : Fichiers exécutables dépendants de la plate-forme (java, javac . . . )

  • demo : Une collection de programmes de démonstration (jfc et applet)

  • jre : Java Runtime Environment. Un environnement d'exécution pour la plateforme.

  • lib : Diverses librairies.

A la racine de l'installation se trouve le fichier src.jar qui contient les sources de la librairie. Il peut être décompressé dans un sous répertoire à créer pour l'occasion (par exemple <rep du jdk>/src).

2. Premier programme

Le point d'entrée d'un programme Java standard est une fonction, nommée main, à l'entête prédéfinie :

/*** fonction appelée automatiquement
* au lancement du programme. 
* @param args la liste des arguments reçus en ligne de commande 
*/
 static public void main(String[] args)
 

Elle est appelée par le système, après chargement, suite à son lancement.

En programmation objet, toute fonction est forcément placée à l'intérieur d'une structure appelée classe .

Un programme Java est donc composé d'au moins une classe, et au plus... il n'y a pas de limite théorique ! Ainsi il n'est pas rare de voir des projets ayant plus d'un millier de classes !

Avant tout chose, créons un répertoire. Il est fortement conseillé de créer un répertoire par projet . Un projet regroupe l'ensemble des fichiers nécessaires à l'application.

[java]$ mkdir tp1 
[java]$ cd tp1 
[tp1]$ 

Bon, créons notre classe. Comment allons nous l'appeler ? Nous avons pris l'habitude en PHP, de bien nommer nos identifiants, nous ferons bien entendu de même avec Java (ou tout autre langage).

[Important]Convention de nommage

Par convention, le nom des classes commence toujours par une majuscule .

A l'inverse, le nom des fonctions (méthodes) et des attributs (variables) commence toujours par une minuscule .

Nous enregistrons notre classe dans un fichier nommé AppTp1.java .

/*** fichier : AppTp1.java 
* date : 02-novembre-2003 
*/ 
public class AppTp1 {
 /**
  * fonction appelée automatiquement
  * au lancement du programme. 
  * @param args la liste des arguments 
  * reçus en ligne de commande
 */
 static public void main(String[] args) {
   // ne fait rien
 }
} 

Vous constaterez que, comme PHP, les structures de blocs sont délimitées par les symboles { et } , et le commentaires par // bout de ligne commenté , pour une ligne, ou /* plusieurs lignes */ .

Il existe une convention très intéressante concernant les commentaires : Ceux multilignes de la forme /** ... */ , observez les deux étoiles du début . S'ils sont placés avant certaines parties (classe, attributs, méthodes), ils seront perçus comme des commentaires javadoc , qui servent de base de données à la construction automatique de la documentation des interfaces (API) de votre code. Vous n'avez pas à vous en inquiéter pour le moment.

Vous devez faire attention à deux choses ici : 1/ l'extension d'un programme source java est .java , en minuscule. 2/ le nom du fichier est identique au nom de la classe (lorsque celle-ci est déclarée publique).

[Note]Déclaration des variables

Toute variable utilisée au sein d'un programme Java doit être déclarée avant toute utilisation.

Déclarer une variable, c'est lui associer définitivement un type.

2.1. Compilation

Tel quel, le programme n'est pas exécutable.

Il doit être compilé , c'est à dire traduit en un autre langage, appelé byte code pour être compris par l'interpréteur. Au cours de cette traduction, de nombreuses et précieuses vérifications sont réalisées. Si des erreurs sont rencontrées par le compilateur (le traducteur), il ne générera pas la version compilée, charge alors au développeur de corriger sa copie. Exemple de compilation avec javac :

[tp1]$ ll
total 4
-rw-rw-r-- 1 kpu   kpu   94 nov  1 20:46 AppTp1.java
[tp1]$
[tp1]$ javac AppTp1.java 
[tp1]$
[tp1]$ ll -tr 
total 8 
-rw-rw-r--   1 kpu   kpu  94 nov  1 20:46 AppTp1.java 
-rw-rw-r--   1 kpu    kpu   257 nov  1 20:47 AppTp1.class 
[tp1]$ 

La compilation a bien fonctionné. Un nouveau fichier est apparu, avec une extension .class .

2.2. Exécution

Nous utiliserons la commande java (comme la commande précédente, mais sans le c pour compilateur). En prenant bien soin de ne pas mentionner l'extension ! Exemple :

[tp1]$ java AppTp1
[tp1]$ 

Ainsi, il ne se passe rien ! Que s'est-il passé au juste ?

La commande java a fait en sorte de charger la classe AppTp1.class en mémoire, puis la méthode main est appelée. Comme elle ne comporte aucune instruction, son exécution se termine et le programme s'arrête.

2.3. Modification

Modifions notre programme afin qu'il affiche un message à l'écran :

public class AppTp1 {
  static public void main(String[] arg) {
   System.out.println ("Hello, ce n'est pas encore tout à fait");
   System.out.println ("de la programmation objet !");
  }
} 

Pour réaliser l'affichage, nous utilisons l'instruction System.out.println(...) . Au regard de la convention de nommage, et avec notre expérience de la programmation en PHP, nous pouvons affirmer sans erreur que :

  • System est une classe; le premier caractère est une majuscule, la suite en minuscule.

  • out est un identificateur qui commence par une minuscule et s'utilise sans parenthèse : c'est donc une variable (autres appellations courantes attribut , champ ou membre). Ici out est un attribut publique détenu par la classe System , il n'y a pas de variables globales en programmation objet .

  • println() débute par une minuscule et s'utilise avec des parenthèses, c'est donc une fonction (mais on dit méthode ). Il n'y a pas de fonctions globales en programmation objet .

Équivalent PHP : print() ou echo() .

Après avoir sauvegardé les modifications, nous devons recompiler le programme pour, de nouveau, le tester :

[tp1]$ vim AppTp1.java     # ici nous avons utiliser l'éditeur vim
[tp1]$ javac AppTp1.java   # compilation
[tp1]$ java AppTp1         # exécution
Hello, ce n'est pas encore tout à fait
de la programmation objet !
[tp1]$

Voilà, le tour est joué. Mais pourquoi avoir mentionné que ce n'est pas tout à fait de la programmation objet ?

Simplement parce que (pratiquement) aucun objet n'a été crée ! Et pourtant il y a un lien extrêmement étroit entre une classe et un objet.

2.4. Premières comparaisons avec PHP

Prenons un exemple en PHP. Ce programme, qui ne devrait pas vous poser de difficulté, il s'attend à recevoir 2 arguments en ligne de commande, à savoir un nom et un prénom.

<?php
require_once("utilitaires.php");
  
if (argc() == 3) {
  $nom = strtoupper(argv(1));
  $prenom = argv(2);
  echo "Bonjour " . $prenom . " ". $nom. ".\n";
}
else {
  echo "Bonjour inconnu(e).\n";
} 
?>

Exemple d'exécution :

$ php -q test.php
Bonjour inconnu(e).

Le même en Java :

public class Bonjour {
  static public void main(String[] arg) {
    if (arg.length == 2) {
      String nom;
      nom = arg[0].toUpperCase();
      String prenom = arg[1];
      System.out.println("Bonjour " + prenom + " "+ nom +".");
    }
    else {
      System.out.println ("Bonjour inconnu(e).");
    } 
  }
}

Exemple d'exploitation (le code source a été sauvegardé dans un fichier nommé Bonjour.java :

$ javac Bonjour.java
$ java Bonjour
Bonjour inconnu(e).

Les variables sont explicitement typées. Le typage d'une variable est réalisé lors de sa déclaration et non plus lors de son affectation comme en PHP. Exemple :

...
String nom;
...

Conséquence, vous devez fournir à une variable une valeur cohérente avec son type, dans le cas contraire le compilateur refusera toute traduction. Exemple :

...
String nom;
nom = toto; // ERREUR : si toto n'est pas une variable de type String
nom = 'toto'; // ERREUR : le symbole ' est réservé au type char (caractère)
nom = "toto"; // OK !
nom = 12; // ERREUR : 12 est une valeur numérique (un entier)
nom = 12.0; // ERREUR : 12.0 est un réel (double)
nom = false; // ERREUR : false et 
nom = true; //           true sont (les seules valeurs) de type boolean
nom = System.out; // ERREUR : out est un Objet de type PrintStream
...

Les fonctions sont préfixées par un identificateur suivi d'un point. Cet identificateur peut être soit le nom d'une classe soit une référence à un objet. Exemple de fonction 'static' :

...
System.exit(0); 
   // Stop l'exécution de la machine virtuelle java (JVM)
...

D'un point de vue PHP, on peut dire que exit est une fonction de la bibliothèque System.

D'un point de vue Java, exit est une fonction utilitaire (static) de la classe System.

Ces deux points de vue sont strictement équivalents.

Toute fois, les fonctions utilitaires sont marginales en Java (et dans tout langage objet). Dans la majorité des cas nous avons à faire à des fonctions membres d'un objet (instance d'une classe). Exemple :

...
String nom = arg[0].toUpperCase();
...

arg[0] référence un objet de type String en mémoire. Cet objet dispose d'un état (un ensemble ordonné de caractères) et propose certains services sous la forme de fonctions membres appelées plus généralement méthode.

[Note]Notation objet

Un appel de méthode se caractérise par un appel de fonction référencée par un objet : objet.fonction(), alors qu'en PHP nous n'avions vu que l'approche type 'utilitaire', de la forme fonction(données). Exemple :

Tableau 1. 

PHP - ProcéduralJava - Objet
...
$nom = argv(1);
$nomMAJ = strtoupper($nom);
...
...
String nom = arg[0];
String nomMAJ = nom.toUpperCase();
...

Avant d'aborder plus avant les notions d'interface, de classe et d'objet, nous allons nous familiariser avec la séquence : édition, compilation et exécution.

2.5. Exploitation des données reçues en ligne de commande

Au lancement d'un programme java, la fonction main de la classe nommée, reçoit une liste d'arguments. Chaque argument est de type chaîne de caractères. Dans le programme suivant, la liste d'arguments est représentée par la variable args, cette liste peut éventuellement être vide.

  
public class Application {
  static public void main(String [] args) {
    System.out.println(args);
  }
}

Une exécution donne :

[kpu@kpu tp]$ java Application
java.lang.String;@1a16869

args est une variable qui référence un objet de type "Tableau de chaînes de caractères". C'est l'adresse de cet objet qui est affichée ici (sans intérêt pour nous). Voici deux des caractéristiques d'un objet de type Tableau (array) :

  • length : un attribut public de l'objet référencé par args.

    => Généralement les attributs d'un objet ne sont pas publics mais privés.

    => Attention : écrire args.length et non args.length() (pas une méthode)

    Exemple d'utilisation de length :

    public class Application {
      static public void main(String [] args) {
        System.out.println("Je reçois " + args.length + " argument(s)");
      }
    }
    

    Exemple d'exécutions :

    [kpu@c419-12 tp]$ javac Application.java
    
    [kpu@c419-12 tp]$ java Application
    Je reçois 0 argument(s)
    
    [kpu@c419-12 tp]$ java Application a
    Je reçois 1 argument(s)
    
    [kpu@c419-12 tp]$ java Application aaa
    Je reçois 1 argument(s)
    
    [kpu@c419-12 tp]$ java Application aaa a
    Je reçois 2 argument(s)
    
  • [ ] : les crochets servent à atteindre la valeur d'un élément du tableau.

    Entre les crochets, on donne le rang (indice) de l'élément dans le tableau.

    => Attention, l'indice du premier élément est 0 (zéro) et non 1.

    Exemple d'utilisation de [ ] :

    public class Application {
      static public void main(String [] args) {
      System.out.println("Le premier argument est " + args[0]);
      }
    }
    

    Exemple d'exécution :

    [kpu@c419-12 tp]$ java Application aaa
    Le premier argument est aaa
    

Nous souhaitons afficher un message particulier lorsque qu'il n'y a pas d'argument. Pour cela nous testons la valeur de length dans une expression booléenne. Nous utilisons l'opérateur arithmétique de comparaison ==

public class Application {
  static public void main(String [] args) {
    if (args.length == 0 ) {
      System.out.println("Aucun argument.");
    }
    else {
      System.out.println("Je reçois " + args.length + " argument(s)");
    }    
  }
}

Exemple d'exécution :

[kpu@c419-12 tp]$ java Application 
Aucun argument.

[kpu@c419-12 tp]$ java Application sd
Je reçois 1 argument(s)

[kpu@c419-12 tp]$ java Application sd zz
Je reçois 2 argument(s)

La structure de décision est la même qu'en PHP.

[Note]Structure conditionnelle

La forme générale de cette structure de contrôle de flux est :

if (expr. bool) {
  // instruction(s) si l'expr. bool est true
}
[ else {
  // instruction(s) si l'expr. bool est false
} ]
  
  • N'oubliez pas les parenthèses qui entourent toujours l'expression booléenne.

  • Les accolades ne sont pas obligatoires lorsqu'il n'y a qu'une seule instruction dans un bloc (toutefois nous conseillons de ne pas les omettre, comme c'est le cas ici).

  • La clause else (sinon) est facultative, d'où la présence des crochets dans la forme générale.

2.6. Exercices

  1. Retrouver les 4 erreurs (syntaxe et non respect des conventions) qui se cachent dans le programme suivant :

    public class application {
      public void main(String [] args) {
        if (args.length > 1 ) {
          String $s1 = args(0);
          s2 = args[1];
          System.out.print("Les 2 premiers arguments sont : ");
          System.out.println($s1 + " et " + s2);
        }
      }
    }
    
  2. Transformer le programme Application.java de sorte qu'il affiche le mot argument sans s lorsqu'il reçoit un seul argument, et avec un s lorsqu'il en reçoit plusieurs.

  3. Afficher la valeur des arguments.

    Rappel : Le premier argument est : args[0], le second args[1], etc.

    On se contentera des 3 premiers arguments au maximum (s'ils existent). S'il y en a plus, le programme affichera des points de suspension (...) après le troisième.

  4. Modifier le programme de sorte que, s'il reçoit deux arguments dont la valeur du premier est soit "Mr" soit "Me", le programme affichera une formule du genre : "Bonjour madame machin" ou "Bonjour monsieur chose", en fonction du deuxième argument.

  5. Modifier le programme afin qu'il affiche les initiales de la personne.

    Vous trouverez ci-dessous, quelques méthodes offertes par tout objet de type String, qui pourrait bien vous être utiles.

    Figure 1. Extrait de la documentation de l'API Java

    Extrait de la documentation de l'API Java

  6. Afficher l'ensemble de TOUS les arguments reçus en ligne de commande. Pour cela vous utiliserez une boucle for, dont la syntaxe est identique à celle de PHP.

2.6.1. Solution

  1. Retrouver les 4 erreurs (syntaxe et non respect des conventions) qui se cachent dans le programme suivant :

    public class Aapplication { 
      // les noms des classes commencent par une Majuscule
    
    static public void main(String [] args) {
        if (args.length > 1 ) {
          String $s1 = args[0];
          String s2 = args[1];
          System.out.print("Les 2 premiers arguments sont : ");
          System.out.println($s1 + " et " + s2);
        }
      }
    }
    
  2. Transformer le programme Application.java de sorte qu'il affiche le mot argument sans s lorsqu'il reçoit un seul argument, et avec un s lorsqu'il en reçoit plusieurs.

  3. Afficher la valeur des arguments.

    Rappel : Le premier argument est : args[0], le second args[1], etc.

    On se contentera des 3 premiers arguments au maximum (s'ils existent). S'il y en a plus, le programme affichera des points de suspension (...) après le troisième.

    public class AfficheArg {
      static public void main(String[] args) {
        if (args.length >=1) System.out.println("arg0 = "+args[0]);
        if (args.length >=2) System.out.println("arg1 = "+args[1]);
        if (args.length >=3) System.out.println("arg2 = "+args[2]);
        if (args.length >=4) System.out.println("...");
      }
    }
    
  4. Modifier le programme de sorte que, s'il reçoit deux arguments dont la valeur du premier est soit "Mr" soit "Me", le programm affichera une formule du genre : "Bonjour madame machin" ou "Bonjour monsieur chose", en fonction du deuxième argument.

    public class AfficheArg {
      static public void main(String[] args) {
        String pol="";
        if (args.length ==3) {
          if (args[0].equals("Mr")  pol="monsieur ";
          if (args[0].equals("Me")  pol="madame ";
          System.out.println("Bonjour " + pol + args[1] + " " + args[2]);
        }
    }
    
  5. Modifier le programme afin qu'il affiche les initiales de la personne.

    public class Initiales {
      static public void main(String[] args) {
        if (args.length == 2 ) {
          String nom = args[0];
          String prenom = args[1];
          System.out.print("Les initiales sont : ");
          System.out.println(nom.substring(0,1).toUpperCase()
              + prenom.substring(0,1).toUpperCase());
        }
      }
    }
    
  6. Afficher l'ensemble de TOUS les arguments reçus en ligne de commande. Pour cela vous utiliserez une boucle for, dont la syntaxe est identique à celle de PHP.

    public class AfficheArg {
      static public void main(String[] args) {
        int i = 0;
        for (i=0; i<args.length; i++) {
          System.out.println("arg "+i+" = "+args[i]);
        }
      }
    }
    

3. Types primitifs

3.1. Tout n'est pas objet

En Java, une variable est soit de type primitif soit de type référence .

De ce fait le langage Java ne peut être qualifié de langage "purement objet", bien que cette différence s'estompe avec un jdk >= 5.O.

Java traite les variables de type primitif selon une sémantique par valeur et les variables de type objet (classe, interface, tableau - array -) selon une sémantique par référence (ce n'est pas la valeur de la variable qui nous intéresse, mais l'objet référencé). Exemple :

Le fragment de code suivant n'EST PAS VALIDE :

...
int x = 2;
System.out.println(x .toString() ); // INTERDIT !! (avec jdk < 1.5.0)
// x ne référence pas un objet. 
// int n'est pas une classe d'objet, mais un type primitif 
...

Java propose 9 types primitifs, leur nom est entièrement en minuscule ( int, boolean, char, byte, float, double, long, short, void) :

3.1.1. Types primitifs

Les types primitifs ne sont pas des classes.

Figure 2. Types primitifs de Java

Types primitifs de Java

Chaque type primitif a son correspondant en type Objet. Alors pourquoi pas le "tout objet" ?

Les créateurs du langage Java ont souhaité conserver l'utilisation des types primitifs dans un but d'efficacité. Un point de vue remis partiellement en cause avec la version 1.5.0 du jdk...

Point de vue plus technique (peut être sauté) : Les "objets" de type primitif sont automatiquement créés sur la pile, et leur valeur confondue avec leur identificateur (sémantique par valeur). Toutes les autres entités sont de véritables objets, créés sur le tas (et non sur la pile) accessible via un ou plusieurs identificateurs; des variables de type référence (sémantique par référence).

La colonne la plus à droite du tableau des types primitifs donne le nom des classe associées à chaque type primitif. Ces classes sont porteuses d'informations telles que les limites du domaine de définition, mais aussi proposent des fonctions utilitaires de conversion relatives au type élémentaire dont elle ont la charge (et qu'elle peuvent représenter en les enveloppant, c'est pour cela qu'on les qualifie de classes wrapper).

3.1.2. Exemple de conversions

  • Conversion de type String vers un type primitif :

    ...
     String s1 = "123";
     String s2 = ".23";
    
     int    x1 = Integer.parseInt(s1);
     double x2 = Double.parseDouble(s2);
     float  x3 = Float.parseFloat(s2);
     float  x4 = Float.valueOf(s2); // même effet
    ...
    

    ... une méthode de la forme typePrimitif ClasseWrapper.parseTypePrimitif(String s) .

  • diverses autres fonctions de conversion, comme par exemple la classe Integer qui propose par exemple les méthodes static String toBinaryString(int i) , static String toHexString(int i) , etc. voir l'aide en ligne.

3.2. Exercices

  1. Une entreprise de location de voitures loue des véhicules de tourisme au conditions suivantes :

    • Forfait journalier : 50 €, majoré de 10% pour les véhicules de plus de 10 chevaux.

    • Le prix du km est de 0.10 €.

    Concevoir ce programme en java. Les données sont reçues en ligne de commande (via la fonction main), dans un tableau de String : elles devront donc être converties en type numérique.

    Une solution : Loca.java

4. Interaction utilisateur <--> application

Nous changeons de registre ici, et présentons quelques moyens pratiques d'interagir avec l'utilisateur d'un programme écrit en java.

4.1. Exemple de fonctions d'interaction avec l'utilisateur (en MODE GRAPHIQUE)

4.1.1. Afficher des informations en mode Graphique

On utilise l'instruction showMessageDialog() .

Attention, cette méthode n'EST PAS UNE MÉTHODE D'INSTANCE (elle est déclarée static ). C'est une fonction disponible dans le module (la classe) JOptionPane . On préfixera le nom de cette méthode par le nom de la classe dans laquelle elle est définie , soit ici JOptionPane , qui se trouve dans le package javax.swing :

import javax.swing.*;
public class Test {
  static public void main(String[] args) {
    Object obj = "Hello !"; // une chaîne de caractères est un objet.
    javax.swing.JOptionPane.showMessageDialog(null, obj); 
  }
}

Ce qui donne :

Figure 3. JOptionPane.showMessageDiaglog

JOptionPane.showMessageDiaglog

4.1.2. Lire des informations entrées au clavier en mode Graphique

Nous utiliserons la méthode showInputDialog de la classe JOptionPane .

L'exemple ci-dessous, demande un nom à l'utilisateur. Si ce dernier clique sur Annuler , la fonction rendra la valeur null , mais si l'utilisateur ne saisit aucune valeur et clique sur OK la fonction rendra une chaîne vide. Dans le cas où l'utilisateur fournit une valeur, elle est affectée au nom de l'étudiant.

import javax.swing.*;
public class Test {
  static public void main(String[] args) {
    String nom = JOptionPane.showInputDialog("Entrez un nom");

    if (nom == null) {
       nom = "";
    }

    if (! "".equals(nom) ) {
       javax.swing.JOptionPane.showMessageDialog(null, "Bonjour " + nom);
    }

  }
}

Figure 4. JOptionPane.showInputDiaglog

JOptionPane.showInputDiaglog

Vous êtes bien entendu invité à tester ce programme.

Plus d'info sur http://www.linux-france.org/prj/edu/archinet/DA/

[Note]Test d'égalité

Pour comparer deux valeurs primitives on utilise (comme en PHP) l'opérateur == . Avec les objets, on utilise la méthode equals , comme dans l'exemple ci-dessus, qui compare l'état des objets , et non leur référence.

En absence de contexte graphique, nous pouvons utiliser l'entrée standard avec un echo à l'écran. C'est cette technique que nous utilisons ci-après.

4.2. Exemple de fonctions d'interaction avec l'utilisateur (en MODE CONSOLE)

4.2.1. Afficher des informations en mode Console

On utilise l'instruction System.out.println() ou System.out.print() - même fonction mais ne provoque pas de retour chariot. Exemple :

public class Application {
   static public void main(String[] args) {
      System.out.print("He");
      System.out.println("llo !");
   }
}//Application

Ce qui donne :

$ java Application
Hello !
$

4.2.2. Lire des informations entrées au clavier en mode Console

Depuis Java 5.0, c'est un peu plus facile. En effet la classe Scanner (de la librairie java.util) est capable de prendre facilement en charge les informations en provenance de l'entrée standard.

Voici un programme qui demande des valeurs de différents types à l'utilisateur :

import java.util.Scanner;

public class Test {
 public static void main(String[] args) {
   Scanner clavier = new Scanner(System.in);

   System.out.println("Entrez votre nom : ");
   String nom = clavier.next();

   System.out.println("Entrez votre age : ");
   int age = clavier.nextInt();
   
   System.out.println("Entrez votre moyenne : ");
   float moyenne = clavier.nextFloat();
   
   System.out.println("Vos caractéristiques sont : \n");
   System.out.println("Nom : " + nom);
   System.out.println("Age : " + age);
   System.out.println("Moyenne : " + moyenne);
 }
}

Ce qui donne à l'éxécution :

Entrez votre nom : 
toto
Entrez votre age : 
2
Entrez votre moyenne : 
12,5
Vos caractéristiques sont : 

Nom : toot
Age : 2
Moyenne : 12.5

4.3. Exercice

  1. On appelle palindrome un mot qui se lit de la même façon de gauche à droite et de droite à gauche. Par exemple « elle », « radar » sont des palindromes.

    Concevoir un programme en interaction avec l'utilisateur, fidèle au scénario suivant :

    1. Le programme demande un mot à l'utilisateur.
    2. Tant Que ce mot est non vide le programme
    3.   Informe l'utilisateur de la qualité palindrome ou non du mot.
    4.   Redemande un mot à l'utilisateur.
    5. Fin TQ.
    
  2. Deux nombres sont dits amis si la somme des chiffres les composant sont identiques.

    Concevoir un programme qui demande deux nombres à l'utilisateur (en fait des chaînes de caractères) et l'informe s'ils sont amis ou non.

  3. Lister les ensembles de nombres amis compris entre 1 et 20, puis entre 1 et 100.

    Coup de pouce : les ensembles seront stockés dans un tableau de chaînes de caractères dont la déclaration est : String[] amis = new String[20];

    Les nombres amis d'un nombre n seront accessibles à l'indice n dans le tableau amis. Par exemple les amis de 3 sont dans amis[3] soit "3, 12, 21, 30"

    Pour terminer, vous afficherez, sans redondance, les ensembles d'amis.

  4. Concevoir un programme qui reçoit un ensemble de notes dont la valeur est soit dans l'intervalle [0..20] soit 'abs'.

    Exemple : 12 8 2 20 15 abs 13 0

    Le programme calcule la moyenne des notes, sans prendre en compte les abs

     moyenne (12 8 2 20 15 abs 13 0) => (12+8+2+20+13) / 5

    2 cas de figure à programmer : les données sont reçues en argument (et réceptionnées par le main dans un tableau de String) ou reçues dans une seule String qu'il faudra décomposer - voir la méthode split de String.

    => Dans un second temps, le programme affichera un message d'avertissement en cas d'impossibilité de déterminer la moyenne (au lieu de boguer).

5. Concepts objet

5.1. Caractéristiques principales d'un objet

Un objet est caractérisé par un ensemble d'opérations (méthodes) et d'attributs, retenus pour leur pertinence par rapport au domaine étudié.

Un attribut est caractérisé par un type, un nom et une valeur. Une méthode par un type, un nom et des instructions. Par exemple :

Etudiant :
  // les attributs
    (entier, age,  18)
    (réel, taille, 1.75)
    (string, nom, "Dupond")
    (booléen, stage_réalisé, faux)
    (réel, moyenne, 12)
    ...
  // les méthodes
    (booléen, passage2annee, { retourner (stage_ok et moyenne > 10) })
     ...
   

La valeur que peut prendre un attribut, une méthode, est fonction de son type. Par exemple l'opération passage2annee retournera soit false soit true.

On appelle état d'un objet, l'ensemble des valeurs de ses attributs à un instant t.

A un instant donné, un objet étudiant aura comme état par exemple {age=18, taille=1.8, nom="Toto", stage_realise=true, ...}

5.2. Classe

Nous avons vu qu'une classe est une sorte de module pouvant comportée des fonctions utilitaires (marquées static).

Une classe sert aussi, et c'est en fait son premier rôle, à représenter les caractéristiques types d'un objet. Par exemple en Java, la classe Etudiant se représente ainsi.

public class Etudiant {
   
   /**** parties cachées ****/

   // attributs 
   private String nom;
   private String prenom;
   private String login;
   private boolean stage_realise;


   /**** parties publiques ****/

   // constructeur
  /**
   *  Création d'un étudiant
   *  avec Anonymous comme valeur du nom par défaut.
   */
   public Etudiant() {
     this.nom="Anonymous";
   }

   // méthodes
   /**
   * Représentation texte de l'objet
   */ 
   public String toString() {
     String res = "*** Etudiant ***";
     res = res + "\nNom  : " + this.getNom();
     res = res + "\nPrenom : " + this.getPrenom();
     res = res + "\nLogin : " + this.getLogin();
     res = res + "\nStage : ";
     res = res + ((stage_realise) ?  "Réalisé" : "Non réalisé");
     return res;
   }

   /**
   * @return String le login
   */
   public String getLogin() {
     return this.login;
   }

   /**
   * @param unLogin le nouveau login
   */
   public void setLogin(String unLogin) {
     this.login = unLogin;
   }

   /**
   * @return String le nom
   */
   public String getNom() {
     return this.nom;
   }

   /**
   * @param unPrenom le nouveau prenom
   */
   public void setPrenom(String unPrenom) {
     this.prenom = unPrenom;
   }

   /**
   * @return String le prenom
   */
   public String getPrenom() {
     return this.prenom;
   }

   /**
   * @param unNom le nouveau nom
   */ 
   public void setNom(String unNom) {
     this.nom = unNom;
   }
}

La classe décrit les caractéristiques types des objets. C'est une sorte de moule servant à la fabrication (via l'opérateur new) des objets.

Le mot clé this désigne l'objet en cours d'exécution (création ou utilisation).

Nous allons compiler la classe Etudiant :

$ javac Etudiant.java
ll
total 8
-rw-rw-r--  1 user  user   1205 jan 17 09:28 Etudiant.class
-rw-rw-r--  1 user  user   1303 jan 17 09:27 Etudiant.java
$

La version compilée (Etudiant.class) est maintenant disponible pour utilisation.

[Note]Comment utiliser la classe Etudiant ?

Il existe au moins deux façons de mettre en oeuvre le concept objet : par délégation (utilisation d'objets) et par héritage (spécialisation de classes).

6. Utilisation d'objets

6.1. Introduction

La délégation est LA façon la plus naturelle d'utiliser des objets.

Un programme délégue à un objet (ou à plusieurs) certaines responsabilités.

L'utilisateur d'un objet ne voit que les caractéristiques publiques (public) de l'objet. On lui fournit la version compilée et une documentation des interfaces (API). L'outil javadoc permet de générer une telle documentation. Exemple d'utilisation

$ javadoc Etudiant.java -d docs/
$ cd docs/
$ firefox index.html &

Ce qui donne :

Figure 5. Documentation utilisateur de la classe Etudiant

Documentation utilisateur de la classe Etudiant

Un lien vers ce fichier : documentation API Etudiant

6.2. Cycle de vie d'un objet

Un objet informatique est comme un objet du "monde réel" ; il naît, vit et meurt.

  • Naît sous l'impulsion d'une instruction programmée avec l'opérateur new et un constructeur.

  • Vit, il stocke et échange des informations, rend des services, répond à des messages.

  • Meurt, lorsqu'il ne sert plus à rien (univers impitoyable).

Son espace est la mémoire informatique (illustrée par le film Matrix)

Son temps peut être bref (quelques milli-secondes) ou très long, dépassant celui de ses concepteurs (comme dans IA, le film)

Exemple de cycle de vie d'un objet Etudiant dans un programme Java :

public class AfficheEtudiant {
 static public void main(String[] args) {
   int nbarg =  args.length;

   // création d'un objet Etudiant
   Etudiant e = new Etudiant();

   // utilisation  de l'objet créé
   if (nbarg == 3){
     // valorisation de certains de ses attributs
     e.setNom(args[0]);
     e.setPrenom(args[1]);
     e.setLogin(args[2]);
   }
   
   System.out.println(e.toString());
   
   // fin de son espace d'utilisation, l'objet va disparaître  
 }// main

}//class

L'objet est créé avec new, et le constructeur sans argument Etudiant() est appelé à ce moment là.

Avant de disparaître un état est obtenu (méthode toString()), puis affiché.

Remarque => Une application orientée objet est composée, pour l'essentiel, d'objets collaborant, comme dans la vie réelle. Par exemple, lorsque je démarre titine, ma voiture, elle (titine est un objet, instance de Voiture, démarrer est une méthode de la classe Voiture) communique avec le démarreur (un autre objet, instance de la classe Démarreur) qui, à son tour envoie des messages au carburateur (instance de Carburateur) etc.

6.2.1. Ce qu'il faut retenir

  • L'utilisateur d'un objet ne connaît généralement que les constructeurs et méthodes marqués public.

  • Pour utiliser un objet, il faut disposer de sa classe compilée.

  • Pour créer un objet, on utilise l'opérateur new suivi d'un des constructeurs de sa classe.

  • Un fois construit, le programme peut faire appel à ses services en appelant certaines de ses méthodes. Par exemple, interroger (get) ou modifier (set) le login d'un objet Etudiant, etc.

  • Les attributs d'un objet sont d'ordinaire inaccessibles à ses utilisateurs. Ces derniers doivent passer par ce que l'on appelle des getter/setter - des accesseurs en lecture (get) et écriture (set) (parfois appelés interrogateur/modificateurs).

6.3. Variable référence

Java est un langage fortement typé : Toute variable doit être déclarée. On ne peut déclarer deux fois la même variable dans une fonction, même si elles diffèrent par leur type.

Les objets sont connus, accessibles, via leurs référents (des variables).

  • Un objet peut avoir 0 à n référents. "La fille de ma soeur", "ma nièce" (2 référents) désignent la même personne (le même objet). Exemple :

       // déclaration d'un variable nommée e
       // et création d'un objet Etudiant
       Etudiant e = new Etudiant();
    
       // déclaration d'un variable nommée le_meme
       // qui référence le même objet que e
       Etudiant le_meme = e;
    
       System.out.println(le_meme.getNom());
         // Affiche Anonymous
    
       e.setNom("Toto");
    
       System.out.println(le_meme.getNom());
         // Affiche Toto
    
  • Un référent référence 0 ou 1 objet : "la voiture que je vais acheter" désigne une voiture (une seule) que je ne connais pas encore (aucune instance). Exemple :

       // déclaration d'une variable référence
       Etudiant e = null;
         // e ne référence rien, sa valeur est : null
    
       System.out.println(e.getNom());
         // IMPOSSIBLE, e ne référence aucun objet !
    
       e = new Etudiant();
         // e référence maintenant un objet en mémoire
    
       System.out.println(e.getNom());
         // affiche Anonymous 
    

6.4. Exercices

  1. Une entreprise spécialisée dans la vente de jouets souhaite informatiser la gestion de ses produits. Un jouet est caractérisé par un nom, une matière principale, un prix et une couleur.

    Le type de public auquel est destiné un jouet est indiqué par une couleur. Par exemple, bleu pour les bébés, vert pour les enfants (moins de 12 ans), rouge pour les adolescents et noir pour les adultes.

    On souhaite construire un programme qui teste la classe Jouet dont voici les spécifications :

    Figure 6. Classe Jouet

    Classe Jouet

    Nous vous fournissons la version compilée de la classe Jouet : jouet.jar (nul besoin d'avoir le code source). A partir d'un projet Eclipse, créer un dossier ordinaire (pas un dossier "source") nommé lib et y placer la bibliothèque "jouet.jar" (qui contient Jouet.class). Puis, ajouter ce fichier (jouet.jar) au chemin des classes du projet (clic droit sur jouet.jar, Add To Build Path)

    1. Concevoir un programme qui crée deux objets de la classe Jouet.

      • Le premier jouet a pour nom "Poupée Barbie" à destination des enfants, coûtant 15 euro, en matière plastique.

      • Le second jouet a pour nom "Échec lumineux" à destination des adultes, coûtant 45 euro, en verre.

    2. Réaliser un menu (prévoir une boucle) permettant à l'utilisateur de :

      1. Afficher l'état de chacun de ces objets (nom, prix, matière et public concerné) en utilisant leurs accesseurs.

      2. Augmenter/Diminuer de x% le prix de chacun des objets. La valeur du pourcentage est fournie par l'utilisateur.

        On prendra soin de convertir la valeur saisie par l'utilisateur dans le bon type (voir exemple de conversions).

      3. Intervertir la couleur des deux jouets : la couleur du premier objet (Barbie) est affectée à la couleur du second (échec) en utilisant leurs accesseurs, et inversement.

      4. Quitter

  2. Exploitation des arguments en ligne de commande.

    Modifier votre programme de sorte que s'il reçoit un nombre suffisant d'arguments, il créé un des deux objets (ou les deux) de type Jouet.

  3. Voici le code source de la classe Jouet : Jouet.java.

    On vous demande

    1. D'ajouter, juste après les déclarations des attributs, un constructeur par défaut (c'est un constructeur sans argument), le voici :

       public Jouet ( ){
          // ne fait rien 
        }
      
    2. Créer un des deux objets à l'aide de ce constructeur, et afficher son état, comme précédemment.

      Une solution Jouet.java, TpJouet.java

    3. Tableau 2. Déterminez les valeurs par défaut allouées aux attributs par Java

      TypeValeur par défaut
      String 
      float 

  4. Ajouter à la classe Jouet une méthode toString() ayant la même signature (même interface) que celle dont dispose la classe Etudiant présentée plus haut dans ce document.

  5. Tester cette méthode dans votre programme.

    Une solution _Jouet.java (attention il faudra sauvegarder ce fichier sous le nom de Jouet.java), Tp2Jouet.java

7. Utilisation de collections d'objets (tableau)

Java propose deux façons de manipuler des objets : par des tableaux (array) ou par des collections (Liste, Dictionnaire, ...).

7.1. Tableau

Caractéristiques principales

  • Sa capacité est fixée lors de la construction, et ne peut plus être modifiée.

  • La capacité est le nombre maximal d'éléments que peut contenir un tableau.

  • Tout tableau dispose d'une propriété length qui est la valeur de sa capacité.

  • Le type des éléments d'un tableau est fixé lors de sa déclaration.

  • L'accès à un élément d'un tableau se réalise en utilisant une paire de crochets et un indice qui est sa position dans le tableau.

Utilisation :

Tableau 3. Utilisation d'un tableau

Désignation/RemarquesSyntaxeExemple

Déclaration

Par convention, le nom d'une variable de type tableau (ou collection) est au pluriel.

Le type des éléments d'un tableau peut être un type primitif ou un type référence (objet).

Type [] variable;
int [] desPoints;
float [] desNotes;
String[] desNoms;
Etudiant [] desEtudiants;

Construction d'un objet tableau avec les valeurs par défaut

Les éléments d'un tableau sont automatiquement initialisés à leur valeur par défaut (zéro pour les types primitifs numériques, false pour les booléens, null pour les types référence).

variable = new Type[capacité];
desPoints = new int[4]; 
desNotes = new float[40];
desNoms = new String[5];
desEtudiants = new Etudiant[40];

Construction d'un objet tableau avec des valeurs

Vous remarquerez que dans le cas d'une initialisation donnée, la capacité n'est pas directement renseignée.

variable = new Type[]{ ensemble de valeurs };
int [] desPoints = new int[]{-1,-1,-1,-1};  // création d'un tableau de 4 éléments
String [] desNoms = new String[] {"toto", "titi"}; // desNoms.length==2
Jouet [] desJouets = new Jouet[] { new Jouet() }; // desJouets.length==1

Remarques

  • Attention, un erreur courante consiste à croire que l'instruction desEtudiants = new Etudiant[40]; crée 40 objets Etudiant, alors qu'elle ne crée qu'un tableau contenant 40 références à des objets de type Etudiant, chacun étant initialisé à null.

Tableau 4. Accès à un élément d'un tableau

Accès à un élément en lecture/écritureExemple

Lecture

TypeElement variable = tableau[position];

int point = desPoints[0]; // accès au premier élément 
point = desPoints[desPoints.length - 1];// accès au dernier élément

Écriture

tableau[position]= valeur;

desPoints[0]=20; // affecte la valeur 20 au premier élément 
desPoints[desPoints.length - 1]=0;//et 0 au dernier élément
desNoms[0]="toto";
desEtudiants[0]= new Etudiant();

Remarque

  • Toute tentative d'accès un élément d'un tableau avec un indice hors bornes [0..tableau.length[ déclenchera à l'exécution un exception de type java.lang.ArrayIndexOutOfBoundsException.

  • Exemple de parcours de l'ensemble des éléments d'un tableau;

    class Tab{
      static public void main(String[] args){
        String[] tab = new String[3];
        for (int i=0; i<tab.length; i++) {
          System.out.println(tab[i]);
        }
      }
    }
    

    Exemple d'exécution :

    $ javac Tab.java
    $ java Tab
    null
    null
    null
    $
    

    Remarque : Les éléments du tableau sont de type String (objet), leur valeur par défaut est null.

Tableau 5. Exemple de tableau multidimensionnel

Désignation/RemarquesSyntaxeExemple
Construction de tableau à deux dimensionsType [] [] variable; // un tableau à 2 dimensions
 // un damier de 20 cases x 20 cases
boolean [ ] [ ] lesCases = new boolean[20][20];

 // une bataille navale
Bateau [ ] [ ] lesCases = new Bateau[nbLignes][nbCol]; 

7.2. Exercices

La classe à utiliser : Compte.jar.

Gestion de plusieurs comptes (d'après une idée de Nathalie Gruson)

  1. Créer un fichier TPGestCompte.java.

  2. Définir le tableau lesComptes : il doit pouvoir contenir au moins 10 objets de classe Compte. Vous trouverez ici l'API documenté de la classe Compte.

  3. Affichez un menu avec les options suivantes: 

    • 0 : Quitter

    • 1 : Création d'un compte

    • 2 : Consultation des comptes

    1=> doit créer un objet Compte à partir du nom du compte saisi par l’utilisateur et l’ajouter à la collection lesComptes.

    2 => doit permettre de visualiser les informations de base (nom du compte et solde) de tous les comptes créés + le solde total (somme des soldes des différents comptes).

  4. Complétez votre menu avec les options suivantes :

    • 3 : Dépôt par position

    • 4 : Retrait par position

    Vous concevez le dépôt/retrait par position du compte concerné dans la collection : le programme invite l'utilisateur à saisir une position.

    Avant et après chaque opération, le solde du compte concerné est affiché.

    On considère que la valeur saisie par l'utilisateur est un nombre entier valide. Si cette valeur est hors des bornes attendues, un message d'erreur sera présenté à l'utilisateur.

  5. Complétez votre menu avec les options suivantes (plus difficile) :

    • 5 : Dépôt par nom

    • 6 : Retrait par nom

    Vous concevez le dépôt/retrait par nom du compte concerné dans la collection : le programme invite l'utilisateur à saisir un nom de compte.

    Avant et après chaque opération, le solde du compte concerné est affiché.

    Si la valeur fournie par l'utilisateur ne correspond à aucun compte, un message d'erreur sera présenté à l'utilisateur.

  6. Complétez votre menu avec l’option suivante :

    • 7 : Récapitulatif des opérations

      Doit récapituler le nombre et montant des crédits et des débits. Exemple

      Compte A1 : 10 dépôts (2500) + 4 retraits (982)
      Compte E2 : 5 dépôts (500) + 2 retraits (100)
      Au total : 15 dépôts (3000) + 6 retraits (1082)
      

Une solution : TPCompte.java

8. Interface et classes d'implémentation

8.1. Introduction

L'exercice précédent a abouti à la réalisation d'une classe TPGestCompte ayant une grosse fonction main mélangeant gestion de l'interface utilisateur avec des détails d'implémentations. Or, C'EST EXACTEMENT CE QU'IL NE FAUT PAS FAIRE !

En effet, en l'état, le programme est difficilement maintenable : les interactions utilisateur (affichage du menu, saisies d'informations, affichage de résultats) sont confondues avec la logique de traitement (créer un compte, rechercher un compte, lister les comptes...).

La solution à ce problème est simple, et consiste à concevoir une classe. On pourra alors réaliser une décomposition fonctionnelle (décomposition des traitements par fonctions), au même titre que nous avons appris à le faire avec PHP. Bien entendu, la syntaxe diffère un peu, mais à peine, comme vous allez le constater.

8.2. Interface

La première chose à faire lorsque l'on conçoit une classe est de définir ses services, en particulier les services offerts au travers de ses instances.

Par exemple, les services (ce sont des opérations) offerts par des objets de type ArrayList, sont spécifiés dans l'interface List (API List). Les opérations d'ajout d'un élément, de suppression, de recherche etc. sont spécifiées ici.

Nous allons faire de même ici, en déclarant des opérations, sans leur corps, dans une structure appelée interface.

Remarque : Noter que cette déclaration peut être réalisée directement dans une classe ; c'est une solution moins souple car elle lie une seule implémentation à une interface donnée, mais satisfaisante dans bien des cas. Nous vous proposons ici, de spécifier les déclarations d'opérations dans une interface, il ne vous restera plus qu'à concevoir plusieurs classes d'implémentation en exercices.

import java.util.*;

interface IFGestCompte {

  /**  retourne la liste des comptes */
  public List getListDesComptes();

  /**  retourne null ou la référence à un compte
  *    dans la liste.
  */

  public Compte getCompte(int position);

  /**  retourne null ou la référence du compte
  *    portant le nom reçu en argument.
  */
  public Compte getCompte(String nom);

  /**  retourne vrai s'il existe dans la liste un compte
  *    ayant comme nom la valeur reçue en argument,
  *    retourne faux sinon.
  */
  public boolean existeCompte(String nom);

  /**  
  * Ajoute un compte  à la liste des comptes.
  */
  public void ajouterCompte(Compte c);

 /**  retourne la liste des comptes, avec un état détaillé
  *    pour chacun d'eux.
  */
  public String details();

}

Nous avons, par l'intermédiaire de l'interface, simplement déclaré un contrat d'utilisation. Cette structure se compile, comme une classe, mais ne peut être instanciée car les opérations n'ont pas de corps !, en d'autres termes elles ne sont pas programmées. C'est aux classes qu'appartient cette responsabilité.

8.3. Modification du programme principal

Nous modifions la solution : TPCompte.java, en utilisant une classe qui implémente l'interface IFGestCompte.

import javax.swing.*;
import java.util.List;

public class AppCompte {
 static public void main(String[] args) {

   IFGestCompte gestComptes = new GestComptesArray();

   String menu = "0: Quitter";
   menu += "\n1: Création d'un compte";
   menu = menu + "\n2: Lister les comptes";
   menu = menu + "\n3: Dépôt par position";
   menu = menu + "\n4: Retrait par position";
   menu = menu + "\n5: Dépôt par nom";
   menu = menu + "\n6: Retrait par nom";
   menu = menu + "\n7: Récapitulatif des opérations";
   menu = menu + "\n.Votre choix SVP :";

   String choix; 
   choix= JOptionPane.showInputDialog(menu);
//   choix = Console.lireClavier(menu);

   while (!"0".equals(choix)) {
      if ("1".equals(choix)){
          String unNom = JOptionPane.showInputDialog("Entrez un nom de Compte");
          Compte c = new Compte(unNom);
          gestComptes.ajouterCompte(c);
      }
      else if ("2".equals(choix)){
         List liste = gestComptes.getListDesComptes();
         String res="**** Les comptes ****";
         for (int i=0; i<liste.size(); i++){
            res += "\n***********\n"+liste.get(i).toString(); 
         }
         JOptionPane.showMessageDialog(null, res);
        // System.out.println(res);
      }
      else if ("3".equals(choix)){
        String spos = 
           JOptionPane.showInputDialog("Entrez la position du compte");
        // traduire en String in int 
        int pos = Integer.parseInt(spos);
        Compte c = gestComptes.getCompte(pos);
        if (c != null){
          String smontant = 
             JOptionPane.showInputDialog("Entrez le montant du dépôt");
          double montant = Double.parseDouble(smontant);
          c.deposer(montant);
        }
       }
       else if ("4".equals(choix)){
         String spos = 
            JOptionPane.showInputDialog("Entrez la position du compte");
         // traduire en String in int 
         int pos = Integer.parseInt(spos);
         Compte c = gestComptes.getCompte(pos);
         if (c != null){
           String smontant = 
             JOptionPane.showInputDialog("Entrez le montant du retrait");
           double montant = Double.parseDouble(smontant);
           c.retirer(montant);
       }
      }
      else if ("5".equals(choix)){
        String nom = 
          JOptionPane.showInputDialog("Entrez le nom du compte");
        Compte c = gestComptes.getCompte(nom);
        if (c != null){
           String smontant = 
               JOptionPane.showInputDialog("Entrez le montant du dépôt");
           double montant = Double.parseDouble(smontant);
           c.deposer(montant);
         }
        }
        else if ("6".equals(choix)){
          String nom = 
             JOptionPane.showInputDialog("Entrez le nom du compte");
          Compte c = gestComptes.getCompte(nom);
          if (c != null){
            String smontant = 
               JOptionPane.showInputDialog("Entrez le montant du retrait");
            double montant = Double.parseDouble(smontant);
            c.retirer(montant);
         }
       }
       else if ("7".equals(choix)){
          JOptionPane.showMessageDialog(null, gestComptes.details());
        }
 
     // redemande un choix à l'utilisateur
      choix= JOptionPane.showInputDialog(menu);
     // choix = Console.lireClavier(menu);
   }
   // pour forcer la fermeture.
   System.exit(0);
 }
}

Vous constater que le programme c'est considérablement simplifié (par rapport au précédent : TPCompte.java).

Le programme délégue certaines responsabilités (recherche d'un compte dans la liste, construction de l'état détaillé de chacun des comptes...) à un objet dont la classe est GestComptesArray. Cette classe implémente l'interface IFGestCompte.

8.4. Classe d'implémentation À COMPLÉTER

L'objectif d'une classe est d'implémenter des services. Elle transforme des opérations en méthodes : une méthode est une réalisation d'opération.

Voici une version à compléter en exercice ! (sur la base de la solution à l'exercice précédent TPCompte.java) :


import java.util.*;

public class GestComptesArray implements IFGestCompte {
  // attributs
  private Compte [] lesComptes;
  private int nbCptes;

  //constructeur
  public GestComptesArray() {
   lesComptes = new Compte[10];
   nbCptes = 0;
  }

  // méthodes
  /**  
  * Ajoute un compte à la liste.
  */
  public void ajouterCompte(Compte c){
    // TODO 
  }

  /**  retourne la liste des comptes */
  public List getListDesComptes(){
    // TODO 
    return null;
  }

  /**  retourne null ou la référence à un compte
  *    dans la liste.
  */
  public Compte getCompte(int position){
    // TODO 
    return null;
  }

  /**  retourne null ou la référence du compte
  *    portant le nom reçu en argument.
  */
  public Compte getCompte(String nom) {
    // TODO 
    return null;
  }

  /**  retourne vrai s'il existe dans la liste un compte
  *    ayant comme nom la valeur reçue en argument,
  *    retourne faux sinon.
  */
  public boolean existeCompte(String nom){
    // TODO 
    return false;
  }

 /**  retourne la liste des comptes, avec un état détaillé
  *    pour chacun d'eux.
  */
  public String details(){
   // TODO
   return "TODO";
  }
}

Vous constatez que la classe d'implémentation reprend TOUTES les opérations définies dans l'interface, à cela elle ajoute des attributs privés nécessaire pour réaliser les méthodes, ainsi qu'un constructeur pour initialiser les attributs.

Une classe d'implémentation est donc généralement composée de 3 parties : attributs, constructeurs et méthodes .

8.5. Collection

Nous avons vu les tableaux. Intéressons nous maintenant aux objets de type List car nous en aurons besoin dans l'exercice qui suit. Les objets de type List sont d'une grande souplesse, particulièrement lorsque nous ne connaissons pas à l'avance le nombre d'éléments que contiendra notre liste.

Caractéristiques principales des collections de type List

  • Sa capacité varie automatiquement en fonction des besoins.

  • Le nombre d'éléments dans la collection est fourni par la méthode size().

  • Par défaut, le type des éléments d'une collection est Object, c'est le type de base de tout objet en Java, mais les collections peuvent être typées à la déclaration.

  • Avec les versions du jdk < 1.5.0, une collection ne peut contenir d'éléments de type primitif (seulement de type référence), pour cela le développeur utilisera les types "enveloppe" associés (Integer pour les int, Float pour les float etc.).

  • Les classes de type Collections sont dans le package java.util.*. Il faudra ajouter une clause 'import' à votre programme.

Tableau 6. Déclaration d'une collection de type List

SyntaxeExemple
List variable;
List desPoints;
List desNoms;
List desJouets;

Tableau 7. Construction d'une liste

SyntaxeExemple
variable = new ArrayList();
desPoints = new ArrayList(); 
desNoms = new ArrayList();
desJouets = new ArrayList();
// ou, en cas de collection typée :  List<Jouet>
desJouets = new ArrayList()<Jouet>;


Remarques

  • Par défaut une liste est vide à la création.

  • ArrayList et Vector sont des listes standard (mais pas les seules) couramment utilisées.

Tableau 8. Opérations courantes

OpérationExemple
Ajouter un élément
desPoints.add(new Integer(-1));
desNoms.add("toto");
desJouets.add(new Jouet("Echec truc", 45, "verre", "noir"));
Obtenir un élément
// On obtient le premier élément.
// que l'on doit typer ("caster") car par défaut le type
// des éléments d'une collection est Object.
Integer i = (Integer) desPoints.get(0);
String nom = (String) desNoms.get(0);
Jouet j = (Jouet) desJouets.get(0);
// ou en cas de collection typée
Jouet j = desJouets.get(0);
Obtenir le nombre d'éléments
// obtient le nombre d'éléments dans la liste (un int)
int n = desJouets.size();
Supprimer un élément
// supprime de la liste l'élément situé à la position 0
desJouets.remove(0); 

Remarques

  • Finalement, les listes sont d'une utilisation bien plus souple que les tableaux. Seul le cast au moment de la récupération d'un objet est verbeux. A noter que depuis le jdk 1.5.0, le développeur peut typer les éléments d'une collection.

  • Voici un extrait des opérations disponibles avec des objets de type List :

    Figure 7. Extrait des opérations de List

    Extrait des opérations de List

    Voir la liste complète des opérations ici : API List

8.6. Exercices

On vous fournit un programme principal (AppCompte.java) qui réalise un certain travail avec un objet de type IFGestCompte. Vous concevez et testez deux classes d'implémentation pour cette interface : GestComptesArray et GestComptesList.

Figure 8. Diagramme UML : relations entre interface et classes d'implémentation

Diagramme UML : relations entre interface et classes d'implémentation

  1. Créer un répertoire et placer y les fichiers IFGestCompte.java, GestComptesArray.java, AppCompte.java et Compte.java.

  2. Compiler ces fichiers, et lancer l'application : java AppCompte.

    Tester l'application. Vous constatez qu'elle n'est pas opérationnelle. Votre mission consiste à remplacer les TODO des méthodes de GestComptesArray par des instructions réalisant les opérations. Par exemple :

    import java.util.*;
    
    public class GestComptesArray implements IFGestCompte {
      ...
      /**  
      * Ajoute un compte à la liste.
      */
      public void ajouterCompte(Compte c){
        if (nbCptes < lesComptes.length) { 
          lesComptes[nbCptes] = c;
          nbCptes++;
        }
      }
      ...
    

    Un autre exemple :

     ...
     /**  retourne la liste des comptes sous la forme d'un objet de type List*/
      public List getListDesComptes(){
        List res = new ArrayList();
        for (int i=0; i < nbCptes; i++){
          res.add(lesComptes[i]);
        }
        return res;
      }
      ...
    

    A vous de poursuivre ce travail.

  3. Proposer une nouvelle classe d'implémentation, que vous nommez GestComptesList, qui implémente la variable membre privée lesComptes de type List :

    import java.util.*;
    
    public class GestComptesList implements IFGestCompte {
    
      private List lesComptes = new ArrayList();
    
      ... (implémenter les opérations déclarées par IFGestCompte)
    
    

    Vous la testerez en instanciant un objet de cette classe (GestComptesList au lieu de GestComptesArray) dans AppCompte.java :

    import javax.swing.*;
    import java.util.List;
    
    public class AppCompte {
     static public void main(String[] args) {
    
       IFGestCompte gestComptes = new GestComptesList();
    
     ... (comme avant)
    

    Il n'y a rien d'autre à modifier dans la classe AppCompte, car GestComptesArray et GestComptesList offrent exactement les mêmes services car ces deux classes implémentent la même interface.

Bonne programmation !

Éléments de solution : GestComptesArray.java, GestComptesList.java, AppCompte.java.

9. Une série d'exercices en conception/utilisation d'objets

9.1. classe Adresse

Nous voulons représenter la notion d'adresse physique d'une personne sous forme d'une classe. Pour cela nous avons retenu 3 propriétés : rue, ville et codePostal de type String, uniquement accessibles en lecture et écriture via des accesseurs (getter/setter).

  1. Proposer une version Java de cette classe (dans un fichier nommé Adresse.java)

  2. Dans un programme de tests (TestAdresse.java), créer un objet Adresse, avec les valeurs suivantes : rue:"03 rue des écoles", ville:"Melun", codePostal:"77000". Vous proposerez deux versions : une qui valorise les attributs à l'aide des setter, et une autre au moment de la construction (à l'aide d'un constructeur à concevoir).

  3. Ajouter une méthode toString à la classe Adresse, et tester cette méthode dans le programme de test.

9.2. classe Personne

Dans notre système d'information, une personne est caractérisée par un nom, un prénom et un login. On vous demande de concevoir une classe Personne respectant les règles de gestion suivantes :

  • Tout programme souhaitant créer un objet Personne, devra fournir au moins un nom et un prénom et au plus un nom, un prénom et un login.

  • En l'absence de login à la construction d'un objet Personne, celui-ci sera déterminé (calculé) par le constructeur sur la base de la règle suivante : login est une chaîne de caractères en minuscule composée de la première lettre du prénom suivi des premières lettres du nom, le tout ne devant pas dépasser 8 caractères, mais ne pas être inférieur à 4 caractères (à compléter de façon arbitraire le cas échéant).

    pré-condition : le prénom et le nom fournit par l'appelant ne sont pas vide (nom.trim().length()>0 et prenom.trim().length()>0).

    Exemple : nom=Arsac prénom=Jacques => login=jarsac

  • Le login d'une personne n'est pas accessible en écriture mais seulement en lecture.

Vous devez :

  1. Concevoir une classe Personne répondant aux spécifications ci-dessus.

  2. Ajouter un méthode toString à la classe Personne.

  3. Concevoir un programme de test (TestPersonne.java) qui créé deux objets de type Personne, sur la base des informations suivantes :

    • personne 1 : nom=Meyer prénom=Bertrand login=eiffel

    • personne 2 : nom=Ny prénom=Yvan

9.3. classe Personne ayant au moins une Adresse

Toute personne connue par le système d'information doit disposer au moins une adresse. Cette relation peut se représenter graphiquement par le diagramme UML suivant :

Figure 9. Un personne dispose au moins d'une adresse

Un personne dispose au moins d'une adresse

On vous demande de :

  1. Intégrer dans la classe Personne l'attribut lesAdresses présentés dans le diagramme UML ci-dessus.

  2. Ajouter à la classe Personne des méthodes permettant d'ajouter/obtenir/supprimer une adresse à la personne courante. La méthode ajouterAdresse reçoit une adresse à ajouter à la liste des adresses de la personne, les autres méthodes reçoivent en argument une position (devant correspondre à la position d'une adresse dans la liste des adresses détenue par la personne courante).

  3. On souhaite que les programmes qui utilisent des objets de type Personne puissent connaître le nombre d'adresses détenues par chacune de ces objets. Proposez une méthode dans Personne offrant ce service.

  4. Modifier le constructeur de Personne prenant en compte la règle de gestion "...une personne dispose au moins d'une adresse..." (on considère que l'appelant fournit une valeur non null).

  5. Modifier la méthode toString de Personne en conséquence.

  6. Concevoir une application (AppGestPersonnes.java) qui gèrent une liste de personnes, et qui propose à l'utilisateur un "menu" permettant de :

    1. Ajouter une personne

      => nom, prenom et adresse sont des données saisies par l'utilisateur.

    2. Supprimer une personne d'après sa position dans la liste.

      => position saisie par l'utilisateur.

    3. Ajouter une adresse à une personne de la liste dont on connaît la position

    4. Supprimer une adresse (d'après sa position) de la liste des adresses d'une personne de la liste.

      => attention à ne pas violer la règle de gestion spécifiant qu'une personne doit obligatoirement avoir au moins une adresse.

    5. Lister l'ensemble des personnes (avec leurs adresses)

  7. (optionnel) Permettre à l'utilisateur de modifier les caractéristiques d'une personne, d'une adresse d'une personne. Les valeurs actuelles sont proposées comme valeur par défaut lors des saisies utilisateur.

10. Les concepts fondamentaux de l'objet

10.1. Intro

La programmation objet est basée sur quatre concepts fondamentaux que sont l'encapsulation, l'abstraction, le polymorphisme et l'héritage. Jusqu'à présent nous avons utilisé l'abstraction au travers du concept d'interface, conçu par encapsulation en cachant les attributs (private), redéfini plusieurs fois la méthode toString (polymorphisme) et hérité nos classes de la classe Object sans le savoir ! Nous présentons sommairement ces concepts dans cette section, et nous nous attarderons sur la notion d'héritage et de polymorphisme.

  • Encapsulation

    L'encapsulation permet de cacher des détails d'implémentation aux clients de la classe

    Comment ? en déclarant privé (private) les propriétés à encapsuler.

  • Polymorphisme

    Le polymorphisme est une technique qui permet de proposer plusieurs versions d'une même opération.

    Comment ? par redéfinition ou par surcharge.

    Par exemple, la classe GestComptesArray propose deux versions de la méthode getCompte : une recevant un argument de type int (position) et un autre de type String (nom). Ce type de polymorphisme, qui consiste à proposer plusieurs méthodes de même nom mais ayant des arguments différents, s'appelle surcharge (overloading). L'autre forme courante de polymorphisme est la redéfinition (overriding), un mécanisme de redéfinition de méthodes lié à l'héritage (on parlent de polymorphisme d'héritage).

  • Abstraction

    L'abstraction est un moyen de fournir un ensemble de contrats (des opérations sans corps, de simples interfaces d'opérations) qui devra être réalisé par des classes d'implémentation.

    Comment ? en concevant une interface.

    Une classe qui ne donnerait pas un corps à toutes ses méthodes est qualifiée d'abstraite (abstract), car elle est "incomplète".

    Une classe qui ne contient aucun attribut d'instance (non static), qui n'implémente aucune méthode et ne dispose pas de constructeur est qualifiée de classe purement abstraite.

    On ne peut pas directement créer des objets d'une classe abstraite ou d'une interface.

  • Héritage

    L'héritage permet de réutiliser une implémentation en redéfinissant certaines méthodes.

    Comment ? en concevant une classe qui hérite d'une autre classe.

10.2. Héritage

Toute classe en Java est construite sur le même modèle, défini par autre classe, dite racine. Cette classe s'appelle Object, une convention dans les langages objet. En terme d'héritage l'autoréférence n'est pas permise, la classe racine n'a donc pas de modèle (elle n'a pas de modèle, elles est le modèle).

Prenons un exemple : la classe Jouet et un programme de test :

public class TestHeritageJouet {
 static public void main(String[] args) {
   Jouet j = new Jouet("Echec truc", 45, "verre", "noir");
   System.out.println(j.toString());
 }
}

Maintenant compilons et exécutons le programme :

$ javac TestHeritageJouet.java

Ho!... le programme compile alors que je n'ai pas défini de méthode toString dans la classe Jouet !???

Voyons quel est le comportement de cette méthode dont j'ignore l'existence.

$ java TestHeritageJouet
Jouet@f5da06
$

Ah...

Comment se fait-il que l'appel de la méthode toString() soit acceptée ? Tout simplement parce que la classe Jouet hérite de la classe Object et que la méthode toString est définie dans cette classe. En fait écrire :

public class Jouet {
  ... 
}

Equivaut à :

public class Jouet extends Object {
  ... // comme avant
}

Donc, ne pas (re)définir dans la classe Jouet la méthode toString, c'est accepter son implémentation par défaut dans Object.

10.3. Vocabulaire et définition

[Note]Une définition de l'héritage [Bertrand Meyer]

L'héritage est un mécanisme permettant de définir une classe par rapport à d'autres en ajoutant toutes leurs caractéristiques aux siennes.

Certains langages (comme C++, Eiffel) autorise la définition d'une classe à partir de plusieurs autres classes. Ceci s'appelle de l'héritage multiple. Bien que parfois très pratique cette possibilité peut poser problème lorsque des caractéristiques de classes héritées portent le même nom et dans le cas d'héritage répété.

Figure 10. Héritage multiple

Héritage multiple

=> Java supporte l'héritage multiple par l'intermédiaire des interfaces uniquement.

[Note]Une définition de l'héritage simple

On appelle héritage simple, une forme restreinte d'héritage par laquelle une classe ne peut avoir plus d'un parent (une seule super-classe directe).

=> Java ne reconnait que l'héritage simple entre classes.

UML représente cette relation orientée selon la spécialisation tout en la nommant généralisation... Exemple :

Figure 11. Représentation de l'héritage avec UML

Représentation de l'héritage avec UML

La classe enfant pointe vers ses parents (ancêtres immédiats). Le trait est plein et la pointe de la flèche forme un triangle non rempli.

Ce diagramme montre que Jouet hérite des méthodes equals et toString de Object (vue partielle).

Nous pouvons percevoir la relation classeObject - classesHéritant (par exemple Object - Jouet) comme une relation orientée selon deux points de vue : généralisation et spécialisation.

  • Généralisation : la classe Object factorise les propriétés communes à toutes ses sous-classes. Termes fréquemment employés :

    • Object généralise Jouet

    • Object est un parent de Jouet

    • Object est un ancêtre de Jouet

    • Object est une super-classe de Jouet

  • Spécialisation : la classe Jouet spécialise la classe Object. Termes fréquemment employés :

    • Jouet hérite de Object

    • Jouet est un enfant de Object

    • Jouet est un descendant de Object

    • Jouet est une sous-classe de Object

UML préconise l'emploi des couples ancêtre/descendant et/ou parent/enfant (UML 1.3 p.94). Les ancêtres d'une classe sont ses parents et leurs ancêtres. Les descendants d'une classe sont ses enfants et leurs descendants.

10.4. Héritage sous contrôle

10.4.1. Contrôle côté parent - exposer ses caractéristiques

Les propriété déclarées privé (private), méthodes comme attributs, ne sont connues ni des classes descendantes ni des classes utilisatrices. Par contre les propriétés protégées (protected) sont connues des sous-classes éventuelles, mais pas des classes 'utilisatrices' des objets.

Figure 12. Visibilité des propriétés

Visibilité des propriétés

10.4.2. Contrôle côté enfants - que faire de son héritage ?

Prenons un exemple simple :

class Client { 
  private String nom; 
  private String prenom; 

  // accesseurs 
  ... 
  public String toString() {. . . }
}

et intéressons-nous aux comportements possibles de cette classe vis à vis de la méthode toString qu'elle hérite d'Object.

On dénombre cinq comportements clés répartis en trois catégories : Conservateur, Réformateur, Nihiliste.

  1. Conservateur : Respecte la tradition. Maintient la tradition en conservant le comportement hérité. Deux possibilités :

    1. Conserver l'existant sans rien modifier.

      => on reprend à la lettre le comportement hérité : c'est le comportement par défaut des langages objets. Exemple :

      public String toString() { return super.toString();  }
       // Un simple appel à la méthode héritée. Adhère au comportement du parent.
       // C'est le comportement par défaut (en absence de redéfinition).
      
    2. Conserver l'existant en ajoutant une touche personnelle.

      => on hérite puis on ajoute des instructions.

      public String toString() { return super.toString() + this.nom;  }
       // Appelle la méthode héritée et ajoute une note personnelle.
      
  2. Réformateur : Renouvelle la tradition : Va de la simple transformation à la métamorphose.

    1. Modéré : On tente de conserver le sens.

      => mais on l'exprime par du code entièrement nouveau (pas d'appel à super.methodeX()).

      public String toString() { 
        return getClass().getName() + " : " this.nom +" " + this.prenom;
      } // Redéfinit la méthode héritée dans l'esprit attendu.
      
      
    2. Radical : On ne conserve pas le sens.

      => On raconte autre chose. Attention le problème de sens peut révéler une mauvaise de conception.

      public String toString() { 
        return "Je passe incognito et je le dis...";
      } // Redéfinit la méthode héritée dans un autre esprit.
      
  3. Nihiliste (du latin nihil <<rien>>) Gomme la tradition. Supprime toutes traces des caractéristiques comportementales héritées.

    1. On remplace la partie héritée par une expression du vide.

      => Attention, peut révéler un problème de conception.

      public String toString() { /* je me tais */ return ""; } 
       // Décide de ne rien faire et rend donc une chaîne vide.  
      

Les comportement les plus usuels sont : Conservateur et Réformateur conservant le sens hérité. Tout autre choix devrait être justifier sous forme de commentaire dans le code.

10.5. Héritage et constructeurs

Comme pour les méthodes héritées, par défaut le constructeur d'une classe hérite du comportement du constructeur par défaut. Si vous redéfinissez un ou plusieurs constructeurs (ce qui est souvent le cas), ces constructeurs devraient appeler le constructeur hérité en premier. Exemple, nous souhaitons étendre la notion d'adresse physique à celle d'adresse Web :

Figure 13. Une adresse Web est une sorte d'adresse

Une adresse Web est une sorte d'adresse

Voici le code source d'Adresse :

public class Adresse {
  /** Attributs */
  private String rue;
  private String ville;
  private String codePostal;
  /** Associations */

  /** Constructeur */ 
  public Adresse() {

  }

  public Adresse(String rue, String ville, String cp) {
    this.rue=rue;
    this.ville=ville;
    this.codePostal=cp;
  }

  public String getRue() {
    return this.rue;
  }
  public void setRue(String rue){
    this.rue=rue;
  }

  public String getVille() {
    return this.ville;
  } 
  public void setVille(String ville){
    this.ville=ville;
  }

  public String getCodePostal() {
    return this.codePostal;
  }
  public void setCodePostal(String cp){
    this.codePostal=cp;
  }

  public String toString(){
    String res="";
    res+="Rue : "    + this.rue;
    res+=", Ville : " + this.ville;
    res+=", Code postal : "  + this.codePostal; 
    return res;
  }
}

Un exemple d'adresseWeb


public class AdresseWeb extends Adresse {
  /** Attributs */
  private String mel;
  private String url;

  /** Constructeur */ 

  public AdresseWeb(String rue, String ville, String cp, String mel, String url) {
    super(rue, ville, cp);
    this.mel=mel;
    this.url=url;
  }

  public String getMel() {
    return this.mel;
  }
  public void setMel(String mel){
    this.mel=mel;
  }

  public String getUrl() {
    return this.url;
  } 
  public void setUrl(String url){
    this.url=url;
  }

  public String toString(){
    String res="";
    res+=", Mel : "    + this.mel;
    res+=", URL : " + this.url;
    return super.toString() + res;
  }
}

Le constructeur de la sous-classe appelle le constructeur de sa super classe en lui passant les valeurs des arguments adéquates :

 public AdresseWeb(String rue, String ville, String cp, String mel, String url) {
    super(rue, ville, cp);
    this.mel=mel;
    this.url=url;
  }

On remarquera que, dans le constructeur, le mot clé super n'est pas suivi du nom du constructeur hérité (contrairement aux méthodes).

Exemple de construction d'objets :

public class TestAdresse {
  static public void main(String[] args){ 
    Adresse a1 = new Adresse("1 allée de la noix", "Grenoble", "38000");
    Adresse a2 = 
      new AdresseWeb("3 rue moulin", "Melun", "77000",
                     "titi@labas.org","http://www.linux-france.org");
    System.out.println(a1);
    System.out.println(a2);
  }
}

Notez que nous aurions pu également déclarer la variable a2 de type AdresseWeb.

10.6. Un zoo

Pour illustrer les concepts objets, nous représenterons un zoo et les animaux qui l'habitent. On souhaite pouvoir interroger le nom d'un animal et de lui demander de dormir.

Figure 14. Diagramme de classes : un zoo est composé d'animaux

Diagramme de classes : un zoo est composé d'animaux

Comment représenter cette classification dans un langage objet comme Java ?

Déclarons une interface (une classe abstraite pure) :

public interface Animal {

    public String getNom();   
    public void dormir();

 } 

Puis les différentes classes d'animaux (Tigre, Lion et Ours) qui implémentent cette interface.

Classe Lion :

public class Lion implements Animal {
   //attribut
   private String nom;

   //constructeur   
   public Lion (String nom) {
      this.nom = nom;
   }

   @Override
   public String getNom() {
     return this.nom;
   }

   @Override
   public void dormir() {
     System.out.println("Moi, " +this.nom + ", je dors sur le ventre.");
   }
 } 

Classe Tigre :

public class Tigre implements Animal {
   //attribut
   private String nom;

   // constructeur   
   public Tigre (String nom) {
      this.nom = nom;
   }

   @Override
   public String getNom() {
     return this.nom;
   }

   @Override
   public void dormir() {
     System.out.println("Moi, " +this.nom + ", je dors sur le dos.");
   }
 } 

Classe Ours :

public class Ours implements Animal {
   //attribut
   private String nom;

   //constructeur
   public Ours (String nom) {
      this.nom = nom;
   }

   @Override
   public String getNom() { 
     return this.nom; 
   }

   @Override
   public void dormir() {
     System.out.println("Moi, " +this.nom + ", je dors dans un arbre.");
   }
 } 

Déclarons le zoo : Il est constitué d'une collection d'objets (ce sera des objets de type Animal bien-sûr), son seul attribut, d'un constructeur qui crée des animaux (une façon de ne pas construire un zoo vide), et d'une méthode demandant à chacun des animaux du zoo de s'endormir.

package org.vincimelun.zoo;
import java.util.*;

public class Zoo {
 private ArrayList<Animal> animaux; // collection d'animaux

 public Zoo(){
    // création de la collection
    this.animaux = new ArrayList<Animal>();
      // List est une interface (classe abstraite)
      // on ne peut donc pas créer directement
      // un objet de ce type. Vector ou ArrayList sont
      // des classes qui réalisent List. Ok.
      // A ce niveau, la liste est crée, mais vide.
 }

  /**
  *  Endormir tous les animaux du zoo
  */
  public void endormirLesAnimaux() {
    Animal animal;
    // déclaration d'une variable locale.

    // les indices d'une liste commencent à zéro.
 
    for (int i=0; i < this.animaux.size(); i++) {
      animal = animaux.get(i);
      animal.dormir();
    }

  }

  /** Permet d'ajouter un animal au zoo
  *  @param animal l'animal a ajouter
  */
  public void ajouteAnimal(Animal animal) {
    this.animaux.add(animal);
  }

 }


Créons une application qui crée un zoo avec quelques animaux, et endort ses animaux :

package org.vincimelun.zoo;

import java.util.Scanner;

public class GestionZoo {
 //attribut
 private Zoo zoo;

 //constructeur
 public GestionZoo() {
   this.zoo = new Zoo();
   zoo.ajouteAnimal(new Lion("seigneur"));
   zoo.ajouteAnimal(new Tigre("gros chat"));
   zoo.ajouteAnimal(new Ours("balou"));
   zoo.ajouteAnimal(new Tigre("sherkan"));
 }

 //méthode
 public void run() {
    Scanner clavier = new Scanner(System.in);
    String choix;
    do {
      System.out.println("0=Quitter, 1=Endormir : ");
      choix = clavier.next();
      if ("1".equals(choix)) {
         zoo.endormirLesAnimaux();
      }
    } while (!"0".equals(choix));     
  }//run

 public static void main(String[] args) {
    GestionZoo app = new GestionZoo();
    app.run();
 }
}

Exemple d'exécution

$ java GestionZoo
0=Quitter, 1=Endormir : 1
Moi, seigneur, je dors sur le ventre.
Moi, gros chat, je dors sur le dos.
Moi, balou, je dors dans un arbre.
Moi, sherkan, je dors sur le dos.
0=Quitter, 1=Endormir : 0
$

La classe Zoo ne dépend pas de classes d'implémentation d' Animal , elle dépend d'une abstraction (seulement de l'interface Animal ).

Nous avons pu ainsi nous concentrer, dans la classe Zoo , essentiellement sur des traitements génériques, comme endormir les animaux. De nouvelles espèces d'animaux pourront être conçues à posteriori , tout en profitant de traitements génériques développés bien avant elles !

Avant de pratiquer les exercices qui vous permettront de vérifier cela par vous même, nous nous arrêtons la notion de static.

10.7. static : lorsque la classe se prend pour un objet

Le qualificatif static déonte une approche non objet, car une propriété static n'est pas associé à une instance (un objet) mais à une classe.

Le caractère static d'un propriété (un attribut ou une méthode essentiellement) dénote une propriété directement liée au module de classe dans lequel elle est définie.

Dans ce contexte, une classe joue le rôle de static dans le sens librairie utilitaire. Dit autrement, une propriété static n'est pas liée à une instance, et c'est pourquoi il est toujours préférable de la préfixer explicitement par le nom de sa classe, afoin de souligner son caractère static. Exemple :

...

double x = Math.PI * 2;
double y = -x;
...
double z = Math.abs(x);

...
 

PI est un attribut de classe (pas un attribut d'instance !), tout comme abs est une fonction utilitaire du module java.lang.Math (le package java.lang étant le package par défaut, inutile de le déclarer).

La documentation de l'API nous confirme que ces propriétés sont bien libéllées static :

Figure 15. Exemple de propriétés liées à une classe (static)

Exemple de propriétés liées à une classe (static)

Les propriétés static sont ordinairement initialisées par des instructions placées dans un bloc static, équivalent des construteurs d'objet. Les blocs static sont exécutés au chargement de la classe.

En mémoire il n'y a au plus qu'une "instance" d'un module de classe et 0 à n instances de cette classe.

10.8. Exercices

Pour ajouter une nouvelle espèce, il suffit de concevoir une sous-classe de Animal et de définir sa méthode dormir() et getNom() , sans avoir besoin de modifier la méthode endormirLesAnimaux() .

Voici les fichiers : zoo.zip.

  1. Après avoir placer ce fichier (zoo.zip) à la racine de l'espace de travail d'Eclipse, à partir de ce dernier, importer le via import.projet existing into workspace/compressed. Ou alors créer un répertoire de projet et copier les fichiers sources à l'intérieur.

  2. Ajouter une nouvelle espèce, comme indiqué ci-dessus.

  3. Instancier la nouvelle classe (créer un objet) dans la méthode run de GestionZoo , en donnant un nom à l'animal, et ajouter votre animal aux animaux du zoo.

  4. Compiler et exécuter le programme.

  5. Ajouter une méthode nommée exprimeToi() dans l'interface Animal que vous testez dans la classe Zoo .

    Cette méthode affichera le cri de l'animal, par exemple "miaou..", pour un gros minet. Vous modifierez les classes implémentant l'interface en conséquence.

  6. On souhaite ajouter une nouvelle espèce : OursPolaire, qui, comme son nom l'indique, représente la classe des Ours Polaire. Un ours polaire est une sorte de Ours. C'est relation est typiquement une relation d'héritage. Notre ours polaire héritera donc du comportement des Ours, mais devra redéfinir au moins un comportement : il n'y a pas (ou trop peu) d'arbre en arctique... la banquise est son lit, et au zoo il se contentera d'un rocher :-(. un ours sur un rocher

  7. On souhaite que l'invocation de la méthode toString d'un animal quelconque rende l'information suivante : nom de sa classe suivi du nom de l'animal. Proposez une solution que vous testez dans Zoo.

  8. Ajouter des traitements de gestion suivant : Affichage du nombre d'animaux du zoo, ajout d'un animal au zoo (l'utilisateur donne la classe et le nom de l'animal), suppression d'un animal du zoo (l'utilisateur donne son nom). Pour cela vous ajouterez les méthodes nbAnimaux() , deleteAnimal(String nom) dans la classe Zoo , et que vous testerez dans la méthode run de GestionZoo .

Bonne programmation !

11. Exercices d'entrainement

11.1. Objectifs

Travail de synthèse. Vous êtes amené à concevoir une hiérarchie de classes d'après un cahier des charges. Vous tesez votre solution en instanciant des objets et en les placant dans une liste. Le programme réalise ces tâches en interaction avec l'utilisateur.

Dans un second temps (jeu du pendu), vous finalisez un programme (première étape) que vous faites évoluer (seconde étape). Vous devez pour cela rechercher par vous mêmes dans l'API, les possibilité offertes par les objets techniques proposés (Random et StringBuffer).

11.2. Conception d'une hiérarchie de classes et gestion d'une liste d'objets

On souhaite simuler une gestion de parc de véhicules d'une société de BTP. Un véhicule est caractérisé par un numéro d'immatriculation, une date de mise en circulation, un kilométrage, un modèle et une marque.

Parmi les véhicules on distingue les véhicules utilitaires (ayant un poids total à vide, une charge maximale) des véhicules des techniciens commerciaux (attitré à un commercial).

On ne cherchera pas à concevoir une interface, mais une superclasse qui factorise les caractéristiques des véhicules utilitaires et commerciaux.

  1. Proposez une classification que vous illustrez avec un diagramme de classes UML.

  2. Implémentez ces classes en Java. Pour représenter la date de mise en circulation, on utilisera le type java.sql.Date. Cette classe propose une fonction utilitaire permettant de convertir une date en chaîne de caractères en un objet de type java.sql.Date : voir en ligne l'API java à ce sujet.

  3. Réalisez un programme offrant à l'utilisateur la possibilité de créer un véhicule utilitaire, de créer un véhicule attitré à un commercial, de lister l'ensemble des véhicules du parc. Vous réalisez cela le plus simplement possible dans la fonction main d'une classe de tests.

11.3. Poursuite d'un implémentation, utilisation en ligne de l'API Java

On veut programmer le jeu du pendu en java. Pour cela on désire concevoir une classe JeuPendu qui réalise la logique applicative du jeu. Une première analyse nous a permis d'identifier les opérations suivantes :

  • public void chargerMots(); charge les mots d'un dictionnaire dans une liste.

  • public void tirerUnMotAuHasard(); tire un mot de façon 'aléatoire' dans la liste.

  • public boolean leMotEstDecouvert(); rend vrai si le mot à découvrir est découvert, rend faux sinon.

  • public boolean analyser(char c); regarde si le caractère reçu en argument fait partie des caractères du mot secret. Si c'est le cas, toutes les occurences de ce caractère sont découvertes dans le mot crypté puis rend true, sinon ne fait rien et rend false.

  • public String getLeMotCrypte(); retourne le mot crypté

  • public String getLeMotSecret(); retourne le mot secret en clair.

Voici une première implémentation, à compléter :


package org.vincimelun.pendu;

import java.util.*;
import java.io.*;

public class JeuPendu {

  // attributs privés
  /** le mot que l'utilisateur doit découvrir */
  private String leMotSecret;

  /** le mot secret pour l'affichage.
  * StringBuffer : un type String modifiable
  */
  private StringBuffer leMotCrypte;
 
  /** pour représenter en mémoire la liste des mots */
  private List lesMots;

  // constructeur
  public JeuPendu() throws java.io.IOException {
     lesMots = new ArrayList();
     chargerMots();
     tirerUnMotAuHasard();
  }

  // méthodes

  public void chargerMots() {
    ChargeurDeMots chargeur = new ChargeurBidon();  
      lesMots.add("programmation");
      lesMots.add("java");
      lesMots.add("informatique");
      lesMots.add("langage");
  }

  /**
  * tire "au hasard" un mot de la liste des mots
  * et l'affecte à this.leMotSecret puis
  * valorise this.leMotSecret en conséquence 
  * (de même longueur que leMotSecret mais formé avec des '#')
  */
  public void tirerUnMotAuHasard() {
    Random alea = new Random();
    // TODO
  }

  /**
  * rend true si leMotCrypté est découvert, 
  * rend false sinon
  */
  public boolean leMotEstDecouvert() {
    return false;
    // TODO
  }

  /**
  * regarde si le caractère reçu en argument fait partie
  * des caractères du mot secret. Si c'est le cas, toutes
  * les occurences de ce caractère sont découvertes dans
  * le mot crypté ET la fonction rend true, sinon false.
  */
  public boolean analyser(char c) {
     boolean res = false;

    // TODO

     return res;
  }

  /** 
  * retourne le mot crypté.
  */ 
  public String getLeMotCrypte() {
     return "####";
  }

  /**
  * retourne le mot secret.
  */
  public String getLeMotSecret() {
     return "TODO";
  }
}

Quelques commentaires

  • Les attributs :

    • leMotSecret correspond au mot à découvrir.

    • leMotCrypte de type StringBuffer. Ce mot représente la vue utilisateur du mot à découvrir, il est initialement composé de # et sera découvert progressivement au cours de la phase du jeu. On utilise ici un StringBuffer car on aura besoin de modifier son contenu lorsque l'on découvrira certains caractères, ce que ne permet pas les objets de la classe String - non mutable . On utilisera pour cela la méthode setCharAt(indice, nouveau_car) qui permet de modifier un caractère de la chaîne à l'indice spécifié.

    • lesMots : une liste de type ArrayList. Attention, comme c'est une structure de liste générique, elle travaille avec des objets de base, de type Object. Pour retirer un élément de type String, on utilisera une conversion de type : par exemple :

                String unMot = (String) lesMots.get(O);
             

      Extrait le premier objet de la liste (indice zéro), et le cast en String, sous-classe de Object.

  • Les opérations (méthodes) :

    • chargerMots : place quelques mots dans une liste (ArrayList) appelée lesMots.

    • tirerUnMotAuHasard(): utilise une objet de la classe Random, dans le but d'appeler sa méthode nextInt( un entier ) qui devra retourner un indice valide.

      Il suffit alors de "retirer" le mot de la liste : this.leMotSecret = (String) lesMots.get(indice); et à formater le mot crypter avec des # (voir méthode replaceAll de la classe String).

    • leMotEstDecouvert : rend vrai si aucun '#' ne figure parmi les caractères du leMotCrypte.

    • analyser(c : caractère) : si le caractère c est inclus dans le mot à découvrir (leMotSecret), montre alors "en clair" ce caractère dans leMotCrypte et rend true, ne fait rien et rend false sinon.

    • getLeMotCrypte : rend le mot crypte. Attention, cette méthode doit retourner du String, il faudra donc transformer le StringBuffer en String en appelant la méthode toString() de l'objet this.leMotCrypte.

Voici un exemple de programme principal :


public class JeuPenduTextUI {

  static public void main(String[] args){ 
   
    JeuPendu jeuPendu = new JeuPendu();

    while (!jeuPendu.leMotEstDecouvert()) {
       String choix =
         Console.lireClavier("Entrez un caractère suivi de ENTER : ");
       jeuPendu.analyser(choix.charAt(0));
       System.out.println(jeuPendu.getLeMotCrypte());
    }
  }
}

A FAIRE

  1. Implémenter la classe JeuPendu (voir les TODO). Vous utiliserez l'aide en ligne de l'API java 1.4.2 pour prendre connaissance des méthodes de StringBuffer et Random.

  2. Permettre à l'utilisateur de quitter la partie en cours de jeu. Dans ce cas, le programme affichera le mot qui n'a pas été découvert.

  3. Modifier la règle du jeu (JeuPenduTextUI.java) de sorte à ce que le joueur ne dispose que d'un nombre limité d'essais infructueux.

  4. Pour les impatients, voici une version de chargeMots qui charge dans la liste des mots, le contenu d'un fichier texte (motsPendu.txt) composé de plus de 1500 mots (un mot par ligne) :

      public void chargerMots() {
        try {
          BufferedReader in
            = new BufferedReader(new FileReader("motsPendu.txt"));
     
          String mot = in.readLine();
          while (mot!=null) {
            lesMots.add(mot);
              // ajoute le mot dans la liste
            mot = in.readLine();
              // lire la ligne (le mot) suivant dans le fichier
              // à la fin du fichier => mot == null
          }
          in.close();
            // fermeture du fichier
        }
        catch(IOException e) {
          // le fichier n'a pas pu être ouvert, on insère quelques mots
          lesMots.add("programmation");
          lesMots.add("java");
          lesMots.add("informatique");
          lesMots.add("langage");
        }
      }
    

12. Exercices de révision

Extraits de sujets d'examens 2000-2002 - STS IG lycée Léonard de Vinci 77000 Melun -

12.1. Question I - Truc : Valeur par défaut et constructeur (7 points)

Soit la classe Truc

class Truc {
   private String nom;
   private double prix;

   public void setNom(String nom) {
     this.nom=nom;
   }
   public void setPrix(double prix) {
     this.prix=prix;
   }
}

Et le programme suivant :

public class Test {
  static public void main(String[] args) {
    Truc t1 = new Truc();
    // dans quel état est le truc
    // référence par t1 ? ===>  REPONSE A
    t1.setNom("bidule");
    t1.setPrix(12.3);
    // dans quel état est le truc t1 ? ===> REPONSE B
  }
}

Figure 16. Répondre aux questions A et B en complétant la figure ci-dessous (2 points)

Répondre aux questions A et B en complétant la figure ci-dessous (2 points)

Justifier vos réponses ( 2 points )

Comment faire pour initialiser un objet à sa création ? Fournir un exemple ( 3 points )

=>une solution ici : exemple de réponses aux questions

12.2. Question II - Voiture : Activité de conception (4 points)

On vous demande de concevoir une classe nommée Voiture permettant au programme ci-dessous de fonctionner.

class Application {

  public void run() {

     Voiture v = new Voiture("Titine", 7);
       // création d'une voiture nommée "Titine" de 7 chevaux
     System.out.println(v.donneVitesse());
     // affiche : 0

     v.passeVitesseSuperieure();
       // passe la première vitesse

     v.passeVitesseSuperieure();
       // passe la seconde vitesse

     v.passeVitesseSuperieure();
       // passe la troisième vitesse

     v.retrograde();
       // retourne en deuxième

     System.out.println(v.donneVitesse());
       // affiche : 2

     System.out.println(v);
       // affiche : Titine, 7 chevaux
  }
  static public void main(String[] args){
     Application app = new Application();
     app.run();
  }
}

Remarque : Ne concevez que le strict minimum (attributs, constructeur et méthodes) nécessaire au fonctionnement du programme Application .

12.3. Question III - Voiture : Paramétrage du nombre de vitesses de la boîte ( 5 points )

On souhaite ajouter un attribut maxVitesse , correspondant au nombre de vitesses disponibles pour une voiture donnée (par exemple 5).

On vous demande d'aménager la classe Voiture en 2 étapes :

  • Déclarer l'attribut et modifier le constructeur en conséquence. ( 2 points )

  • Faire en sorte que les méthodes passeVitesseSuperieure() et retrograde() laissent toujours la voiture dans un état cohérent (pas de vitesse impossible). ( 3 points )

12.4. Question IV - Voiture : Ajout d'une méthode, et test unitaire ( 4 points )

On vous demande d'ajouter à la classe Voiture :

  • Une méthode nommée estPointMort qui rend la valeur booléenne vrai si la vitesse en cours est nulle (valeur zéro) et faux dans le cas contraire. ( 1 point )

Afin de vérifier le bon fonctionnement de la méthode estPointMort() , on vous demande de compléter la classe TestVoiture , et plus particulièrement de :

  • Concevoir la méthode testEstPointMort() . Cette méthode devra créer une voiture, puis tester la méthode estPointMort en changeant plusieurs fois l'état de l'objet.

    La méthode testEstPointMort affichera OK si la valeur de la méthode est conforme à celle que vous attendez, ERREUR dans le cas contraire.

    Remarque 1 : les valeurs booléennes de Java sont représentées par les littéraux true et false .

    Remarque 2 : vous serez évalué sur la qualité de votre test, qui devra comporter au moins 3 appels de la fonction estPointMort . ( 3 points )

    public class TestVoiture {
      /**
      * Test la méthode donneVitesseSuperieure
     */
      public void testDonneVitesseSuperieure() {
         Voiture v = new Voiture("Deuche", 2);
         v.passeVitesseSuperieure();
         if (v.donneVitesse() == 1) {
           System.out.println("OK")
         }
         else {
           System.out.println("ERREUR")
         }
    
         etc.
    
    
    
    
    
    
    
      }
      
    
    /**
      * Test la méthode estPointMort :: A FAIRE ::
      */
      public void testEstPointMort() {
         Voiture v;
      
    
    
    
    
       
    
      }
    
      static public void main(String[] args){
         TestVoiture test = new TestVoiture();
         test.testEstPointMort();
      }
    }
    

=>une solution ici : Voiture.java

13. Programmer des interfaces graphiques

13.1. Intro

Aujourd'hui, il est courant qu'une application de gestion fournisse une interface graphique à ses utilisateurs. Il existe deux types d'interface graphique utilisateur IGU, plus connues sous le sigle GUI pour Graphic User Interface : interface Web et interface de type window. L'une est orientée page et l'autre fenêtre. Ces deux types d'interface ont en commun les caractéristiques suivantes :

  • Sensible aux événements externes et internes

  • Construites avec une technologie objet

  • Permettent plusieurs vecteurs de communication (image, son, texte, couleur, ...)

Avec Java le développeur dispose en standard de deux catégories d'outils (en dehors de HTML) pour construire des interfaces, ce sont awt et swing .

  • AWT Abstract Window Toolkit

    Avantages

    • Gestion des événements efficace depuis Java 1.1.

    • Plus rapide que Swing.

    • Compatible avec les Navigateurs (Netscape et IE dans leur version >= 4 supportent les applets composées avec Java 1.1.)

    Inconvénients

    • API fortement lié aux ressources du SE hôte.

    • Bibliothèque de composants moins riche que Swing.

    Utilisation

    • Applet (application Java embarquée dans un navigateur).

    • Application autonome avec GUI simple.

  • SWING Permet de construire des GUI de haut niveau. Basé sur AWT et sa gestion des événements.

    Avantages

    • Richesse des éléments d'interface.

    • API d'accessibilité.

    • Choix du "look and feel".

    • Dessin 2D/3D.

    • Gestion des événements sophistiquée.

    Inconvénients

    • Plus lent que l'AWT

    Utilisation

    • Application autonome, client/serveur.

    • Application intranet et, dans une moindre mesure, internet (applet).

Il existe une alternative à AWT et Swing, c'est un projet initié par IBM, qui prend le meilleur de ces deux technologies : SWT ( Standard Widget Toolkit ) - voir http://www.eclipse.org/swt/ ). Nous nous concentrons ici sur Swing , le plus utilisé du moment (2003).

La construction d'une interface graphique utilisateur (GUI) nécessite environ 6 étapes

  1. Concevoir une rapide maquette à la main, sur papier par exemple, afin de déterminer les principaux éléments de l'interface.

  2. Concevoir une classe puis déclarer, créer et configurer les composants (couleur, texte...).

  3. Placer les composants dans leur conteneur, puis dans le conteneur principal de la fenêtre.

  4. Gérer le positionnement des composants dans leur conteneur (un panneau le plus souvent).

  5. Associer aux composants générateurs d'événements un gestionnaire d'événements (le plus souvent la fenêtre elle-même).

  6. Coder la logique événementielle de traitement.

Une application fenêtrée comporte généralement un menu. Nous vous proposons de construire une fenêtre d'application disposant d'un menu, que vous étendrez en exercice.

13.2. Étape 1 : La maquette papier

Nous choisissons de concevoir une fenêtre avec une barre de menus, munie d'une commande permettant de quitter l'application.

Figure 17. Maquette de l'application exemple

Maquette de l'application exemple

13.3. Étape 2 : Conception de la fenêtre et de ses composants

JFrame est une classe de la bibliothèque javax.swing qui représente une fenêtre graphique.

L'idée est de spécialiser la classe de base JFrame en une classe qui contient les composants dont vous avez besoin. Par exemple, nous désirons concevoir une fenêtre ayant une barre de menus.

Figure 18. Réutilisation par héritage

Réutilisation par héritage

Nous ajoutons un constructeur et une méthode d'initialisation. Voici une traduction partielle en Java :

import javax.swing.*;  public class FenetreMenu extends JFrame {
  // attributs 
  ...
  // constructeur 
  public FenetreMenu() {
    // appel un constructeur de son parent, en passant 
    // une valeur de type chaîne de caractères  
    super ("Fenetre avec une barre de menus");
    // effet : donne un titre à la fenêtre 

    // permettre de quitter l'application lorsque l'utilisateur 
    // clique sur la croix en haut à droite de la fenêtre. 
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 

    this.init(); 
      // voir plus bas pour des explications concernant cette méthode. 
  }
  ...
} 

On remarquera le mot clé super , qui permet d'appeler un des constructeurs de la classe parent (rappel : un constructeur n'est pas une méthode).

Nous devons maintenant créer et configurer les composants.

Nous pouvons réaliser cela soit directement dans le constructeur, soit dans une méthode d'initialisation spécialement conçue à cet effet ( init ) et appelée dans le corps du constructeur. Afin de ne pas alourdir le constructeur, nous créons la barre de menus et ses composants dans la méthode init . Extrait :

 private void init(){
   //on a besoin de créer une barre de menus
   JMenuBar menuBar;
   // et un menu
   JMenu menuFichier;
    
   //création de la barre de menus
   menuBar = new JMenuBar();

   //construisons le premier menu
   menuFichier = new JMenu("Fichier");
   menuFichier.setMnemonic(KeyEvent.VK_F);

   // création de la commande "quitter" (un JMenuItem)
   JMenuItem mnItemQuitter = new JMenuItem("Quitter",
                       KeyEvent.VK_Q);

   mnItemQuitter.setActionCommand("Quitter");

   ...

   }

Nous avons choisi d'utiliser un constructeur JMenuItem permettant de spécifier une touche d'accès rapide (un mnemonic ) spécifiée en second paramètre. Vous noterez que la valeur du paramètre est une constante static de la classe KeyEvent , VK voulant dire Virtual Key .

Nous spécifions le nom de l' "action" auquel le composant sera lié . C'est une simple information ( String ) que le composant transmet à ses "écouteurs" lorsqu'il est activé (ce point est détaillé plus bas dans ce document).

Remarque : L'aide en ligne est la principale ressource permettant de connaître les possibilités de configuration offertes par un composant.

13.4. Étape 3 : Placer les composants dans un conteneur

La barre de menus contient des menus, et les menus contiennent des commandes ( menu item ).

Ces relations sont sous la responsabilité du programmeur. Comme on peut s'y attendre, les conteneurs disposent d'une méthode nommée add permettant l'ajout de composants.

private void init() { 
   ...
   // le menu Fichier contient la commande Quitter 
   menuFichier. add (mnItemQuitter);

   // on peut placer une barre de séparation pour 
   // séparer des groupes logiques de commandes. 
   // menu.addSeparator(); 
   
   // la barre de menus contient le menu Fichier 
   menuBar. add (menuFichier);
   // plus un autre menu bidon, pour l'exemple 
   menuBar. add (new JMenu("Un autre menu"));

   ...
 } 

13.5. Étape 4 : Gérer la position du composant dans la vue

Deux façons de faire :

  1. Gérer soi-même la position en x, y du composant.

  2. Sous-traiter le positionnement du composant par un gestionnaire de positionnement ( layout ).

    Java propose en standard quelques gestionnaires, qui seront étudiés prochainement.

En ce qui concerne la barre de menu, il existe exceptionnellement une méthode, nommée setJMenuBar , dédiée à son placement dans la fenêtre (une barre de menus est traditionnellement située sous la barre de titre) :

private void init() {
   ...
  // fournir à la fenêtre une barre de menus 
  this. setJMenuBar (menuBar);
  ...

  // donnons une largeur et une hauteur à la fenêtre 
  this.setSize(300,200);
} 

Ce qui donne :

Figure 19. FenetreMenu à l'exécution

FenetreMenu à l'exécution

Une fois notre composant créé (avec new ), placé et positionné dans la vue, il ne nous reste qu'à nous occuper de la gestion de ses événements.

13.6. Étape 5 : Associer un gestionnaire d'événement

Bon nombre de composants sont générateurs d'événements. Par exemple, lorsque l'utilisateur clique sur un bouton, celui-ci est capable de prévenir tout autre objet qui l'écoute. Pour qu'un objet puisse être prévenu de l'événement, il doit s'abonner au composant générateur de l'événement .

La fenêtre s'abonne à la commande du menu de cette façon :

mnItemQuitter. addActionListener (this); 

this fait référence à la fenêtre en cours (celle qui sera en exécution). Par cette instruction, on abonne la fenêtre en tant qu' écouteur ( listener ) des événements générés par mnItemQuitter .

Pour que le JMenuItem accepte la fenêtre en tant que gestionnaire de ses événements, la fenêtre doit être une sorte de ActionListener (un écouteur d'événement). Pour cela la fenêtre devra implémenter l' interface ActionListener .

public class FenetreMenu 
  extends JFrame implements ActionListener {
    ... 
}

... et donc donner corps à la méthode :

public void actionPerformed(ActionEvent evt) 

... seule opération déclarée par l'interface ActionListener . C'est l'objet de l'ultime étape.

13.7. Étape 6 : Coder la logique de traitement

C'est dans cette dernière étape que le développeur justifie pleinement son rôle...

Il doit programmer le comportement de l'application lorsqu'un événement survient.

Par exemple, ici l'utilisateur souhaite mettre fin à l'application.

...
public void actionPerformed(ActionEvent evt) {
  String action = evt.getActionCommand(); 
  if (action.equals("Quitter")) {
    System.exit(0); 
  }
}
... 

La méthode actionPerformed est automatiquement appelée lorsque l'utilisateur sélectionne la commande Quitter.

Puisqu'une fenêtre est généralement abonnée à plusieurs composants, la méthode actionPerformed , détermine l'action (méthode) devant être déclenchée. Pour cela, elle interroge la "command action", détenue par le paramètre.

Si la command action vaut "Quitter", alors, nous savons que mnItemQuitter est à l'origine de l'événement et nous mettrons fin à l'application.

13.8. L'exemple complet


   
import javax.swing.*;
import java.awt.event.*;

public class FenetreMenu extends JFrame implements ActionListener{
 
   // une constante (mot clé final)
   // c'est un moyen très pratique d'associer un écouteur d'événement
   // à un générateur d'événement.
   static final String ACTION_QUITTER = "Quitter";
 
   // constructeur
   public FenetreMenu() { 
     // appel un constructeur de son parent
     super("Ma première interface graphique");
        // effet : donne un titre à la fenêtre

     // l'application s'arrête lorsque la fenêtre est fermée.
     setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
   
     // initialisation de la fenêtre
     init();
   }

 private void init(){
   //on a besoin de créer une barre de menus
   JMenuBar menuBar;
   // et un menu
   JMenu menuFichier;
   
   //création dela barre de menus
    menuBar = new JMenuBar();
   //construisons le premier menu
   menuFichier = new JMenu("Fichier");
   menuFichier.setMnemonic(KeyEvent.VK_F);
   menuFichier.getAccessibleContext().setAccessibleDescription(
        "Menu permettant d'accéder à une commande pour quitter");

   //création de la commande "quitter"
   JMenuItem mnItemQuitter = new JMenuItem(ACTION_QUITTER,
                        KeyEvent.VK_Q);
   mnItemQuitter.getAccessibleContext().setAccessibleDescription(
        "Quitter le programme");

   //mnItemQuitter.setActionCommand(ACTION_QUITTER);

   // le menu Fichier contient la commande Quitter   
   menuFichier.add(mnItemQuitter);
   //menu.addSeparator();    
   // la barre de menus contient le menu Fichier
   menuBar.add(menuFichier);
   JMenu autre = new JMenu("Un autre menu");
  autre.setMnemonic(KeyEvent.VK_U);
   autre.add(new JMenuItem("une commande",KeyEvent.VK_C));
   menuBar.add(autre);
   // on l'ajoute à la fenêtre
   setJMenuBar(menuBar);

   // la fenêtre est à l'écoute d'une action sur ce menu
   mnItemQuitter.addActionListener(this);

   setSize(300,200);     
  } 
     
  public void actionPerformed(ActionEvent evt) {
     String action = evt.getActionCommand();
   
     if (action.equals(ACTION_QUITTER)) {
       System.exit(0);
     }  
  }
   
  static public void main(String[] args) {
     JFrame f = new FenetreMenu();
     f.setVisible(true);
  }

}// FentreMenu   
  

13.9. Résumé

Nous avons vu, au travers d'un exemple, les 6 étapes nécessaires à la conception d'une interface graphique en Java .

  1. Analyser et concevoir une maquette "papier".

  2. Réutiliser par héritage, puis créer et configurer les composants.

  3. Placer les composants dans un conteneur.

  4. Gérer la position des composants dans le conteneur.

  5. Lier des gestionnaires d'événements aux composants retenus.

  6. Coder la logique en réponse aux événements.

[Important]Composants Swing et Awt

Les composants swing sont reconnaissables par le fait que leur nom commence par un J, comme par exemple JButton (pour des boutons), JTextField (zone de texte pour la saisie), JLabel (simple zone de texte), JPanel (des panneaux), etc.

En règle générale, vous ne devriez jamais mélanger dans une même interface, des composants AWT et SWING.

[Note]Action Command

Nous vous conseillons d'utiliser des CONSTANTES comme valeur des action command , comme par exemple ACTION_QUITTER.

static final String ACTION_QUITTER = "Quitter";

Par convention, les constantes sont spécifiées en MAJUSCULE, et sont généralement déclarées static . Il est en effet inutile de dupliquer une constante pour chaque objet. Rappelons qu'une propriété static est liée à la classe elle-même et non à chacun des objets de la classe en particulier. Du coup, tous les objets d'une même classe partagent la même propriété (si celle-ci est déclarée static). C'est pourquoi, cette dernière est très souvent une constante ( final ).

Vous trouverez chez Sun un tutoriel dédié à l'utilisation des menus. Vous apprendrez comment placer des cases à cocher, des sous-menus, des images dans un menu, c'est ici http://java.sun.com/docs/books/tutorial/uiswing/components/menu.html .

13.10. Exercices

  1. Ajouter à la barre de menus de la fenêtre, un menu nommé Configuration .

  2. Ajouter à ce nouveau menu, les commandes Modifier la hauteur , Modifier la largeur , Modifier la position en x , Modifier la position en y .

  3. Coder la logique de traitement pour chacune des commandes. Une JFrame a un attribut privé nommé width (largeur) et height (auteur). Ces propriétés sont interrogeables par la méthode getSize et modifiable par setSize . Attention, getSize retourne un objet de type Dimension qui dispose de deux attributs publics : width et height . Exemple d'utilisation :

    Dimension dim = this.getSize(); 
    String msg = "La largeur d la fenêtre est : " + dim.width ;
    javax.swing.JOptionPane.showMessageDialog(null, msg); 
    

    Pour modifier la position en X,Y d'un composant, vous pouvez utiliser la méthode setLocation , et accessoirement la méthode getLocation . Exemple d'aide de l'API :

    // méthode de la classe java.awt.Component 
    public Point getLocation() 
      // Returns: 
      //  an instance of Point representing the top-left corner 
      //  of the component's bounds in the coordinate space 
      //  of the component's parent.  
    

    Remarque 1 : Point est une classe qui a deux attributs publics X et Y, de type int .

    Remarque 2 : le composant "parent de la fenêtre" (celui qui contient la fenêtre) est dans notre cas l'écran.

    On vous demande de concevoir une méthode privée par commande ( JMenuItem ) , ainsi le gestionnaire d'événements ne fera que sélectionner la bonne méthode en fonction de la source de l'événement. Exemple :

    public void actionPerformed(ActionEvent evt) {
       String action = evt.getActionCommand();
       if (action.equals(ACTION_QUITTER) {
          System.exit(0);
       }
       else if (action.equals(ACTION_MODIF_X) { 
         // une méthode à concevoir dans cette classe
         this.setPositionX();
       }
       // etc. 
    } 
    
  4. Présenter à l'utilisateur, dans la boîte de dialogue, la valeur actuelle qu'il s'apprête à modifier.

  5. Ajouter une commande permettant à l'utilisateur de changer le titre de la fenêtre.

  6. Ajouter une commande permettant de placer automatiquement la fenêtre au centre de l'écran.

    [Note]Astuce

    Voici comment obtenir la taille de l'écran :

    Dimension screenSize = getToolkit().getScreenSize();

    La taille de la fenêtre s'obtient par :

    Dimension paneSize = getSize(); 

    Il ne vous reste plus qu'à chercher dans l'aide en ligne comment utiliser un objet de type Dimension .

    Vous remarquerez que ses champs (attributs) width et height sont publics. C'est un des rares contre-exemple du principe d'encapsulation .

14. Jeu du pendu en mode graphique

14.1. Introduction

Nous nous proposons de concevoir une version graphique du jeu du pendu. Nous ne redéfinirons que la partie interaction avec l'utilisateur, la logique du jeu restant la même.

Objectifs

  • Utiliser et Comprendre le rôle des gestionnaires de positionnement (layout)

  • Savoir placer dynamiquement des images dans une IHM

  • Utilisation des contrôles JButton, JProgressBar, JPanel et JLabel.

  • Sensibilisation à un modèle en couches (IHM, applicative)

14.2. Gestionnaire de positionnement

ref : http://java.sun.com/docs/books/tutorial/uiswing/layout/using.html

Un "gestionnaire de positionnement" (Layout manager) est un système qui détermine la taille et la position de composants. Par défaut, chaque conteneur a un layout manager - un objet qui gère la taille et la position des composants à l'intérieur du conteneur. Un composant peut tenter de gérer sa taille et sa position, mais au final, c'est le layout manager qui aura le dernier mot.

Les gestionnaires de positionnement les plus courants sont : BorderLayout , FlowLayout , GridLayout . Il en existe d'autres : BoxLayout , GridBagLayout ...

  • BorderLayout : Permet de positionner des composants selon un positionnement repéré par des points cardinaux (nord, est, sud, ouest) plus le centre.

    Remarque : BorderLayout est le layout par défaut de chaque contentPane (conteneur principal des objets de type JFrame , JApplet et JDialog ).

    Figure 20. Exemple d'utilisation d'un BorderLayout

    Exemple d'utilisation d'un BorderLayout

      ...
      JPanel panelBorderLayout = new JPanel();
      panelBorderLayout.setLayout(new BorderLayout());
      panelBorderLayout.add(new JButton("Button 1 (NORTH)"), BorderLayout.NORTH);
      panelBorderLayout.add(new JButton("2 (CENTER)"), BorderLayout.CENTER);
      panelBorderLayout.add(new JButton("Button 3 (WEST)"), BorderLayout.WEST);
      panelBorderLayout.add(new JButton("Long-Named Button 4 (SOUTH)"), BorderLayout.SOUTH);
      panelBorderLayout.add(new JButton("Button 5 (EAST)"), BorderLayout.EAST);
      ...
    
  • FlowLayout : Place les composants de la gauche vers la droite , et au besoin, continue sur une nouvelle ligne en dessous.

    Remarque : C'est le gestionnaire par défaut des JPanel.

    Figure 21. Exemple d'utilisation d'un FlowLayout

    Exemple d'utilisation d'un FlowLayout

      ...
      JPanel panel = new JPanel();
      panel.setLayout(new FlowLayout());
      panel.add(new JButton("Button 1"));
      panel.add(new JButton("2"));
      panel.add(new JButton("Button 3"));
      panel.add(new JButton("Long-Named Button 4"));
      panel.add(new JButton("Button 5"));
      ...
    
  • GridLayout : Taille les composants à une même taille et les range selon une grille exprimée en nombre de lignes (rows ) et de colonnes (columns ).

    Figure 22. Exemple d'utilisation d'un GridLayout

    Exemple d'utilisation d'un GridLayout

      ...
      JPanel panel = new JPanel();
      // création d'une grille de positionnement de
      // 3 lignes sur 2 colonnes
      panel.setLayout(new GridLayout(3,2));
      panel.add(new JButton("Button 1"));
      panel.add(new JButton("2"));
      panel.add(new JButton("Button 3"));
      panel.add(new JButton("Long-Named Button 4"));
      panel.add(new JButton("Button 5"));
      ...
    

    Remarque : une des deux dimmensions peut être à zéro, dans ce cas, la dimension nulle (ligne ou colonne) pourra accepter un nombre indéterminé d'objets. Par exemple l'instruction de création du gridLoyout de l'exemple ci-dessus pourrait être remplacée par new GridLayout(0,2) (nombre de lignes=0, nombre de colonnes=2).

  • On peut également de refuser le positionnement automatique des composants. Pour cela on affecte la valeur null au layout et on utilise la méthode setBounds par exemple pour positionner le composant au pixel près (en coordonnées relatives au conteneur direct du composant).

      ...
      JPanel panel = new JPanel();
      panel.setLayout(null);
      JButton button1 = new JButton("Bouton en positon (10,10)");
      JButton button2 = new JButton("Bouton en positon (200,150)");
      panel.add(button1);
      panel.add(button2);
      button1.setBounds(10,10,250,30);
      button2.setBounds(200,150,250,30);
      ...
    

Lorsqu'un programme utilise des gestionnaires de positionnement, en cas de changement de dimension du conteneur principal (la fenêtre par exemple), les positions de composants, et souvent leur taille également, seront modifiées.

14.3. Affichage d'une image

Bon nombre de composants d'IHM peuvent contenir une image, c'est le cas des menus, des boutons et des listes.

Java propose en standard la classe ImageIcon, qui, comme son nom l'indique, est chargée de représenter en mémoire une image. Un objet ImageIcon peut être instancier via une URL, comme l'indique un de ses constructeurs :

public class ImageIcon ... {
...
 /** constructeur
 *  Création d'un ImageIcon à partir d'une URL spécifiée.
 */
 ImageIcon(URL location) { ... }
...
}

Nous utiliserons donc ce constructeur très générique. Exemple de chargement d'une image :

 ...
 java.net.URL iconURL = 
    ClassLoader.getSystemResource("images/image_1.jpg");
 ImageIcon icon = new ImageIcon(iconURL);
 ...

Notez l'utilisation de la méthode static getSystemResource, une méthode de ClassLoader qui utilise le classpath pour trouver le fichier (nom de l'image), ainsi nous n'avons pas à spécifier le nom complet du chemin menant au fichier (the fully qualified path name).

Le plus simple pour afficher une image est d'utiliser un JLabel. En effet, ImageIcon n'est pas un composant visuel.

C'est donc un JLabel qui fera office de "conteneur" responsable de l'affichage de l'image. Un JLabel dispose d'une méthode setIcon tout à fait adaptée à nos besoins.

Voici comment nous pouvons le configurer pour les besoins de l'application.

   ...
   // création d'un tableau pouvant contenir 8 images
   ImageIcon[] images = new ImageIcon[8];

   ...

   java.net.URL iconURL = 
     ClassLoader.getSystemResource("images/image_1.jpg");
   images[0] = new ImageIcon(iconURL);
   
   ...

   // Création d'un label pour l'affichage d'une image.
   iconLabel = new JLabel();    
   iconLabel.setHorizontalAlignment(JLabel.CENTER);
   iconLabel.setVerticalAlignment(JLabel.CENTER);
   iconLabel.setText("");
     
   ...
   
   // affiche la première image du tableau images
   iconLabel.setIcon(images[0]);

   ... 

14.4. Analyse de la maquette du jeu

Voici une maquette de l'IHM du jeu :

Figure 23. Détails de conception de l'IHM

Détails de conception de l'IHM

  • Le contentPane de la fenêtre principale est privé de layout, ses deux composantes (deux JPanel) sont positionnées selon des coordonnées fixées à l'avance.

  • L'interface (le panel central) est divisée en 2 parties : l'une se charge de la vue et l'autre de contrôler la vue.

  • L'évolution du jeu est gérée par 2 points de contrôle : l'action sur un bouton et la commande Jeu/Nouveau.

14.5. Programmation de l'application

  • Le code source pour bien commencer (à copier dans un nouveau répertoire projet) : JeuPendu.java, JeuPenduGUI.java, Console.java, JeuPenduTextUI.java et le dictionnaire motsPendu.txt.

  • Les images : images-pendu.tar.

  • Implémenter les zones et méthodes marquées 'A FAIRE'

  • Faire en sorte que le joueur ne puisse pas sélectionner plus d'une fois la même lettre.

  • Modifier le fait de pourvoir paramétrer le nombre de coups perdus admis (entre 5 et 10). Dans ce cas, les premières images de constructions du pendu seront ignorées.

14.6. Annexe : diagramme de classes du jeu dans ses versions texte et graphique

Figure 24. Vue d'ensemble du projet

Vue d'ensemble du projet

15. Gestion de fichiers

15.1. Intro

Il existe deux grands types de format de fichier : "texte" et "binaire". Est qualifié de "fichier texte", tout fichier composé exclusivement de caractères dédiés ou compatible avec une impression standard, un affichage en mode console.

Par exemple un code source est un fichier texte, par contre un fichier MS World 2000 n'est pas un fichier texte, car il comporte des caractères non affichables (information sur la police, objets incorporés, et autres informations plus ou moins documentées par l'éditeur)

ATTENTION : On appelle format de fichier, la structure de son contenu , et non la valeur de son extension . Un fichier portant le nom de "document.txt" est certainement un fichier de type texte, pouvant être lu par un éditeur quelconque, mais rien ne le garantit ! L'inverse est également vrai : un fichier nommé "document.jpg" est certainement un fichier de type binaire (ici une image), mais ce n'est qu'une convention.

Le format texte

Contrairement au fichier binaire, le contenu d'un fichier texte est constitué de valeurs correspondant, pour la majorité, à des codes de caractères affichables (plus certains interprétables comme \n (fin de ligne), \t (tabulation)...).

Les fichiers au format texte sont le plus souvent structurés soit par :

  1. des lignes non formatées, séparées par un symbole de fin de ligne.

  2. des lignes formatées CSV ( Comma Separated Value ) une ligne est contituée d'une suite de champs séparés par un caractère spécial (une virgule par exemple) et d'un symbole de fin de ligne.

  3. des balises , c'est le cas des fichiers XML ( eXtensible Markup Language ) - non étudié ici.

  4. des mots clés d'un langage et une grammaire (comme le code source java) - non étudié ici.

Exemple document.txt

[kpu@kpu seance-15]$ cat document.txt
Bonjour,
futur informaticien,
nous vous souhaitons
bonne aventure.

Version hexadécimale

[kpu@kpu seance-15]$ hexdump document.txt
0000000 6f42 6a6e 756f 2c72 660a 7475 7275 6920
0000010 666e 726f 616d 6974 6963 6e65 0a2c 6f6e
0000020 7375 7620 756f 2073 6f73 6875 6961 6f74
0000030 736e 0a20 6f62 6e6e 2065 7661 6e65 7574
0000040 6572 0a2e
0000044

Version caractère

[kpu@kpu seance-15]$ hexdump -c document.txt
0000000   B   o   n   j   o   u   r   ,  \n   f   u   t   u   r       i
0000010   n   f   o   r   m   a   t   i   c   i   e   n   ,  \n   n   o
0000020   u   s       v   o   u   s       s   o   u   h   a   i   t   o
0000030   n   s      \n   b   o   n   n   e       a   v   e   n   t   u
0000040   r   e   .  \n
0000044
 

A titre d'information, il existe un outil bien pratique, nommé file , qui analyse le contenu d'un fichier pour en extraire des informations sur son type. Exemple :

$ file document.txt
document.txt: ASCII text

$ file zoo.png
zoo.png: PNG image data, 446 x 182, 8-bit/color RGB, non-interlaced

$ file TestGE.java
TestGE.java: UTF-8 Unicode Java program text

15.2. Java et la gestion des fichiers

Java considère un fichier comme un cas particulier d'un flux ( stream ).

[Note]Flux

Un flux est une séquence de caractères ou d'octets voyageant d'une origine vers une destinaton.

Un programme qui écrit dans un fichier est à l'origine d'un flux (ou producteur ). Un programme qui lit le contenu d'un répertoire, d'un fichier, ou de toute autre ressource est considéré comme une destination (ou consommateur ), qui reçoit progressivement le contenu du flux.

Java, pour des raisons pratiques, propose deux types de flux : les InputStream/OutPutStream et les Reader/Writer . Les premiers ( Stream ) sont utilisés pour la gestion de fichiers dit binaires, les Reader/writer sont spécialisés pour la gestion de flux de caractères UNICODE (16-bit) et Local (8-bit).

Notez qu'il existe de nombreuses classes de flux spécilisées (plus de 70 !), et certaines sont dans des packages utilitaires comme java.util.zip . Sachez également qu'il existe des passerelles pour passer d'une logique de flux d'octets à celle de flux de caractères (16-bit), ce sont les classes : InputStreamReader pour convertir un InputStream en un Reader et OutputStreamWriter pour convertir un OutputStream en un Writer .

Vous trouverez ici, une traduction d'un extrait d'un tutorial chez Sun , présentant la hiérarchie des classes I/O.

[Note]Connaître les caractéristiques d'un fichier

Les caractéristiques d'un fichier peuvent être interrogées en utilisant la classe java.io.File , qui est présentée comme une représentation abstraite d'un fichier et d'un chemin de répertoire . C'est cette classe qui connait par exemple le symbole de séparation des chemins ( pathSeparatorChar ) (par exemple : sous Unix et ; sous MSWindows ).

Pour en savoir plus sur cette classe, retrouvez l'API API File et des exemples ici :http://java.developpez.com/faq/java/?page=langage_fichiers

Les flux de caractères sont associés à des classes spécialisées dans la lecture des données ( reader ) et dans leur écriture ( writer ).

Les principales étapes de gestion d'un fichier sont :

  1. Ouverture du flux

  2. Exploitation (par lecture ou écriture)

  3. Fermeture du flux

15.3. Exploitation en lecture d'un fichier texte

1 - Ouverture d'un fichier

// ouvre un fichier en lecture
FileReader fr = new FileReader(nomFic);

// passe par un buffer pour simplifier la lecture du fichier
BufferedReader buf = new BufferedReader(fr);

2 - Exploitatin en lecture séquentielle du fichier

// déclaration d'une chaîne de caractères
// afin de stocker la chaîne courante
// à chaque tour dans la boucle.
String ligne;

// lecture de la première ligne du fichier
ligne = buf.readLine();

// tant que la fin de fichier n'est pas atteinte
while (ligne != null) {
   // faire quelque chose avec ligne
   // ...
   // à la fin du corps de la boucle, lire la ligne suivante
   ligne = buf.readLine();
}
   

3 - Fermeture fichier

fr.close(); 

15.4. Exploitation en écriture d'un fichier texte

1 - Ouverture d'un fichier en écriture

// ouvre un fichier en écriture (1ère version)
FileWriter out  = new FileWriter(nomFic);

/* autre version avec choix du jeu de caractères 
  (ici ISO Latin Alphabet No. 1 - ISO-LATIN-1)
OutputStreamWriter out = 
  new OutputStreamWriter(new FileOutputStream(nomFic), "ISO-8859-1");
*/

// passe par un buffer pour simplifier l'écriture dans le fichier
PrintWriter pw = new PrintWriter(out);

Exemple d'écriture dans le fichier (relativement à la position courante dans le fichier)

// déclaration d'une chaîne de caractères
// afin de stocker la chaîne à écrire
String ligne = "coucou";
// écriture de la ligne dans le fichier
pw.write(ligne);

Exemple de fermeture fichier

pw.close(); 

15.5. Exercice

Ecrire un programme qui recoit un nom de fichier texte en argument du main et qui affiche :

  1. la première ligne non vide du fichier

  2. la dernière ligne non vide du fichier

  3. le nombre total de lignes (vide ou non).

Exploitation d'un fichier CSV, et transformation en XML : http://www.linux-france.org/prj/edu/archinet/DA/tpXML/

15.6. Gestion de fiches Contact

On souhaite gérer un fichier de contacts (contacts.txt)

Une fiche contact est caractérisée par un nom, prénom, adresse rue, code postal, ville, pays, tel et email.

Les caractéristiques d'une fiche ont placées sur la même ligne. S'il y a 2 fiches, alors le fichier contiendra 2 lignes, et ainsi de suite. Exemple :

Durand;Michel;35 rue du lavoir;38000;Grenoble;France;0123456789;mdurand@nullepart.com
Valerian;Denise;250 avenue Louise;1050;Bruxelles;Belgique;00 32 (0)2 345 67 89;vale@belgeunefois.com

On souhaite produire une application permettant dans un premier temps de présenter, une à une, les fiches contact du fichier. Voici une maquette :

Figure 25. Maquette gestion de fiches Contact

Maquette gestion de fiches Contact

Première analyse. Nous concevrons trois classes : l'IHM (Interface Homme Machine) - une JFrame - , un contrôleur ( NavigatorContactFic ) qui détient une collection d'objets Contact , et une classe Contact représentant la structure type d'une fiche contact.

Figure 26. Diagramme de classes de l'application

Diagramme de classes de l'application

La relation NavigatorContactFic --> Contact dénote une liste nommée lesContacts (un objet de type ArrayList ou Vector ). C'est un attribut privé de la classe NavigatorContactFic . Cet attribut est créé et valorisé dans le constructeur de NavigatorContactFic .

Donc, à la création, le fichier est lu et son contenu placé en mémoire sous forme d'une collection d'objets de type Contact.

Figure 27. Diagramme de communication : Demande de visualisation de la fiche suivante

Diagramme de communication : Demande de visualisation de la fiche suivante

15.7. Exercices

  1. Pour simplifier, nous considérons provisoirement qu'une fiche est caractérisée par un nom et un prénom (si on sait le faire pour 2 propriétés, on saura le faire pour n=8).

    A FAIRE : Concevoir les classes Conact et NavigatorContactFic, de sorte que le programme de test suivant puisse fonctionner :

    
    import java.io.*;
    public class Test {
    
     public static void main(String[] a){
      try {
        NavigatorContactFic nav = new NavigatorContactFic(a[0]);
    
        System.out.println("-- Première fiche");
        System.out.println(nav.premier());
        System.out.println("-- Dernière fiche");
        System.out.println(nav.dernier());
    
        System.out.println("-- Voici les 3 premieres");
        System.out.println(nav.premier());
        System.out.println(nav.suivant());
        System.out.println(nav.suivant());
        System.out.println("-------------");
        
        System.out.println("-- Voici les \"3 précédentes\"");
        System.out.println(nav.precedent());
        System.out.println(nav.precedent());
        System.out.println(nav.precedent());
        System.out.println("-------------");
    
      }
      catch (FileNotFoundException e) {
        System.out.println("Fichier introuvable");
      }
      catch (IOException e) {
        System.out.println( e );
      }
     }
    }
    
    

    Sur le fichier de données suivant ( doc.txt) :

    Meyer;Bertrand
    Goodwill;James
    Kay;Michael
    

    La commande java Test doc.txt , produit le résultat suivant :

    $ java Test doc.txt
    -- Première fiche
    Nom : Meyer    prenom : Bertrand
    -- Dernière fiche
    Nom : Kay   prenom : Michael
    -- Voici les 3 premieres
    Nom : Meyer    prenom : Bertrand
    Nom : Goodwill   prenom : James
    Nom : Kay   prenom : Michael
    -------------
    -- Voici les "3 précédentes"
    Nom : Goodwill   prenom : James
    Nom : Meyer    prenom : Bertrand
    Nom : Meyer    prenom : Bertrand
    -------------
    $
    
    

    Remarquer que si la fiche courante est la première, la précédente est elle-même (même logique pour la fiche suivante de la dernière fiche).

  2. Modifier la solution proposée, afin qu'elle prenne en compte les huit propriétés énoncées précédemment. Voici un fichier contenant plusieurs fiches : contacts.txt .

  3. Implémenter les méthodes isLast, isFirst , nbContacts et sauvegarde :

    • isFirst : rend vrai si la fiche courante est la première de la liste, faux sinon.

    • isLast : rend vrai si la fiche courante est la dernière de la liste, faux sinon.

    • nbContacts: rend le nombre de contacts dans la collection (initialement le nombre de fiches dans le fichier).

    • sauvegarde : place le contenu de la liste dans le fichier initial.

  4. Concevoir un application en mode graphique, fidèle à la maquette présentée.

16. Boîte de dialogue

16.1. Introduction

La fonction première d'une boîte de dialogue est de paramétrer un service offert par le système. Cela peut être, par exemple, une fonction d'exportation de données, une sauvegarde, ou une confirmation de suppression.

Java propose en standard des fonctions de boîte de dialogue : tutoriel d'utilisation des boîtes de dialogue de JOptionPane.

Nous présentons ici une utilisation de la classe JDialog.

16.2. Item de menu

Nous souhaitons ajouter au programme de gestion de contacts, la possibilité d'ajouter un contact, vous programmerez la modification et la suppression en exercice.

Nous commençons par ajouter une commande accessible par un item de menu.

Figure 28. Menu ajouter un contact

Menu ajouter un contact

Et le code associé :

// attributs de classe
static final String ACTION_AJOUTER_CONTACT = "AjouterContact";
...

//... dans init
...
JMenu menu = new JMenu("Contacts");
menu.setMnemonic(KeyEvent.VK_C);
JMenuItem mnAjouterContact = new JMenuItem(ACTION_AJOUTER_CONTACT,
   KeyEvent.VK_A);
mnAjouterContact.setActionCommand(ACTION_AJOUTER_CONTACT);
mnAjouterContact.addActionListener(this);
menu.add(mnAjouterContact);
...
menuBar.add(menu);
...

16.3. Logique de l'événement

Ensuite, programmons l'événement. L'algorithme est simple, et facilement généralisable :

1: création d'un objet Contact
2: création d'un objet boîte de dialogue. On lui passe
   la référence à notre objet Contact nouvellement créé.
3: affichage de la boîte de dialogue en MODE MODAL
4: après la fermeture de la boîte de dialogue, on
   l'interroge afin de savoir s'il est utile de
   poursuivre l'action. 
4.1 : l'utilisateur n'a pas abandonné, on exploite
   l'objet dont la référence a été passée en argument de
   la boîte de dialogue. Ceci se passe en deux temps :
   4.1.1 : Demande de confirmation
   4.1.2 : Exécution ou non exécution de l'opération
4.2 : l'utilisateur a abandonné : aucune action n'est entreprise.

En voici une traduction :

if (action.equals(ACTION_AJOUTER_CONTACT)) {
   Contact c = new Contact("", "");
   DailogAjouterContact dac = new DailogAjouterContact(this, c);
   dac.setVisible(true);
   // à ce niveau d'étape, l'utilisateur a fermé la boîte de dialogue
   if (!dac.isCancel()) {
     int res = JOptionPane.showConfirmDialog(null,
       "Confirmez-vous la création ?", "Confirmation",
        JOptionPane.YES_NO_OPTION);
     if (res == JOptionPane.OK_OPTION) {
       JOptionPane.showMessageDialog(this, "L'objet " + c
           + " va être sauvegardé.");
       // faire quelque chose
     }
   }
   // else aucune action
}

16.4. La boîte de dialogue

Nous avons vu qu'elle reçoit en argument un objet Contact. De plus notre boîte de dialogue sera modal, c'est à dire qu'elle focalise les saisies utilisateur de l'application (elle garde le focus). Le constructeur de notre boîte appelera un des constructeurs de JDialog, classe dont elle hérite.


public class DailogAjouterContact extends JDialog implements ActionListener {
  private Contact contact;
  private boolean cancel;
...
 public DailogAjouterContact(JApplication app, Contact c) {
    super(app, "Ajouter un contact", true);
      // le premier argument référence la fenêtre à qui
      // la boîte de dialogue prend le focus.
      // le deuxième rag. est le titre de la boîte de dialogue.
      // le dernier arg. est un booléen pour activer le comportement
      // modal de la fenêtre.
    contact = c;
    cancel = true;
    init();       
    setSize(300,100);
    setLocationRelativeTo(null); 
     // centre la boîte
 }

Nous lui ajoutons quelques JTextField et deux boutons : un pour valider et un autre pour annuler :

  ...
   private JTextField nom;
   private JTextField prenom;
   private JButton btCancel;
   private JButton btValider;
  ...

La création et le positionnement sont réalisés dans le corps de la méthode init:

  private void init() {       
      JPanel panel = new JPanel();
      panel.setLayout(new GridLayout(2,0));
      panel.add(new JLabel("Nom"));
      panel.add(new JLabel("Prénom"));
      
      getContentPane().add(panel, BorderLayout.WEST);
      panel = new JPanel();
      panel.setLayout(new GridLayout(2,0));
      nom = new JTextField("Nom : ");
      prenom = new JTextField("Prénom : ");
      
      nom.setText(contact.getNom());
      prenom.setText(contact.getPrenom());
      
      panel.add(nom);
      panel.add(prenom);
      getContentPane().add(panel, BorderLayout.CENTER);
      btValider = new JButton("Valider");
      btCancel = new JButton("Abandonner");
      btValider.setActionCommand(ACTION_VALIDER);
      btCancel.setActionCommand(ACTION_CANCEL);
      btValider.addActionListener(this);
      btCancel.addActionListener(this);
      
      panel = new JPanel();
      panel.add(btValider);
      panel.add(btCancel);
      getContentPane().add(panel, BorderLayout.SOUTH);      
    }

Ce qui donne :

Figure 29. Boîte de dialogue ajouter un contact

Boîte de dialogue ajouter un contact

16.5. Code de fermeture de la boîte de dialogue

La propriété cancel étant positionnée à vrai (true) dans le constructeur, la seule chose à laquelle nous devons faire attention est la validation.

La boîte de dialogue ne devrait retourner qu'un objet valide à l'appelant. La valeur de la propriété cancel sera placée à false, QUE SI les données saisies par l'utilisateur sont conformes. C'est alors que nous donnons les valeurs des données saisies (ou proposées) par l'utilisateur aux accesseurs de l'objet Contact pris en charge par la boîte de dialogue.

public void actionPerformed(ActionEvent e) {
  String action = e.getActionCommand();
  if (action.equals(ACTION_VALIDER)){

    if (verification()) {
      cancel = false;
      contact.setNom(nom.getText());
      contact.setPrenom(prenom.getText());
      setVisible(false);
    }
    else {
      JOptionPane.showMessageDialog(this, "Erreur de saisie");
    }
  }

  else if (action.equals(ACTION_CANCEL)){
      setVisible(false);
  }       
}

La méthode verification vérifie que le nom et le prénom ne soit pas vide :

/**
 * @return true si le nom et prenom sont renseignés
*/
private boolean verification() {
  return (!"".equals(nom.getText().trim()) &&
          !"".equals(nprenom.getText().trim()));
}

La méthode isCancel ne fait que retourner la valeur de la variable privée cancel:

public boolean isCancel() {
  return cancel;
}

16.6. Conclusion

Nous venons de présenter un façon simple, mais très facilement personnalisable, de concevoir et de programmer une boîte de dialogue héritant de JDialog.

Nous avons personnalisé le constructeur afin de fournir un objet Contact à la boîte de dialogue. L'action de validation (un bouton) valorise l'objet Contact à partir des composants de l'interface.

Les fichiers : gestionContacts.tgz.

16.7. Exercices

  • Proposez une commande permettant de modifier le contact courant, et une autre de le supprimer.

17. Interaction avec un SGBDR

17.1. Introduction

Les programmes Java disposent d'un pont logiciel pour s'interfacer avec un SGBDR, représenté par une API nommé JDBC ( Java Database Connectivity ). Par l'intermédiaire de JDBC, vous pouvez programmer des créations de tables, des insertions et modifications de valeurs et aussi des requêtes, le tout dans un contexte de transactions avec gestion d'exceptions.

17.2. Pilotes JDBC

L'API JDBC se trouve dans java.sql et les pilotes doivent implémenter l'interface java.sql.Driver.

Il existe quatre types de pilotes jdbc :

  • Type 1 : Pont jdbc-odbc (livré en standard, idéal comme premier driver sous Windows)

  • Type 2 : API native + un peu de java.

  • Type 3 : Comme type 2 mais avec un protocole réseau tout en java.

  • Type 4 : Protocole natif 100% java.

Les éditeurs de SGBDR proposent leurs propres pilotes JDBC.

Figure 30. Exemple de pilotes

Exemple de pilotes

L'interaction à un système de gestion de base de données réquiert en général au moins quatre étapes :

  1. Chargement du pilote

  2. Etablissement de la connexion

  3. Exécution d'une requête

  4. Exploitation des résultats

Nous présentons ci-dessous chacune de ces étapes.

17.3. I - Chargement du pilote dans la JVM ( Java Virtual Machine )

On charge généralement le pilote par son nom. Ci-dessous, un exemple de programme chargeant un pilote défini sous forme de chaînes de caractères.


final String driverPostgreSql = "jdbc.postgresql.Driver"; 
       // driver PostgreSql : http://jdbc.postgresql.org/
final String driverOdbc       = "sun.jdbc.odbc.JdbcOdbcDriver"; 
       // driver odbc inclus dans le jdk 
       // (pratique sous Windows pour SQL Server, Access...)
final String driverHsql       = "org.hsqldb.jdbcDriver"; 
       // driver Hypersonic SQL
final String driverMySQL = "com.mysql.jdbc.Driver";
     // driver MySQL : http://www.mysql.com/products/connector/

String driver = driverHsql;

Class.forName(driver).newInstance();  
  // Autochargement du driver

Les pilotes JDBC sont à télécharger directement chez les éditeurs de SGBDR. Par exemple :

Puis à rendre accessible dans le chemin des classes (classpath). Sous Eclipse, vous pouvez créer un répertoire bin dans lequel vous placez le ou les drivers en question (un fichier ayant l'extension .jar) que vous ajouter ensuite à votre buildpath (via un clic droit...)

17.4. II - Etablissement de la connexion

Une fois le driver chargé en mémoire, nous obtenons (si tout va bien) une connexion via la méthode de classe getConnection() de DriverManager


 Connection con = DriverManager.getConnection(URL, "user", "passwd");
 //  URL : url de connexion de la forme jdbc:sous-protocole:sous-nom
 //     sous-protocole:identification du pilote
 //     sous-nom  :  informations nécessaires au pilote
 //                  pour la connexion (chemin, port, nom) 
 // "passwd" : Mot de passe 
 // "user"   : Nom de l'utilisateur référencé par la base 

Exemple

//final String driver = "org.hsqldb.jdbcDriver";
// final String url = "jdbc:hsqldb:/home/kpu/hsql/refuge/refuge"; 
final String driver = "com.mysql.jdbc.Driver";
  // driver disponible ici : http://www.mysql.com/products/connector/
final String url      = "jdbc:mysql://localhost/refuge";
final String user     = "sa";    
final String password = "secret"; 
Connection con = null;
try {
  Class.forName(driver).newInstance();
  con = DriverManager.getConnection(url, user, password); 
  ... 

17.5. III - Exécution d'une requête SQL

L'exécution d'une requête SQL s'effectue via un objet de la classe java.sql.Statement . C'est l'objet Connection qui nous fournira une référence d'objet Statement (à ne pas instancier directement ). Exemple :

 Statement stat = con.createStatement();  

L'accès aux données peut être sensible ou non aux accès concurrents et la navigation dans le modèle peut être possible ou non (forward only).

 Statement stat = con.createStatement();  
Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
                                     ResultSet.CONCUR_READ_ONLY);

// autre exemple, une source de données insensible aux changements concurrents
Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
                                     ResultSet.CONCUR_READ_ONLY);

plus d'info ici

On distingue deux types de requêtes : requête d'interrogation et de mise à jour.

  • Requête d'interrogation avec l'ordre SELECT

    La méthode executeQuery( ) de Statement retourne un objet java.sql.ResultSet .

    ResultSet rs = stat.executeQuery("SELECT * FROM ANIMAL"); 
    
  • Requête de mise à jour avec les ordres UPDATE, INSERT, DELETE

    On utilisera la méthode executeUpdate( ) de Statement .

    Cet exemple supprime de la table ENTREPRISES toutes les entreprises de Seine et Marne.

    stat.executeUpdate( "DELETE FROM ENTREPRISES WHERE CODEPOST LIKE '77%'"); 
    

17.6. IV - Exploitation des résultats

17.6.1. Requête d'interrogation avec l'ordre SELECT

Le retour d'un ordre executeQuery(...) est un objet de type ResultSet , une collection de lignes constituées de 1 à n attributs (colonnes).

Pour accéder à la première ligne du résultat, il est nécessaire d'appeler la méthode next() , pour passer à la ligne suivante, il suffit d'appeler de nouveau cette méthode, etc.

ResultSet rs = stat.executeQuery("SELECT * FROM ANIMAL");
   
// Pour accéder à chacun des tuples du résultat de la requête : 
while (rs.next()) { 
   String nom              = rs.getString("nom"); 
   java.sql.Date date_nais = rs.getDate("date_nais");
   int id                  = rs.getInt(1);
   ... 
}

Remarque 1 : L'appel à la méthode next() de l'objet Statement est obligatoire avant tout appel aux méthodes permettant d'accéder à une valeur d'un attribut de la ligne courante.

Remarque 2 : Il y a deux façons d'accéder à une valeur d'un attribut (colonne) : 1/ soit par le nom de la colonne , comme par exemple les deux premiers appels de l'exemple. 2/ soit par position, qui commence à la position 1 (et non 0 comme avec les collections), comme le montre le troisième appel.

17.6.2. Requête de mise à jour (UPDATE, INSERT, DELETE)

La méthode executeUpdate( ) de Statement , ne retourne pas un objet java.sql.ResultSet mais retourne le nombre de lignes impactées par l'instruction.

Cet exemple supprime de la table ENTREPRISES toutes les entreprises de Seine et Marne.

int count = 
  stat.executeUpdate(
    "DELETE FROM ENTREPRISES WHERE CODEPOST LIKE '77%'");
System.out.println("Il y a eu " + count + " lignes supprimées.");

17.7. Requêtes paramétrées

Pour des raisons d'efficacité (compilation de la requête côté sgbdr) et de souplesse de construction des requêtes paramétrés (concaténation et placer les valeurs entre quotes ou non), le développeur peut utiliser un objet PreparedStatement pour envoyer des instructions SQL.

L'exemple suivant illustre les deux approches (source : tutoriel jdbc) :

 
Code Fragment 1:

    String updateString = "UPDATE COFFEES SET SALES = 75 " + 
                          "WHERE COF_NAME LIKE 'Colombian'";
    stmt.executeUpdate(updateString);

Code Fragment 2:

    PreparedStatement updateSales = con.prepareStatement(
            "UPDATE COFFEES SET SALES = ? WHERE COF_NAME LIKE ? ");
    updateSales.setInt(1, 75); 
    updateSales.setString(2, "Colombian"); 
    updateSales.executeUpdate():

La deuxième version, bien que plus verbeuse, nous permet de mieux structurer nos instructions et de ne pas gérer la présence ou non de quotes encadrantes.

On constatera l'usage de méthodes setXXX selon le type de l'argument SQL attendu, et que l'appel à executeUpdate se fait sans argument.

De plus, une requête paramétrée est toujours compilée par avance côté SGBDR, ce qui accélère son traitement, surtout en cas d'appels répétés, dans une boucle par exemple.

17.8. Exemple de programme

Le programme ci-dessous utilise une base nommée Refugedb .

Cette base de données contient une table nommée ANIMAL; voici un script de création :


  CREATE TABLE ANIMAL (
    id INTEGER PRIMARY KEY, 
    categorie VARCHAR NOT NULL, 
    nom VARCHAR, race VARCHAR, 
    sexe CHAR, 
    date_nais DATE, 
    id_proprio INTEGER, 
    present BIT
  )
  INSERT INTO ANIMAL 
    VALUES (1,'CRM', 'kiki','berger','M','2000-2-21',21,false)
  INSERT INTO ANIMAL 
    VALUES (2,'CRM','rex','caniche','M','1996-12-2',11,true)
  ...
  

Lorsque l'on accède à une base de données, une gestion des exceptions s'avère nécessaire car de multiples problèmes peuvent survenir : le pilote ne peut être chargé (introuvable ?), connexion refusée, requête SQL mal formée... Voici l'exemple complet.

 1  import java.sql.*;
 2  
 3  public class TestAnimal {
 4    public void test() {
 5      final String driver = "com.mysql.jdbc.Driver";
 6      final String url = "jdbc:mysql://localhost/Refugedb";
 7      final String user = "sa";
 8      final String password ="";
 9          
10      Statement  st  = null;
11      Connection con = null;
12      ResultSet  rs  = null;
13      String    sql  = "";
14      try {
15         Class.forName(driver).newInstance();
16         con = DriverManager.getConnection(url, user, password);
17         st  = con.createStatement();
18         sql = "SELECT * FROM ANIMAL";
19         rs  = st.executeQuery(sql);
20         System.out.println("ID\tTYPE\tNOM\t\tRACE\t");
21         while (rs.next()) {
22           System.out.print(rs.getInt(1)+"\t");
23               // ATTENTION, les indices commencent à 1.
24           System.out.print(rs.getString(2)+"\t");  
25           System.out.print(rs.getString("nom")+"\t\t");  
26           System.out.println(rs.getString("race")+"\t");
27         }//while  
28       }
29       catch (ClassNotFoundException e) {
30          System.err.println("Classe non trouvée : " + driver );
31       }
32       catch (SQLException e) {
33          System.err.println("SQL erreur : "+
                 sql + "  " + e.getMessage());
34       }
35       catch (Exception e) {
36          System.err.println("Erreur : "+ e);
37       }
38   
39       finally {
40          try { if (con != null) { con.close(); } }
41          catch (Exception e) { System.err.println(e); }
42       }
43    }
44    static public void main(String[] arg) {
45       TestAnimal app = new TestAnimal();
46       app.test();
47    }
48  }

Quelques commentaires :

  • Ligne 15 : Chargement du pilote Hypersonic SQL.

  • Ligne 16 : Etablissement d'une connexion à la base.

  • Ligne 17 : Création d'un objet Statement en préparation à l'exécution d'une requête SQL.

  • Lignes 18 - 27 : Selection de toutes les lignes de la table ANIMAL.

    Affichage de quelques attributs (ID, TYPE, NOM et RACE). La condition de poursuite dans le while permet d'avancer à la prochaine ligne et de tester si la fin n'est pas atteinte (rend false alors).

  • Lignes 29 - 37 : Une gestion des exceptions.

  • Lignes 39 - 42 : A ne pas oublier, fermeture de la connexion.

17.9. Type Java et SQL

Comatibilité entre type Java et SQL, un "x" signifie que la méthode getXXX est compatible avec le type SQL correspondant.

Figure 31. Extrait de la documentation de l'API Java by Sun

Extrait de la documentation de l'API Java by Sun

17.10. Requête de modification avec les méthodes updateXXX

Il est possible, grâce aux méthodes setXXX de mettre à jour des données sans utiliser d'ordres SQL.

Les méthode updateXXX prennent 2 arguments, le premier est la référence à la colonne concernée (index ou nom de colonne) et le second représente la nouvelle valeur à assigner au tuple courant.

   ...
   String nom = rs.getString("nom"); // kiki
   rs..updateString("race", nom.toUpperCase());
   System.out.print(rs.getString("nom")); // KIKI
   ... 
   ... [autre modification]
   ....
   rs.updateRow();  // répercution dans la base de données
   // ou rs.cancelRowUpdates()
         
   ...   
   
 

La suite sur Sun Guide JDBC...

17.11. Transaction

Avec l'API JDBC, une transaction est pilotée par l'objet Connection.

Par défaut une instruction SQL est exécutée comme une transaction individuelle immédiatement validée dans la base de données.

Ce mode par défaut peut être modifié via la méthode setAutoCommit() afin de regrouper plusieurs instr. SQL, ceci est représenté par le pseudo-code suivant :

 Si la transaction est composé d'1 seule instr. SQL Alors
   Si laConnection.getAutoCommit() == false Alors
 	 laConnection.setAutoCommit(true)
   FSi
   Une instr. SQL => une transaction automatique
Sinon
   laConnection.setAutoCommit(false)
   [série d'instr. SQL]
   Si aucune erreur Alors
      laConnection.commit() 
   Sinon
      laConnection.rollback()
   Fsi
Fsi
 

Exemple de code Java avec contrôle simple sur exception :

  // Permettre le regroupement d'instructions SQL
    con.setAutoCommit(false);
    try {
      Statement stmt = con.createStatement();
      stmt.executeUpdate(
        "UPDATE COMPTE_A SET SOLDE = (SOLDE - 1000) WHERE IDCLT = 5");
      stmt.executeUpdate(
        "UPDATE COMPTE_B SET SOLDE = (SOLDE + 1000) WHERE IDCLT = 5");

      con.commit();
      out.println("Ordre de transfert réussi");
    }
    catch (Exception e) {
      // une erreur est survenue, retour en arrière 
      try { 
        con.rollback();
        out.println("Ordre de transfert annulé");
      }
      catch (Exception ignoree) { } 
    }
 

En exercice : proposer une autre version Java avec contrôle plus fin qui exploite la valeur de retour de la méthode executeUpdate.

18. Prise en main d' HSQLDB (Hypersonic SQL)

Nous souhaitons faire évoluer notre application de gestion de fiches (de type Client). Nous allons lui permettre de s'interfacer avec un SGBDR.

Comme SGBDR, nous utiliserons HSQLDB, un projet open source.

18.1. Introduction à HSQLDB

HSQLDB est un petit système de gestion de bases de données écrit en Java, initialement développé par Thomas Mueller et hsqldb Group .

HSQLDB est très souvent utilisé pour des démonstrations, du maquettage et de petites applications ayant besoin de s'interfacer avec un système de gestion de bases de données relationnel (SGBDR). Sa taille (<300 ko), son prix (gratuit), sa portabilité (Java) et sa rapidité de mise en oeuvre sont ses principaux atouts.

Ajoutons à ces qualités la relation privilégiée de ce système avec le langage Java, et nous avons une solution bien adaptée à un apprentissage de la programmation, portable sous Windows comme sur Linux.

Il existe un projet de portage d'HSQLDB sur une autre plateforme, voir wikipedia

18.2. Installation

Téléchargez HSQLDB à l'adresse suivante http://hsqldb.sourceforge.net .

Décompressez le fichier dans un répertoire prévu à cet effet (ex: hsqldb).

18.3. Démarrage d'hsqldb

Pour utiliser hsqldb, vous pouvez :

  • Administrer une base via l'outil Database Manager livré avec hsqldb.

  • Utiliser la base via une application cliente écrite en Java.

18.4. Outil Database Manager

La base de données que nous allons créer est celle qui correspond à notre fichier Clients .

Nous allons donc créer notre base de données. Pour cela nous utilisons Database Manager, un des outils livrés avec hsqldb (package org.hsqldb.util ).

  • Lancer l'application

    Le répertoire demo contient des fichiers permettant de lancer certains outils. Par exemple le fichier de commandes runManager.bat permet de lancer le Database Manager afin d'interagir avec les bases de données. Ces fichiers de commandes existent aussi pour les environnements Unix, par exemple runManager.sh .

    Nous vous invitons à comparer ces deux fichiers, runManager.bat et runManager.sh , leurs différences sont mineures.

    Lancer le gestionnaire.

    ./runManager.sh 
    

    Une première fenêtre apparaît :

    Figure 32. Manager SQL

    Manager SQL

    Elle vous demande :

    • Le mode dans lequel vous voulez créer ou ouvrir la base de données (Type).

      Choisir Standalone , en effet par défaut les données ne sont gérées qu'en mémoire (In-Memory), ce qui signifie que les informations sont perdues à la fermeture de l'application.

      En choisissant Standalone, vous utilisez HSQL en mode non partagé. En mode Client/Serveur, nous choisirons Server ou WebServer selon le cas.

      Pour la création de la base nous nous contenterons d'une utilisation Standalone (In-Process).

    • La classe du driver d'hsqldb (Driver)

    • L'URL de la base de données que vous allez créer ou utiliser

      Dans l'url, le nom de la base fait suite à jdbc:hsqldb:file: . Dans notre exemple, la base se nomme dbtest , dans le répertoire <chemin d'inst. de hsqldb>/data ou /chezmoi/ici/ en cas de chemin absolu.

    • Le nom de l'utilisateur (User)

    • Le mot de passe de l'utilisateur (Password)

Vous accédez alors à la fenêtre suivante, permettant de réaliser des opérations SQL sur la base.

Figure 33. Database Manager (./runManager.sh)

Database Manager (./runManager.sh)

Vous avez à votre disposition un fichier ( creercltsdb.sql ) permettant de créer la table client et de la valoriser avec quelques données.

Une fois le fichier rapatrié, ouvrez-le via la commande File->Open Script... de l'application. Le contenu est alors automatiquement placé dans la zone d'édition de Database Manager.

Figure 34. Database Manager (importation d'un script)

Database Manager (importation d'un script)

Le fichier creercltsdb.sql contient des ordres de création de tables, ainsi que des insertions. Il ne vous reste plus qu'à déclencher l'interprétation des ordres SQL en cliquant sur le bouton Execute.

Figure 35. Database Manager (après exécution)

Database Manager (après exécution)

Vérifiez le bon déroulement de l'opération en sélectionnant View -> Refresh Tree . La table CLIENTS doit apparaître dans l'arbre de gauche.

Figure 36. Database Manager (la table CLIENTS est visible)

Database Manager (la table CLIENTS est visible)

... et en exécutant l'ordre de sélection de tous les tuples de la table clients .

Figure 37. Database Manager (select * from clients;)

Database Manager (select * from clients;)

19. Utilisation de HSQLDB en mode serveur, avec deux clients

Les clients seront l'outil DatabaseManager (dans le rep. demo) et un cours programme en Java.

Scénario de test

  1. Nous lançons hsqldb en mode serveur :

    # on se place dans le dossier data (la ou se trouve la base)
    cd <chemin d'inst. de hsqldb>/data
    
    # on lance le serveur en fournissant un alias (dbTest)
    java -cp ../lib/hsqldb.jar org.hsqldb.Server -database.0 projTest/db -dbname.0 dbTest
               
  2. A ce niveau, le serveur est lancé et "pointe" sur notre base vide. Nous allons la peupler en utilisant l'outil DatabaseManager.

    Lancer Database Manager en mode serveur : jdbc:hsqldb:hsql://127.0.0.1/dbTest

    Figure 38. Database Manager (en mode connection à un serveur)

    Database Manager (en mode connection à un serveur)

    Executer le script : creercltsdb.sql

  3. Créer un répertoire, puis copier le package <chemin d'inst. de hsqldb>/lib/hsqldb.jar à l'intérieur (pour faire simple).

  4. Copier le fichier TestClients.java dans ce répertoire. Extrait :

    import java.sql.*;
     
    public class TestClients {
     public void test() {
      final String driver   = "org.hsqldb.jdbcDriver";
      final String url      = "jdbc:hsqldb:hsql://127.0.0.1/dbTest";
      final String user     = "sa";
      final String password ="";
            
    ...
    

    On remarquera que l'on se connecte à la base avec son alias, sur la machine locale (localhost).

  5. Compiler le fichier : javac TestClients.java

  6. Exécuter le programme en spécifiant la bibliothèque hsqldb.jar:

    [dans rep de test]$ java -classpath hsqldb.jar:. TestClients
    max id = 3
    ID      NOM     PRENOM
    0       pam     lafrite
    1       Durand  Michel
    2       Tagada  Nelson
    3       Heisenk Martine
    [dans rep de test]$
    

    OK !

19.1. Conclusion

Nous avons vu comment créer et utiliserune base de données pour hsqldb.

Avec Database Manager nous disposons d'un bon outil visuel pour gérer les données d'une base.

[Note]Bon à savoir

Vous trouverez dans le répertoire doc le fichier hSqlSyntax.html . Il contient la syntaxe SQL compatible avec hsqldb.

Le répertoire demo contient le fichier TestSelf.txt qui fourmille d'exemples d'ordres SQL compatibles avec hsqldb.

hsqldb.jar tient sur une disquette, n'hésitez pas à le transporter chez vous ! (que vous soyez sous Windows ou Linux).

20. TP avec JDBC et HypersonicSQL

Nous souhaitons faire évoluer notre application de gestion des ccontacts, en la faisant dépendre non pas d'un fichier mais d'un SGBDR.

Le diagramme de classe ci-dessous montre cette dépendance (à comparer avec le précédent)

Figure 39. Diagramme de classes de l'application à développer

Diagramme de classes de l'application à développer

Si vous analysez bien la différence entre les deux versions (fichier texte et base de données), vous vous appercevrez qu'elle se situe à deux endroits : au niveau du navigateur de fiches : NavigatorContactFic et NavigatorContactDB et au niveau du Contact (un attribut identifiant id a été ajouté).

Vous remarquerez également - ceci est très important - que ces deux classes ont les mêmes déclarations d'opérations (entête de méthode) .

On effectuera un premier changement sur la version fichier de notre application en ajoutant un attribut id aux caractéristiques d'un contact. La valeur de cet attribut doit être unique pour un contact (pas de doublons).

Donc, du point de vue de l'objet (la classe) qui utilise un navigateur de fiches, seule la construction du navigateur (par new) change. Exemple :

  • Version avec Fichier texte

       ...
       public JApplication(String titre) throws Exception{
         super(titre);
         nav = new NavigatorContactFic("document.txt");
         init();
         contact=nav.courant();
         updateView();
       }
       ...
     
  • Version avec base de données

    ...
    public JApplication(String titre) throws Exception{
      super(titre);
      nav = 
        new NavigatorContactDB("jdbc:hsqldb:file:/chezmoi/ici/labase", "sa","");
      init();
      contact=nav.courant();
      updateView();
     }
    ...
    

Finalement, pour que ce soit aussi transparent, il faut concevoir une interface commune (un type) entre ces deux classes ( NavigatorContactFic et NavigatorContactDB ). C'est très facile car elles ont les mêmes opérations ! (à quelques aménagements près, comme expliqué plus loin).

Rappelez vous, une interface est une classe déclarant des "méthodes sans leur corps" (des opérations).


import java.io.*;
import java.util.*;

interface NavigatorContact {

public Contact courant();
public boolean isLast();
public boolean isFirst();
public int nbClients();
public Contact suivant();
public Contact precedent();
public Contact premier();
public Contact dernier();
public void ajouter(Contact c);
public void supprimer(Contact c);
public void sauvegarde() throws FileNotFoundException, IOException; 
} 

Du coup, la déclaration de notre NavigatorContactFic devient :

public class NavigatorContactFic implements NavigatorContact {
  ...
  // comme avant
} 

idem pour notre nouveau navigateur :

public class NavigatorContactDB implements NavigatorContact {...} 

La méthode sauvegarde aura un corps vide dans NavigatorContactDB , car toutes les opérations mis à jour seront immédiatement répercutées sur la base (via des requêtes SQL)

Dans la classe JApplication, la déclaration du navigateur est :

// avant : private NavigatorContactFic nav; 
private NavigatorContact nav; 

Bon, il ne vous reste plus qu'à implémenter cela, et "coller les morceaux". Ce n'est pas forcemment un travail facile, réalisable en une séance de TP. Ce sera donc l'objet d'un mini-projet de fin d'année.

Bonne programmation.

20.1. Projet à rendre

Ce projet est à rendre à la rentrée de septembre 2005 pour les étudiants prenant l'option DA, au plus tard pour le 09/septembre/2005.

Le projet sera composé de deux parties : rapport et code

Rapport écrit - format HTML - sur 10 points

  • Critères d'évaluation

    • Description de l'organisation de votre travail (les étapes, qui a fait quoi)

    • Diagramme UML de l'ensemble. Vous commenterez les relations.

    • Compréhensible (pour une grande partie) par un lecteur non informaticien.

    • Bilan.

Code source sur 10 points

  • Critères d'évaluation

    • Code suffisamment bien commenté

    • Originalité de vos apports personnels

    • Architecture de l'ensemble

    • Respect des conventions : Indentation, choix de nommage, mise en page... Utilisez Eclipse !