TP 7   2324v2

Il est fortement conseillé de passer sous Linux dès le début de ce TP.

A. Les exceptions

1) Vous avez tous vu votre programme s’arrêter brutalement suite à une exception, par exemple :

Ces trois classes (et bien d’autres) héritent de la classe RuntimeException qui elle-même hérite de la classe Exception.
Les exceptions des sous-classes de RuntimeException sont en général des erreurs du programmeur qu’il suffit de corriger ; donc, nul besoin de prévoir dans le programme de quoi les traiter pendant l'exécution. Elles sont appelées « exceptions non vérifiées » (unchecked exceptions).

2) Par contre, les exceptions qui ne sont pas des sous-classes de RuntimeException (donc seulement des sous-classes d'Exception) sont dites « exceptions vérifiées » (checked exceptions). Elles sont généralement dues soit à une erreur de l'utilisateur, soit un problème du système d'exploitation. Dans ce cas, si dans une méthode m on écrit une instruction susceptible de lancer une exception vérifiée, le compilateur java nous oblige à choisir entre 2 solutions :
- soit on traite l'exception dans la méthode m (c'est-à-dire on met en place ce qu'il faut pour détecter si l'exception survient et on indique ce qu'il faut faire si elle survient)
- soit on indique que la méthode m est elle-même susceptible de lancer cette exception (ce qui reporte le problème à la méthode qui a appelé m)
Ce choix s'appelle catch or specify !, et le message d'erreur du compilateur dans ce cas est : Must be caught or declared to be thrown.

3) En java, une exception est une instance (un objet) d’une sous-classe d’Exception qui est créée par la machine virtuelle Java en cas de problème. On peut récupérer cette instance et la traiter si on le souhaite, pour éviter que le programme ne s’arrête brutalement, ou au moins, pour afficher un message clair avant que le programme ne s’arrête.

Il existe aussi une troisième sorte d'exception appelée Error qui correspond généralement à une erreur système grave, ne permettant pas à la JVM de continuer à s'exécuter, par exemple StackOverflowError en cas de récursion infinie, ou plus rarement OutOfMemoryError.
Il ne sert évidemment à rien de les "attraper" pour les "traiter" puisque la JVM ne pourra pas continuer.

4) Traiter une exception, 1ère partie : Comment mettre en place la détection d'exception ?
Il suffit de placer la ou les instructions susceptibles de lancer une exception dans un bloc try :
try {
    instructions à vérifier
}
À partir de là, dès qu'une de ces instructions lance une exception, on sort du bloc try et les instructions suivantes ne seront jamais exécutées.

5) Traiter une exception, 2ème partie : Comment spécifier ce qu'il faut faire en cas d'exception ?
Il suffit d'ajouter (juste après le bloc try) un bloc catch :
catch ( final Exception pE ) {
    instructions en cas d'exception
}
Dans ces instructions, on peut utiliser l'objet pE (c'est-à-dire l'exception qui a été lancée dans le bloc try et "rattrapée" ici). En effet, il contient des informations sur les circonstances de l'exception et des méthodes de la classe Exception peuvent être appelées sur cet objet (getMessage() par exemple).

6) Comment spécifier qu'on ne veut attraper qu'un certain type d'exceptions ?
Il suffit de préciser le type du paramètre pE dans le catch :
catch ( final FileNotFoundException pE ) {
    instructions en cas d'exception "fichier non trouvé"
}
Si une exception différente de FileNotFoundException survient, elle est répercutée sur la méthode appelante.


7) Comment spécifier qu'on veut attraper plusieurs types d'exceptions ?
Il suffit d'écrire plusieurs blocs catch de suite juste après le bloc try.
Attention !
L'ordre des blocs catch peut avoir de l'importance si les classes d'exceptions concernées héritent l'une de l'autre. En effet, c'est seulement si l'exception n'est pas attrapée par le premier bloc catch, qu'elle est "proposée" au bloc catch suivant.
Exemple : sachant que la classe FileNotFoundException (exception de fichier non trouvé) hérite de la classe IOException (exception d'entrée/sortie plus générale), il faut placer le catch de FNFE avant celui d'IOE. Si l'exception qui survient est une FNFE, c'est le 1er bloc qui traite, sinon si c'est une IOE, c'est le 2ème bloc qui traite, sinon s'il n'y a pas de 3ème bloc, l'exception remonte à la méthode appelante.
Que se passerait-il si on inversait les 2 blocs catch ?

8) Comment spécifier qu'on ne veut pas traiter l'exception ?
Il suffit d'ajouter à la fin de la signature throws Exception, ce qui signifie "est susceptible de lancer une Exception" et ce qui forcera la méthode appelante à soit traiter l'exception, soit déclarer qu'elle ne la traite pas (on peut bien sûr spécifier une sous-classe d'Exception, et on peut même spécifier plusieurs exceptions séparées par des virgules).

9) Comment peut-on volontairement lancer une exception ?
Il suffit de tester une condition et de créer/lancer une instance d'Exception. Par exemple :
if ( pN < 0 ) throw new IllegalArgumentException( "Paramètre négatif : "+pN );
Attention à ne pas confondre l'instruction throw (Lance cette instance d'exception !) avec la déclaration throws (est susceptible de lancer une exception de cette classe). La classe IllegalArgumentException existe déjà dans le JDK (voir la hiérachie des classes d'exception).

10) Comment faire pour qu'une instruction soit toujours exécutée,  qu'il y ait une exception ou pas  ?
Imaginons que vous écrivez une méthode qui recopie un fichier dans un autre (bon exercice !). S'il survient une exception au beau milieu de la copie, il faut quand-même fermer les 2 fichiers, de la même façon que si tout s'était bien passé. Pour spécifier des instructions qui seront exécutées quelle que soit la manière de sortir de la méthode (qu'il n'y ait pas d'exception, qu'il y ait une exception traitée, ou qu'il y ait une exception non traitée), il suffit de regrouper ces instructions dans un bloc finally situé juste après le ou les bloc(s) catch :
finally {
    instructions à exécuter dans tous les cas (à la sortie du bloc try)
}

11) Pour aller plus loin
Il est aussi possible de créer ses propres classes d'exception, d'y stocker des informations, et bien d'autres choses encore.
Pour plus de détails (s'il n'y a pas de cours 7) : cliquer ici. And for the official tutorial : click here.


B. Exercices sur les exceptions

Exercice 1 (y consacrer suffisamment de temps pour tout bien comprendre)

1) Créez un nouveau projet BlueJ TP7 et créez une nouvelle classe avec n'importe quel nom.
2) Ouvrez l'éditeur sur cette classe et remplacez le code par copier/coller du contenu du fichier FinallyDemo.java ci-joint.
3) Compilez la classe et exécutez la méthode essai.
4) En mettant côte à côte la fenêtre avec le code Java et la fenêtre Terminal de BlueJ, comprenez le déroulement du programme ligne à ligne, en étant capable d'expliquer le pourquoi de chaque affichage. (n'hésitez pas à demander si vous ne comprenez pas quelque chose dans ce programme ou si vous ne comprenez pas vraiment pourquoi un affichage survient -- ou ne survient pas -- à tel ou tel moment, notamment "maMethode: bloc finally")
- Pourquoi l'instruction break; des case 3 et 4 ne peut jamais être exécutée ?
- Quelle est la différence entre pE.toString() et pe.getMessage() ?

Aide : Si vous n'êtes pas à l'aise avec l'instruction switch , lisez ces explications jusqu'au bout.



Exercice 2
1) Créer une nouvelle classe LireFichier qui ne contiendra aucun attribut (et qui n'a pas vocation à être instanciée) et deux procédures (de classe et non d'instance) nommées essai et lire acceptant chacune un paramètre de type String. La procédure essai se contentera d'appeler lire en lui transmettant son paramètre, puis d'afficher "Cette ligne doit toujours s'afficher." .

2) La procédure lire (qui a vocation à devenir privée) devra lire le fichier de texte (dont le nom a été passé en paramètre) ligne par ligne, et afficher chaque ligne précédée de son numéro (en commençant à 1) et des 2 caractères ": ".
2a. Après avoir importé java.util.Scanner et java.io.File, l'instruction
Scanner vSc = new Scanner( new File( nomFichier ) );
ouvre en lecture le fichier dont le nom se trouve dans la String nomFichier.
2b. Compiler. Résoudre le problème d'exception en annonçant juste qu'on ne veut pas traiter l'exception (laquelle * ?) dans lire (relire le cours partie 8).
* Ne pas utiliser la classe Exception, trop générale, bien lire le message d'erreur du compilateur.
2c. Recompiler. Tiens ! C'est le même message, mais pas au même endroit, et ce coup-ci, résoudre le problème en traitant l'exception dans essai (relire le cours parties 4) 5) 6) pour afficher un message indiquant que tel fichier n'a pas été trouvé. Recompiler.
2d. Tester avec un fichier qui n'existe pas, puis avec le fichier "LireFichier.java" (on est sûr qu'il existe).
2e. Ensuite, chaque appel à vSc.nextLine() lit et retourne une ligne du fichier (donc quel type de valeur ?), mais il faut avant chaque appel vérifier que vSc.hasNextLine() renvoie bien VRAI.
Au vu de ce fonctionnement, comprenez-vous pourquoi/comment la classe Scanner implémente l'interface Iterator<String> ?

3) Compiler/tester avec un fichier qui n'existe pas, puis avec le fichier "LireFichier.java".
Si vous avez un try/catch dans la procédure Lire, vous vous êtes trompé ; relisez l'énoncé à partir du 2b).

4)
4a. Une fois que tout marche parfaitement, ajouter la ligne int vN = 1 / 0; à la fin du bloc try (voir A.4). Compiler, tester.
4b. Constater que le programme s'arrête "salement" malgré le try/catch, car il s'agit ici d'une autre exception que celle qui est attrapée.
Ajouter maintenant un second bloc catch( final Exception pE ) et lui faire afficher pE. Compiler.
4c. Tester les 2 cas d'exception.
4d. (optionnel) Remplacer la ligne du 4a. par double vN = 1. / 0;
Si vous ne comprenez pas pourquoi le comportement est différent, demandez à l'intervenant.


C. Méthode main et ligne de commandes

Les objectifs sont :
- maîtriser l'écriture de la méthode "spéciale" main
- savoir exécuter un programme java indépendamment de BlueJ
- savoir développer un programme java sans BlueJ 

Cette partie devra se dérouler entièrement sous Linux (rappel des principales commandes).

I. La méthode main

I.1 Comment faire maintenant pour exécuter le "programme" LireFichier en dehors de BlueJ, c'est-à-dire à partir de la ligne de commande dans une fenêtre Terminal ? (ici sous Linux, mais tout aussi valable sous Windows ou MacOS)

Pour lancer la JVM, il suffit de taper la commande java MaClasse (en étant dans le répertoire du projet BlueJ ; tapez les commandes cd nécessaires pour cela).
Essayez !  Que se passe-t-il ?  La JVM ne sait pas quelle méthode lancer dans cette classe (après tout, il y en a deux !). Elle pourrait lancer la première, mais nous avons que l'ordre des méthodes dans une classe est non significatif.
La convention est que la commande java MaClasse lance forcément la procédure main qui doit se trouver dans MaClasse, d'où le message d'erreur que vous avez obtenu puisqu'elle ne l'a pas trouvée dans la classe LireFichier.

I.2 Nous allons donc devoir écrire cette méthode. Attention, il pourrait exister plusieurs méthodes main avec des signatures différentes. La convention fixe donc la signature exacte de la méthode que la JVM cherchera automatiquement à lancer :
- comme l'appel provient de l'extérieur de la classe LireFichier ==> public
- comme l'appel se fait directement sur la classe LireFichier (sans avoir besoin d'en créer une instance) ==> static
- comme nous avons vu qu'il s'agissait d'une procédure ==> void
- comme son nom est imposé ==> main
- l'information qui manquait était la liste de paramètres ; comme on veut pouvoir passer des arguments sur la ligne de commande (en écrivant par exemple java LireFichier nom0 arg1 arg2 arg3), le paramètre imposé est un tableau de String.
Quel que soit le type de données que vous souhaitez récupérer sur la ligne de commande et même si vous n'avez pas prévu de passer des arguments sur la ligne de commande, ce paramètre est obligatoire pour que la signature soit toujours la même ; on obtient donc :
public static void main( final String[] pArgs )

I.3 Ajoutez la méthode ci-dessus qui, dans un premier temps, se contentera d'appeler la méthode essai en lui passant en paramètre le fichier à lire (qu'elle aura trouvé en premier argument de la ligne de commande).
Il est à noter que le déroulement de la procédure main ne sera en rien perturbé si par malheur le fichier demandé n'existe pas.

I.4 Maintenant fermez BlueJ. Et exécutez votre programme directement sur la ligne de commande, d'abord sans passer d'arguments, puis en passant un fichier inexistant, enfin en passant un fichier existant.
Améliorez l'affichage si besoin est (par exemple, est-il normal de déclencher une exception lorsqu'on oublie de fournir le nom du fichier à afficher ?).
Attention !  BlueJ compile en java 11. Si la version par défaut sous Linux est java 8 (ce que l'on peut savoir en tapant la commande java --version), il vous faut faire la manipulation suivante :
Taper dans la fenêtre Terminal  alias java11=/opt/jdk-11.0.2/bin/java  (ou éventuellement un autre chemin selon l'endroit où le JDK est installé, c'est à vous de chercher !), ne plus fermer ce terminal et, pour exécuter un programme compilé par BlueJ, utiliser la commande java11 plutôt que la commande java.

I.5 Dernière chose : comme dans d'autres langages comme le C (où main est une fonction retournant un entier), et bien que main soit une procédure en java, il est possible de retourner une valeur entière au système d'exploitation "appelant" votre programme, en utilisant la méthode System.exit(unEntier); avec pour convention de retourner 0 quand tout s'est bien passé ou un numéro d'erreur sinon. Attention, cette procédure termine immédiatement le programme !
Il est ensuite possible pour le système d'exploitation de tenir compte de cette valeur pour choisir la commande qu'il va lancer après votre programme (si cela a bien été prévu dans le script sous Linux ou MacOS, ou dans le fichier .BAT sous Windows/DOS).

I.6 Donc, transformez la procédure essai en une fonction qui retourne (dans tous les cas !) un entier : le code d'erreur (0 si tout se passe bien, 1 si le fichier n'est pas trouvé, 2 pour un autre cas d'exception).
Et modifiez le main pour qu'il renvoie ce code d'erreur, ou bien 3 s'il n'y a pas le bon nombre d'arguments sur la ligne de commande (c'est-à-dire 1).
Sous Linux, vous pouvez afficher le code d'erreur retourné par le dernier programme exécuté en tapant la commande : echo $?   Essayez !
Et plus intéressant encore, vous pouvez le tester dans un script !


II. Le développement sans BlueJ

II.1 Il y aura des situations professionnelles où vous devrez développer "à la ligne de commande" et c'est de plus un bon entrainement pour la programmation en C. Il va nous falloir remplacer plusieurs fonctionnalités de BlueJ : éditer le texte de notre programme java, le compiler, et l'exécuter. Nous savons déjà faire la 3ème fonctionnalité, voyons les 2 premières.

II.2 Pour éditer votre classe, vous devez utiliser un éditeur de texte sur votre fichier MaClasse.java ; vous avez peut-être le choix entre nedit, gedit, nano, emacs, vi, vim, kwrite, notepad, notepad++, notepadqq, etc...
Attention ! Ne pas utiliser de "traitement de textes" comme Word dans Office ou Writer dans OpenOffice ou GoogleDocs en ligne, car ces logiciels ajoutent des informations de formatage (gras, couleur, italique, police de caractères ...) qui sont malvenues dans le source d'un programme java.
Choisissez un éditeur de texte et modifiez la procédure Lire pour qu'elle aligne correctement les lignes du fichier (au moins s'il n'en comporte pas plus de 99).
Si vous ne voyez pas le problème d'alignement, regardez mieux les lignes numéros 9 et 10...
Et si elles sont déjà alignées, bravo ! mais dans ce cas, faites-en sorte que toutes les lignes soient alignées tant qu'il y en a moins de 1000 dans le fichier.
Sauvegardez votre fichier et passez au point suivant.

II.3 Pour compiler une classe, il suffit de taper la commande javac MaClasse.java (javac veut dire 'java compiler', c-à-d compilateur java).
Corrigez vos erreurs dans l'éditeur de texte s'il y a lieu, sauvegardez, recompilez, puis exécutez !
Attention !  S'il n' y a aucune erreur de compilation, cette commande n'affiche rien !

II.4 Recommencez les points II.2 et II.3 jusqu'à ce qu'il n'y ait plus d'erreur. Puis retestez votre programme comme au I.4.