Travaux Pratiques n°4
Table des matières
Objectifs (1h30)
- Savoir déclarer des exceptions
- Savoir émettre des exceptions
- Savoir intercepter des exceptions
- Savoir lire dans un fichier
- Savoir écrire dans un fichier
Rendu du travail
Le TP doit être rendu en fin séance, quel que soit son état d'avancement. Envoyez le source des classes (.java) ainsi que les classes compilées (.class) dans une archive zip (voir les instructions du TP1b et veiller à bien ajouter les sources !) à Gauthier Picard à la fin, avec comme sujet [LCPOO] TP4 <nom1> <nom2>. L'absence d'envoi sera sanctionnée dans la note finale.
Exceptions
Déclaration des exceptions levées
Une méthode doit déclarer dans sa signature toutes les
classes d'exceptions qu'elle peut émettre. La liste des classes
d'exceptions susceptibles d'être émises est indiquée juste
après le mot-clé throws
en séparant chaque classe
par une virgule. Par exemple, la méthode parseInt
de la classe Integer
transforme une chaîne de
caractères en un entier mais peut renvoyer une exception de la
classe NumberFormatException
si la chaîne n'est
pas transformable. La méthode est donc déclarée ainsi :
public static int parseInt(String s) throws NumberFormatException { ... }
Classes d'exception
Les exceptions sont des objets, donc des instances de
classe. Ces classes sont appellées classes d'exception. Une
classe d'exception est une classe qui hérite directement
ou indirectement de la classe java.lang.Exception
. Il
existe un grand nombre d'exceptions, dont vous pouvez trouver
les descriptions dans l'API Java à partir de
java.lang.Exception
.
Nous pouvons par exemple créer notre propre classe
d'exception pour la classe HelloWorld
, et
notamment la méthode getMessage
qui peut être
appelée avec un paramètre incohérent (indice de message
erroné). Nous utilisons la notion d'héritage afin de définir notre classe d'exception
comme un cas particulier d'Exception (voir l'API pour plus
d'information) :
package fr.emse.helloworld; /** * Une exception indiquant que l'indice du message demandé est erroné. * * @author Gauthier Picard */ public class NonValidMessageIndexException extends Exception { /** A constructor*/ public NonValidMessageIndexException() { super("The requested message index is erroneous."); } }
Émission d'une exception
L'émission d'une exception dans le corps d'une méthode passe par deux étapes :
- L'instanciation d'un objet en appelant le constructeur d'une classe d'exception
- L'envoi de cette exception par le mot-clé
throw
Par exemple, la méthode getMessage
de la classe
HelloWorld
doit prévoir d'émettre une exception
dans le cas où l'on tente d'accéder à un message dont l'indice
n'est pas valide (inférieur à 0 ou supérieur au nombre de
messages). Pour signaler cette erreur, nous utilisons la classe
d'exception créée précédemment. La méthode s'écrira comme suit :
/** * get the message at index i * * @param i the index of the message to get * @exception NonValidMessageIndexException if the index is erroneous */ public String getMessage(int i) throws NonValidMessageIndexException { if ((i < 0) || (i > messages.size() - 1)) throw new NonValidMessageIndexException(); return messages.get(i); }
Traitement d'une exception
Il y a deux moyens de traiter une exception émise par une méthode appelée :
- La propagation qui consiste à interrompre la méthode en cours pour renvoyer l'exception à sa propre méthode appelante. Dans ce cas, la méthode en cours doit déclarer la classe d'exception concernée comme étant envoyable.
- Le traitement local qui consiste à doter la méthode en cours d'un bloc d'instruction à exécuter lorsque l'exception est interceptée.
Pour traiter localement une exception, il faut mettre une
partie du code "sous écoute" par le mot-clé try
et
indiquer les classes d'exceptions attendues par le mot-clé
catch
.
Entrées/sorties en Java
Le chapitre 7 du livret de cours présente les principales classes utiles pour la lecture et l'écriture dans des fichiers.
Ce TP vous permettra de mettre en oeuvre les concepts vus en TD sur les exceptions et ceux issus du livret de cours sur les entrées/sorties. En effet, les méthodes des classes d'entrées/sorties lèvent souvent des exceptions dûes par exemple à l'impossibilité de trouver un fichier ou d'écrire en mémoire.
Ouverture d'un fichier à partir de son nom
Pour ouvrir un flux d'entrée de données à partir d'un fichier, une classe assez pratique est FileInputStream. Pour pouvoir écrire dans un flux de données de sortie, dans un fichier, la classe FileOutputStream est la plus indiquée.
Exercice 1
- Consultez l'API des classes FileInputStream et FileOutputStream
- Créez une classe dans Eclipse, et ajoutez lui une méthode
main
qui permet d'ouvrir des flux d'entrée et de sortie à partir d'arguments passés en paramètre du programme. Pour info, pour passer un argument en paramètre de la classe sous Eclipse, faire :- Clic droit sur la classe
- Run As > Run Configurations…
- Créez une nouvelle configuration (icône avec un "+") pour votre classe
- Dans l'onglet Arguments, dans le champs Program Arguments, ajoutez les arguments nécessaires (ici le nom du fichier).
- Créez un fichier "source.txt" à la racine de votre projet et ajoutez-lui un texte quelconque. Il servira pour vos tests et la suite du TP.
Lecture de données dans un flux d'entrée
Une fois qu'un flux d'entrée (resp. sortie) est ouvert on peut
lire depuis (resp. écrire sur) ce flux grâce aux méthodes de la classe
du flux. Par exemple, pour lire des octets, on peut utiliser la méthode
read()
d'un InputStream
. Si l'on veut lire des
données plus complexes, il faut passer le flux d'entrée en paramètre du
constructeur d'une autre classe prévue pour le formatage des données.
Par exemple, la méthode readLine()
de la classe BufferedReader permet de lire ligne par ligne. L'exemple suivant montre l'utilisation d'un BufferedReader
pour lire ligne par ligne un fichier texte contenant un matrice d'objets séparés par des espaces :
FileReader fileReader = new FileReader("matrice.txt"); // creation du lecteur pour le fichier BufferedReader reader = new BufferedReader(fileReader); // creation du lecteur bufferise pour le lecteur de fichier while (reader.ready()) { // tant qu'il y a des donnees String[] line = reader.readLine().split(" "); // creer un tableau de chaines correspondant à la ligne en cours for (String s : line) // pour chaque chaine System.out.print(s); // afficher la chaine System.out.println(); // sauter une ligne en fin de ligne du fichier }
De même, la classe Scanner
possède de nombreuses méthodes pour lire des entiers, des chaînes, etc. L'exemple suivant est identique au précédent avec l'usage d'un Scanner
(par défaut le Scanner
considère les espaces comme des séparateurs pour la méthode next()
) :
Scanner fileScanner = new Scanner(new File("matrice.txt")); // creer un scanner pour le fichier while (fileScanner.hasNextLine()) { // tant qu'il y a des lignes à scanner Scanner lineScanner = new Scanner(fileScanner.nextLine()); // creer un scanner pour la ligne en cours while (lineScanner.hasNext()) // tant qu'il y a une chaine a scanner dans la ligne System.out.print(lineScanner.next()); // afficher la chaine System.out.println(); // sauter une ligne en fin de ligne du fichier }
Cryptage simple de fichiers textes
Cryptage avec clé unique entière
Afin de manipuler les fonctions d'entrées/sorties en Java, nous allons implémenter un codeur/décodeur de flux (qui pourront être des fichiers).
Ici, (en)coder/crypter un fichier signifie le transformer grâce à une fonction paramétrée par des clés qui permettront de le déchiffrer. Nous allons nous intéresser à une fonction de codage très simple qui :
- encode caractère par caractère ;
- utilise une seule clé, passée en paramètre de l'encodeur ;
- utilise une fonction simple : le XOR (ou OU exclusif).
Pour plus d'information sur le XOR, vous pouvez consulter la page Wikipedia sur le sujet.
En Java le XOR, s'écrit grâce au caractère "^". Ainsi pour coder
un octet (représenté par un int
), on effectuera
l'instruction suivante :
encoded = source^key;
avec encoded
qui est la donnée codée, source
qui est la donnée à coder, et key
qui est l'entier
représentant la clé de cryptage.
Par exemple, avec source
= 155 en décimal (soit
10011011 en binaire) et key
= 123 en décimal (soit 01111011
en binaire), on a :
encoded
= 10011011 ^ 01111011 = 11100000 en binaire (soit 224 en décimal)
n peut également vérifier que 224 ^ 123 = 155, donc avec la même clé on peut déchiffrer le message crypté pour retrouver le message original.
Exercice 2
- Ecrire une classe
XOREncoder
permettant d'encoder des entiers grâce à une méthodeint encode(int data)
. La clé sera passée en paramètre du constructeur de cette classe. La clé doit être comprise entre 0 et 255. Vérifiez que cette donnée est correcte sinon, lancez une exception adéquate et explicite. - Ecrire une méthode
encodeStream(InputStream input, OutputStream output)
dans la classeXOREncoder
permettant de coder un flux d'entrée passé en paramètre (un fichier, par exemple) dans un flux de sortie passé en paramètre (un autre fichier). La lecture s'effectuera octet par octet. Gérez les exceptions avec parcimonie, et renvoyez les messages explicites si nécessaire. - Modifiez le
main
précédent pour crypter le fichier passé en paramètre dans un autre fichier également passé en paramètre, en utilisant la classeXOREncoder
. La clé de cryptage est également passée en paramètre du programme. - Cryptez un fichier "source.txt" dans un fichier "destination.txt". Consultez le contenu de "destination.txt", puis cryptez le dans un fichier "verification.txt". Vérifiez le contenu de "verification.txt".