TD1 - Entrées/Sorties

De $1

Introduction

Ce td/tp fait un rappel sur la manipulation d'entrées/sorties indispensable pour la gestion de plugin. La deuxième partie constitue un rappel sur les notions de composition et d'héritage. La troisième partie s'intéresse à un patron de structure appelé Adaptateur. D'une manière générale, il ne faut pas perdre de vue la documentation java :

Pour commencer, nous pouvons nous contenter de faire du java avec un simple éditeur de texte. Vous utiliserez donc un simple éditeur de texte et vous compilerez en ligne avec javac. Vérifiez que vous disposez d'un JDK (tapez javac dans une console).

Exercice 1 : la classe File

La classe File (paquetage java.io) ne réfère pas exactement à un fichier. C'est soit le nom d'un fichier particulier soit un ensemble de noms de fichiers dans un répertoire. Dans ce cas, la méthode String[] list() de la classeFile renvoie un tableau de chaînes de caractères (type String).  

Choses à connaitre :

  • Un objet de type File "n'évolue" pas
  • Le chemin (et le nom) passé en paramètre de création d'un fichier (File f = new File("nom");) ne changera pas. 
  • L'objet de type File permet de tester le fichier avec les méthodes (boolean exists() ; boolean isFile() ; boolean isDirectory() ...), de créer (void mkdir() ; void mkdirs() ; ...), de renommer (void renameTo(File dest)) ou encore d'effacer (void delete() ; void deleteOnExit() ; ).

Pour une information exhaustive et détaillée sur les fichiers, consulter l'API de Sun.  

1.a) Création d'un programme qui liste le contenu d'un répertoire, avec filtrage

L'exercice qui suit est inspiré du livre « Thinking in Java » (http://mindview.net/Books/TIJ4).

Cet exercice est utile si nous visons la scrutation d'un répertoire de dépôt de plug-ins.

Travail à faire :

  1. Réaliser un programme qui liste le contenu d'un répertoire (le répertoire "."). Pour lister le contenu d'un répertoire (instance de la classe File), il suffit d'utiliser la méthode String[] list() de File. Sans paramètre, elle renvoie tout le contenu du répertoire.
  2. Faire un parcours en profondeur: quand un répertoire contient un autre répertoire, ce dernier est listé également.
  3. Ajouter à votre programme le support pour les filtres. En effet, il est possible de filtrer ce listing, à la manière d'un "ls *.java", en utilisant String[] list(FilenameFilter filter)

Pour le second point, vous allez créer un filtre (interface FilenameFilter) que nous allons mettre en œuvre de trois façons différentes :

  • Dans une classe indépendante, 
  • Dans une classe interne anonyme (anonymous inner class - une méthode qui renvoie une « instance » d'une interface) 
  • Dans une classe interne anonyme en argument de la méthode list() (une « instanciation » d'une interface). 

Les classes internes sont un outil supplémentaire pour permettre de disposer de la puissance de l’héritage via la composition (ceci est en complément des interfaces). Les classes anonymes permettent de cacher tout en assurant une certaine séparation d’une partie de l'implémentation, cependant, cela peut vite devenir illisible. A utiliser judicieusement.  VOIR LE COURS pour des exemples de classes anonymes.

En paramètre, de votre programme, il y aura un argument optionnel : un filtre (par exemple, symboliquement .class). Pour ce filtrage, l'outil le plus puissant est l'utilisation d'expressions régulières.

1.b) Quelques exercices sur les expressions régulières

Les expressions régulières sont implémentées dans le package java.util.regex, et en particulier dans les classes Pattern et Matcher. Les expressions régulières s'utilisent de la façon suivante :

  1. créer un pattern : Pattern p = Pattern.compile(regex); // regex est une chaîne représentant une expression régulière ;
  2. utiliser un Matcher pour chaque chaîne : Matcher m = p.matcher("chaine") ;
    la classe Matcher permet de réaliser différentes opérations dont boolean matches() pour savoir si l'expression régulière s'applique entièrement à la chaîne et lookingAt() pour savoir si la chaîne (depuis son début) correspond à l'expression régulière.

Les expressions régulières sont très variées (et assez standards) :

  • les caractères normaux : le caractère "x" ne notera x, \n pour newline, ...
  • les classes de caractères : "." représente n'importe quel caractère, [abc] n'importe quel caractère parmi "a", "b", et "c", [a-n] n'importe quel caractère compris entre a et n, \s à un caractère d'espacement, \w à un mot composé de caractères parmi [a-zA-Z_0-9], etc.
  • Opérateurs : ab pour a suivi de ba|b pour a ou b(a) pour un groupe. a et b sont ici soit des caractères soit des groupes.
  • Limites : ^ pour le début de la ligne, $ pour la fin (comme sous vi)
  • Répétition : X? pour X une fois ou zéro ; X* pour X zéro ou plusieurs fois ; X+ pour X une fois ou plus ;... Sous cette forme tout ce qui convient est pris (Greedy). En ajoutant un ? (par exemple X + ?) seul ce qui est suffisant pour satisfaire l'expression régulière est pris.

Exemples de filtres :

  •  .*class : toutes chaînes finissant par class
  •  .*\.class : toutes chaînes finissant par .class
  •  .*\$.* : toutes chaînes contenant $
  •  .*\$[0-9]*\.class : toutes chaînes finissant par $<un nombre>.class

Pour mettre en œuvre le filtrage, il faut utiliser l'interface FilenameFilter et sa méthode « public boolean accept(File dir, String name) ; » qui doit renvoyer true si le fichier de nom « name » est dans le répertoire « dir ».

Note : il ne s'agit pas ici de maîtriser les expressions régulières, mais plutôt d'en comprendre le fonctionnement. Il vous incombe de regarder plus en détail, par exemple les fonctionnalités de recherche et de remplacement qui ne sont pas évoquées ici.

Exercice 2 : entrées - sorties

L'objectif est de vous faire (re-)découvrir les entrées-sorties en Java. En Java, les entrées sorties sont regroupées dans les packages java.io (input-output) et java.nio (new input output). L'objectif de java.nio (JDK 1.4) est d'améliorer l'efficacité des programmes (vitesse) en introduisant notamment les notions de canaux et de tampon (channel et buffer) qui sont assez bas niveau. La plupart des classes de java.io ont été réécrites ou remplacées par de nouvelles classes de java.nio. Nous ne traiterons pas de ces « nouveautés », pour en savoir plus sur java.nio lisez le chapitre 12 de « Thinking in java » ou consultez la documentation java. 

Les entrées-sorties reposent donc sur des flux d'octets, InputStream en lecture et OutputStream en écriture et sur des flux de caractères avec les Reader en lecture et les Writer en écriture. Toutes ces classes de java.iosont des classes abstraites. Ces classes proposent des méthodes read() et write() [soit des octets, soit des caractères, soit des int] et java disposent de classes plus évoluées pour lire/écrire des lignes, des double, etc.
Les sources d'entrées sorties sont multiples : des fichiers, des chaînes de caractères, etc. Pour les applications à multi processus et la communication entre deux processus, les <Pipe> (
PipeInputStream par exemple) sont fournies dans Java. (nous ne les étudierons pas).
Pour la plupart, ces méthodes peuvent lever des 
java.io.IOException qu'il vous faudra attraper (avec try/catch)! 

2.a) Exemple d'utilisation : lecture et écriture d'un fichier texte.

On veut écrire un programme Java (une classe SeLit) qui lit un fichier texte et l'affiche sur la console. Pour tester ce programme on essaiera de lire le code source du programme lui-même (le fichier SeLit.java).

Compléter le code ci-dessous:

import java.io.File;
import java.io.FileNotFoundException; 
import java.util.Scanner; 

public class SeLit {
   static void lecture(Scanner source) {

       while(source.hasNextLine()) {
           String s = source.nextLine();        
           System.out.println(\"LU:\"+s);
       }
   }

   static public void main(String[] args) { 
      // A compléter     
   }
}

Pour écrire dans un fichier texte, il faut utiliser conjointement les classes FileWriter et PrintWriter.

Travail à faire :

  1. Compléter la classe SeLit en ajoutant le code pour afficher le contenu d'un fichier texte (ex: SeLit.java) sur la console (System.out) en supprimant les lignes de commentaires commençant par //.
  2. Bonus pour les fortiches de la classe : on pourra, par exemple, utiliser une expression régulière pour lister plus d'un fichier à la fois (par ex : "*.java" listera l'ensemble des sources dans le répertoire courant).

2.b) Entrées et Sorties standards.

La classe java.lang.System propose un flot d'entrée standard (System.in) et deux flots de sortie standards (System.out et System.err). System.in est de type InputStream. Par exemple pour lire une chaîne sur l'entrée standard, il suffit d'écrire le code suivant qui utilise la classe Scanner : 

Scanner sc = new Scanner(System.in);
System.out.println(sc.nextLine());

Vous manipulez System.out depuis longtemps. Tout comme System.err, il s'agit d'instances de la classe PrintStream 

Chacun de ces flots peut être redirigé vers un autre flot ( un fichier par exemple). Bien sûr, il faut préalablement sauvegarder le flot initial, affecter le nouveau (avec les méthodes System.setInsetOut ou setErr et finalement restaurer le flot initial.

Travail à faire : modifier la classe SeLit pour rediriger la sortie standard vers un fichier (ex. Output.txt).

Exercice 3 - Le patron Adaptateur

La paquetage java.io propose plusieurs exemples d'Adaptateurs dont notamment les classes InputStreamReader et OutputStreamWriter qui adaptent des flots d'octets aux flots de caractères. Cet exercice sert à illustrer la mise en oeuvre du patron Adaptateur en recréant un équivalent simplifié de la classe OutputStreamWriter. Il est donc absolument interdit de l'utiliser directement !

3.a) MyStreamWriter

On suppose qu'on dispose d'un côté d'un objet de type OutputStream et de l'autre d'un programme qui sait (veut) écrire seulement dans des objets de type Writer. On veut faire une classe d'adaptation. Ecrire une classe MyStreamWriter qui offre une solution. 

  1. Bien étudier la documentation de la classe Writer avant de commencer. Il s'agit d'une classe abstraite. Quelles sont les méthodes à surcharger ?
  2. La difficulté principale consiste à transformer un char[] en byte[]. Il y a plusieurs façons de le faire. En effet, cela dépend de l'encodage utilisé pour coder les caractères (UTF-8, ISO-8859-1, ...). Selon l'encodage choisi, certains caractères sont codés sur plusieurs octets ! La classe CharSet permet d'encoder des CharBuffer et de décoder des ByteBuffer. Comment en tirer profit ?

3.b) WriterOutputStream

Sauriez-vous faire l'adaptation inverse ? C'est-à-dire adapter un objet de type Writer pour pouvoir l'utiliser avec des classes qui ne savent manipuler que des OutputStream !

Cela peut par exemple servir avec les méthodes System.setOut ou System.setErr pour utiliser un Writer alors qu'un PrintStream est imposé par l'API !

Correction du TP

Mots clés:
 
Images (0)
 
Commentaires (0)
Vous devez être connecté pour poster un commentaire.