Travaux Pratiques n°4

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 :

  1. L'instanciation d'un objet en appelant le constructeur d'une classe d'exception
  2. 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 :

  1. 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.
  2. 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

  1. Consultez l'API des classes FileInputStream et FileOutputStream
  2. 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).
  3. 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

  1. Ecrire une classe XOREncoder permettant d'encoder des entiers grâce à une méthode int 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.
  2. Ecrire une méthode encodeStream(InputStream input, OutputStream output) dans la classe XOREncoder 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.
  3. 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 classe XOREncoder. La clé de cryptage est également passée en paramètre du programme.
  4. 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".

Gauthier Picard