TD1 - Entrées/Sorties

De $1

Introduction

Ce td/tp fait un rappel sur la manipulation des entrée/sortie et est indispensable pour la gestion de plugin. La deuxième partie constitue un rappel sur les notions de composition et d'héritage. D'une manière générale, il ne faut pas perdre de vue la documentation java :

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 File[] listFiles() de la classe File renvoie un tableau de Fichiers (type File).  

Choses à connaitre :

  • Un objet de type File "n'évolue" pas, il est immutable
  • 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 un répertoire (void mkdir() ; void mkdirs() ; ...),
    • de renommer le fichier ou le répertoire (void renameTo(File dest))
    • ou encore d'effacer le fichier/répertoire(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 (regarder la documentation de la méthode File[] listFiles()).
  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 File[] listFiles(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 (externe dans un fichier séparé), 
  • Dans une classe interne nommée, 
  • Dans une classe interne anonyme (anonymous inner class) en argument de la méthode listFiles() (une « instanciation » d'une interface),
  • Essayez aussi avec une classe anonyme embarquée dans la méthode (cf. cours).

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'implantation, 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) Version pour Java 7+

Depuis Java 7 il existe une alternative à la traversée "manuelle" d'une arborescence qui fait appel au patron de conception visiteur. Le code obtenu est beaucoup plus concis mais ne fonctionne que sur les versions récentes de Java.

  1. Reprenez l'exercice précédent en utilisant les classes Files et SimpleFileVisitor

 

1.c) 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" se 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 correspond à 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.io sont 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 {
   void lecture(Scanner source) {

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

   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 : 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).