Previous Up Next

B.5.5  Entrées-sorties formatées

B.5.5.1  Sorties formatées

Nous savons désormais écrire dans un fichier de nom name de façon efficace en construisant un FileWriter, puis un BufferedWriter.

  Writer out = new BufferedWriter (new FileWriter name) ;

Toutefois, écrire caractère par caractère (ou même par chaîne avec write(String str)), n’est pas toujours très pratique. La méthode print de System.out est bien plus commode. Malheureusement, les PrintStream de la bibliothèque sont très inefficaces. Par exemple, la copie du fichier a dans le fichier b par java Cat < a > b est environ vingt-cinq fois plus lente que java Cp2 a b !

Mais la méthode print est bien pratique, voici par exemple une méthode simple qui affiche les nombres premiers jusqu’à n directement dans System.out.

static void sieve(int n) {
  boolean [] t = new boolean [n+1] ;
  int p = 2 ;
  for ( ; ; ) {
    System.out.println(p) ; // Afficher p premier
    /* Identifier les multiples de p */
    for (int k = p+p ; k <= n ; k += p) t[k] = true ;
    /* Chercher le prochain p premier */
    do {
      p++ ; if (p > n) return ;
    } while (t[p]) ;
  }
}

L’application du crible d’Eratosthène est naïve, mais le programme est inefficace d’abord à cause de ses affichages. On s’en rend compte en modifiant sieve pour utiliser un PrintWriter. Les objets de la classe PrintWriter sont des Writer (flux de caractères) qui offrent les sorties formatées, c’est-à-dire qu’ils possèdent une méthode print (et prinln) surchargée qui allège un peu la programmation. Par ailleurs, les PrintWriter sont bufferisés par défaut.

  static void sieve(int n, PrintWriter out) throws IOException {
    boolean [] t = new boolean [n+1] ;
    int p = 2 ;
    for ( ; ; ) {
      out.println(p) ;
      for (int k = p+p ; k <= n ; k += p) t[k] = true ;
      do {
        p++ ; if (p > n) return ;
      } while (t[p]) ;
    }
  }

  public static void main(String [] arg) {
     ⋮
     try {
      PrintWriter out = new PrintWriter ("primes.txt") ;
      sieve(n, out) ;
      out.close() ;
    } catch (IOException e) {
      System.err.println("Malaise: " + e.getMessage()) ;
      System.exit(2) ;
    }
  }

La nouvelle méthode sieve écrit dans le PrintWriter out dont l’initialisation est rendue pénible par les exceptions possibles. Noter aussi que la sortie out est vidée (et même fermée) par la méthode main qui l’a créée. Il y a plusieurs constructeurs des PrintWriter, qui prennent divers arguments. Par exemple, si l’on avait voulu écrire dans la sortie standard, et non pas dans le fichier primes.txt, on aurait très bien pu « emballer » la sortie standard dans un PrintWriter, par new PrintWriter(System.out). Pour ce qui est du temps d’exécution des essais rapides montrent que le nouveau sieve (avec PrintWriter) peut être jusqu’à dix fois plus rapide que l’ancien (avec PrintStream). Nous insistons donc : System.out est vraiment très inefficace, il faut renoncer à son emploi dès qu’il y a beaucoup de sorties.

Les PrintWriter n’ont rien de magique, et il est également possible d’employer un BufferedWriter.

  static void sieve(int n, BufferedWriter out) throws IOException {
    boolean [] t = new boolean [n+1] ;
    int p = 2 ;
    for ( ; ; ) {
      out.write(String.valueOf(p)) ; out.newLine() ;
      for (int k = p+p ; k <= n ; k += p) t[k] = true ;
      do {
        p++ ; if (p > n)  return ;
      } while (t[p]) ;
    }
  }

  public static void main(String [] arg) {
     ⋮
     try {
      BufferedWriter out =
        new BufferedWriter (new FileWriter ("primes.txt")) ;
      sieve(n, out) ;
      out.close() ;
    } catch (IOException e) {
      System.err.println("Malaise: " + e.getMessage()) ;
      System.exit(2) ;
    }
  }

Noter surtout que l’affichage d’une ligne qui contient un entier est pénible : conversion explicite (par String.valueOf) et retour à la ligne explicite (par out.newLine()).

B.5.5.2  Entrées formatées

Les Reader permettent de lire caractère par caractère, les BufferedReader permettent de lire ligne par ligne. C’est déjà pas mal, mais c’est parfois insuffisant. Par exemple, on peut imaginer vouloir relire le fichier primes.txt de la section précédente. Ce que l’on veut alors c’est lire une séquence d’entiers int. Pour ce faire nous pourrions reconstituer nous même les int à partir de la séquence de leurs chiffres, mais ça ne serait pas très moderne.

Heureusement, il existe des objets qui savent faire ce style de lecture pour nous : ceux de la classe Scanner du package java.util. Les objets Scanner sont des flux de lexèmes (tokens). Les lexèmes sont simplement à un langage informatique ce que les mots sont à un langage dit naturel. Par défaut les lexèmes reconnus par un Scanner sont les mots de Java — un entier, un identificateur, une chaîne, etc.

Un Scanner possède une méthode next() qui renvoie le lexème suivant du flux d’entrée (comme un String) et une méthode hasNext() pour savoir si il existe un lexème suivant, ou si le flux est terminé. Il possède aussi des méthodes spécialisées pour lire des lexèmes particuliers ou savoir si le lexème suivant existe et est un lexème particulier (par exemple nextInt() et hasNextInt() pour un int). On peut donc utiliser primes.txt pour décomposer un int en facteurs premiers ainsi (même si ce n’est pas une très bonne idée algorithmiquement parlant).

import java.util.* ;
import java.io.* ;

class Factor {
  private static PrintWriter out = new PrintWriter (System.out) ;

  private static void factor (int n, String filename) {
    try {
      Scanner scan = new Scanner (new FileReader (filename)) ;
      out.print(n + ":") ;
      while (n > 1) {
        if (!scan.hasNextInt())
          throw new IOException ("Format of file " + filename) ;
        int p = scan.nextInt() ;
        while (n % p == 0) {
          out.print(" " + p) ;
          n /= p ; // Pour n = n / p ;
        }
      }
      out.println() ; out.flush() ; scan.close() ;
    } catch (IOException e) {
      System.err.println("Malaise : " + e.getMessage()) ;
      System.exit(2) ;
    }

  }

  public static void main(String arg []) {
    for (int k = 0 ; k < arg.length ; k++)
      factor(Integer.parseInt(arg[k]), "primes.txt") ;
  }
}

On note que le Scanner est construit à partir d’un Reader. Il y a bien entendu d’autres constructeurs. On note aussi que l’entrée scan est fermée explicitement par scan.close(), dès qu’elle n’est plus utile ; tandis que la sortie n’est que vidée par out.flush(). En effet, il ne serait pas adéquat de fermer la sortie out qui sert plusieurs fois, mais il faut s’assurer que les sorties du programme sont bien envoyées au système d’exploitation. Le contrôle fin du comportement des Scanner se fait à l’aide des expressions régulières de Java (voir 6.3.3 et la classe de bibliothèque Pattern)


Previous Up Next