5 Cours ENPC, printemps 2001
Java, objets, interfaces et Internet

Le modèle de flots

Les entrées et les sorties sont organisées en Java autour du concept de flot (anglais stream), à l'aide d'un ensemble très développé de types. Un flot est une suite de valeurs (octets, caractères, objets quelconques) successivement lues ou écrites. Ces flots sont ainsi classifiés en flots d'entrée (qui comportent des méthodes de lecture), et en flots de sortie (qui comportent des méthodes d'écriture). Outre le programme, un flot d'entrée est connecté à une source, et un flot de sortie à une cible. La source ou la cible d'un flot peut être un fichier, un tampon en mémoire, une chaîne de caractères, un autre flot, une ressource Web ou bien un port Internet.

Les flots les plus basiques sont des flots d'octets. Les classes des flots d'octets en écriture ont un nom en ...OutputStream ; celles des flots d'octets en lecture ont un nom en ...InputStream. Pour travailler avec des flots de caractères Unicode, on utilisera des classes en ...Writer et ...Reader.

Lire et écrire des flots d'octets

Les méthodes générales de lecture sur un flot d'octets sont :
  • int read(), qui lit l'octet suivant disponible sur le flot (et se bloque en l'attendant), et le retourne dans un int entre 0 et 255, ou bien retourne -1 si la fin du flot est atteinte.
  • int read(byte b[]), qui lit au plus b.length octets du flot, les place dans le tableau b, et retourne le nombre d'octets lus ou bien -1 si la fin du flot est atteinte
Les méthodes générales d'écriture sur un flot d'octets sont :
  • void write(int c), qui écrit un octet, représenté par un int
  • void write(byte[] b), qui écrit les b.length octets de b
  • void flush(), qui vide le flot sur sa cible (mémoire, fichier, etc)

Connecter un flot à un fichier

La lecture et l'écriture d'octets sur un fichier se fait à l'aide des classes FileInputStream et FileOutputStream. L'exemple suivant montre comment copier un fichier dans un autre (dont les noms sont donnés sur la ligne de commande), octet par octet ; si les deux noms de fichiers ne sont pas données sur la ligne de commande, on utilise les flots standards System.in , et System.out (de type PrintStream), ce qui est possible car FileOutputStream et PrintStream sont des sous-classes de OutputStream.

import java.io.*;

class CopierOctets {
  public static void main(String[] args)
    throws IOException {
    InputStream in = System.in;
    OutputStream out = System.out;
    int c;

    if (args.length > 0) in = new FileInputStream(args[0]);
    if (args.length > 1) out = new FileOutputStream(args[1]);
    while ((c = in.read()) != -1) out.write(c);
    in.close();
    out.close();
  }
}

En l'absence de récupération de l'exception IOException, la méthode main doit déclarer qu'elle est susceptible de déclencher (c'est-à-dire de propager) cette exception.

Pour concaténer des octets à la fin d'un fichier (au lieu d'écrire en écrasant éventuellement son contenu), on utilise un autre constructeur de FileOutputStream, avec l'argument supplémentaire true :
    if (args.length > 1) out = new FileOutputStream(args[1], true);

Obtenir des propriétés d'un fichier

La classe java.io.File a pour objet des chemins d'accès à des fichiers ou à des répertoires (et non les fichiers eux-mêmes). Cette classe est utile pour obtenir diverses propriétés des fichiers (savoir si un chemin désigne un fichier ordinaire ou un répertoire, est accessible en lecture ou en écriture, etc) :

 File cheminRepertoire =
   new File("/usr/local/www/doc/java/jdk1.1.5/docs");
 File cheminFichier =
   new File(cheminRepertoire, "index.html");
 ...
   if (cheminRepertoire.isDirectory() &&
       cheminFichier.canRead()) {
     ...
   }

Si un accès direct à une position quelconque du fichier est nécessaire, on devra utiliser la classe RandomAccessFile, qui fonctionne à la fois en écriture et en lecture, permettant de sauvegarder, puis de restituer, la position d'une opération dans le fichier.

Lire et écrire sur un flot de caractères

Les flots de caractères sont des objets de classe Reader (flots de caractères d'entrée) ou Writer (flots de caractères de sortie). Les méthodes read et write de ces classes sont analogues à celles opérant sur des flots d'octets, à la différence que c'est un caractère 16 bits qui est lu ou écrit, et non un octet.

La conversion entre un flot d'octets et un flot de caractères se fait à l'aide des classes OutputStreamWriter et InputStreamReader. Cette conversion se fait en connectant un flot d'octets à un flot de caractères :

InputStreamReader isr = new InputStreamReader(System.in);

Cette conversion permet éventuellement de spécifier le codage utilisé (par exemple, par la chaîne "MacSymbol" s'il s'agit d'un codage MacIntosh) :

 InputStreamReader isr =
   new InputStreamReader(
     new FileInputStream("toto"),
     "MacSymbol"
   );

Pour connecter un flot de caractères à un fichier, si cette conversion n'est pas nécessaire, il est plus simple de recourir aux classe FileWriter et FileReader.

La classe PrintWriter permet d'écrire sur un flot de sortie des données en les représentant à l'aide de chaînes de caractères Unicode (16 bits), à l'aide des méthodes print et println.

On doit attacher un objet PrintWriter à un autre flot pour bénéficier de ces méthodes supplémentaires, lors de sa création :
  PrintWriter pw =
    new PrintWriter(
      new FileWriter(...));
  ...
  pw.println("ici, un texte en caractères Oriya");

Connecter un flot à un tampon

Chaque opération de lecture ou d'écriture peut être très coûteuse sur certains flots ; c'est notamment le cas des accès à un fichier, ou des accès à l'Internet. Pour éviter des opérations individuelles (sur un octet ou sur un caractère à la fois), on préfère souvent travailler sur un tampon (anglais buffer). Par exemple, pour écrire sur un fichier, on écrira sur un flot-tampon, lequel est connecté à un flot d'écriture sur un fichier.

Les classes qui mettent en oeuvre ces tampons sont :

Un flot de caractères de la classe BufferedReader permet des opérations supplémentaires (par exemple, lecture d'une ligne de texte). Il est très courant de connecter un tel tampon à un flot de lecture sur un fichier :

 BufferedReader in =
   new BufferedReader(new FileReader("toto"));
 String s = in.readline();
Symétriquement, pour écrire sur un fichier, il est préférable de travailler avec un tampon :
 PrintWriter out =
   new PrintWriter(
     new BufferedWriter(
       new FileWriter("toto")));
 out.println("un long texte");

Lire et écrire des « données » sur un flot d'octets

Si l'on veut lire et écrire, non pas des octets, mais des valeurs d'un type connu, il faut utiliser les classes DataInputStream et DataOutputStream. Ces classes disposent de méthodes spécialisées pour divers types de valeurs : readInt(), readDouble(), readChar(), readBoolean(), etc, et les méthodes write... correspondantes.

On doit connecter un flot de données à un autre flot pour bénéficier de ces méthodes supplémentaires, lors de sa création :

  DataOutputStream dos =
    new DataOutputStream(
      new FileOutputStream(...));
  ...
  dos.writeBoolean(true);
  dos.writeInt(4);
  dos.close();

On connecte ainsi un flot d'entrée à un autre flot d'entrée, ou un flot de sortie à un autre flot de sortie. Par exemple, pour lire un entier sur l'entrée standard :

  DataInputStream dis =
    new DataInputStream(System.in);
  int n = dis.readInt();

Lire et écrire des « données » sur un tableau d'octets

Le modèle des flots peut être utilisé afin de lire ou d'écrire des données sur un tableau d'octets, considéré comme un flot. Supposons que l'on dispose d'un tableau d'octets data (reçu par exemple par une communication UDP sur l'Internet). On sait que ce tableau contient un booléen et un entier. Pour le lire, on recourt aux flots suivants, de classes DataInputStream et ByteArrayInputStream :

byte[] data = ...;

DataInputStream dis =
  new DataInputStream(
    new ByteArrayInputStream(data));

boolean b = dis.readBoolean();
int n = dis.readInt();
dis.close();
Ceci suppose que ce tableau contient des données codées de façon compatible avec le décodage réalisé par les méthodes readBoolean(), etc. Ce sera le cas, symétriquement, si le tableau d'octets a été obtenu par les classes DataOutputStream et ByteArrayOutputStream :

byte[] data;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.writeBoolean(true);
dos.writeInt(4);
data = baos.toByteArray();
dos.close();

Persistance et sérialisation

Les classes ObjectOutputStream et ObjectInputStream permettent de rendre persistants les objets de Java en les sauvegardant sur un flot (qui peut être écrit sur un fichier), puis en les relisant. Seuls les objets dont la classe implémente l'interface Serialisable peuvent bénéficier de ce mécanisme, appelé sérialisation. Si un objet comporte des membres qui sont des références à d'autres objets, ces objets sont aussi sauvegardés.

 ObjectOutputStream s =
   new ObjectOutputStream(new FileOutputStream("toto"));
 s.writeObject("Aujourd'hui");
 s.writeObject(new Date());
 s.flush();

La lecture de ces objets, qui se fait dans le même ordre que leur écriture, doit opérer une coercition d'Object vers la classe que l'on veut restituer :

 ObjectInputStream s = 
   new ObjectInputStream(new FileInputStream("toto"));
 String chaine = (String)s.readObject();
 Date date = (Date)s.readObject();

Connecter un flot à une ressource Web

Le programme suivant connecte un flot d'entrée à une ressource Web spécifiée par son URL, transforme ce flot d'octets en un flot de caractères et le place dans un tampon ; les lignes successivement lues sur ce flot d'entrée sont copiées sur la sortie standard du programme (jusqu'à ce que readLine retourne null).

import java.net.*;
import java.io.*;

class URLReader {
  public static void main(String[] args) 
    throws MalformedURLException, IOException {
    URL url = new URL("http://cermics.enpc.fr/");
    BufferedReader in = 
      new BufferedReader(
        new InputStreamReader(
	  url.openStream()));
    String ligne;
    
    while ((ligne = in.readLine()) != null)
      System.out.println(ligne);
    
    in.close();
  }
}

Connecter un flot à une socket TCP

Le programme suivant connecte un flot d'entrée à un port Internet spécifié par un nom de machine et un numéro de port ; ce numéro, 13, est celui d'un serveur dont la réponse est une ligne contenant la date et l'heure courante ; la connexion à ce port utilise le protocole TCP. Ce flot d'entrée est ensuite transformé en un flot de caractères, puis placé dans un tampon ; la ligne lue sur ce flot d'entrée est simplement copiée sur la sortie standard du programme.

import java.io.*;
import java.net.*;

class DateReader {
  public static void main(String[] args) 
    throws UnknownHostException, IOException {
    
    String nomHote = args.length>0 ? args[0] : "localhost";
    Socket s = new Socket(nomHote, 13);
    BufferedReader reponse = 
	new BufferedReader(
	  new InputStreamReader(
	    s.getInputStream()));
    String date = reponse.readLine();
    System.out.println(nomHote + " : " + date);
    reponse.close();
    s.close();
  }
}

Flots d'instructions : les threads

Un flot d'instructions est contrôlé en Java par un thread. Quand une application est démarrée, l'environnement Java crée un thread exécutant la fonction main de l'application. D'autres threads peuvent être créés, soit par l'environnement d'exécution, soit par le programme. Chacun de ces threads exécute une fonction, en concurrence avec les autres ; ils communiquent entre eux via des objets partagés.

Contrôle d'un flot d'instructions

Le flot d'instructions est celui défini par une méthode run d'une classe implémentant Runnable. On crée un objet de type Thread associé à un objet de cette classe et on démarre ce thread par start().

Class A implements Runnable {

  Thread t;

  A() {
    t = new Thread(this);
    t.start();
  }

  public void run() { ... }
}
    

États d'un thread

Un thread, une fois initialisé par un constructeur de la classe Thread est dans l'état initial.

La méthode start(), qui retourne immédiatement, le fait passer dans l'état actif, dans lequel il peut être effectivement exécuté (sur un monoprocesseur, il sera exécuté en temps partagé avec les autres unités). Il faut noter que la méthode run n'est pas appelée explicitement dans le programme (exactement de la même façon que la méthode main d'une application).

Le thread passe dans l'état suspendu quand :

  • la méthode sleep(long m) lui est appliquée ; il reste suspendu au moins m millisecondes, puis redevient actif
  • il exécute wait() : il est suspendu jusqu'à ce qu'un thread exécute notify() (ces deux méthodes ne peuvent être appelées qu'à partir d'une méthode synchronisée)
  • il est bloqué sur une opération d'entrée/sortie.

Le thread passe dans l'état terminé (Dead) quand run() termine.

Connecter un flot de sortie à un flot d'entrée

Il s'agit d'une technique importante pour connecter entre eux deux programmes, en connectant le flot de sortie du premier programme avec le flot d'entrée du second programme. Cette technique, issue d'une pratique courante dans le système Unix, est appliquée ici, non à deux programmes, mais à deux threads.

Les classes permettant ces connexions sortie/entrée sont :

Un tube d'entrée doit être connecté à un tube de sortie. Considérons une classe Producteur, dont un constructeur prend en argument un flot de sortie out, et une classe Consommateur, dont un constructeur prend en argument un flot d'entrée in. Comme on doit connecter in et out, ces flots doivent être des tubes, par exemple de caractères :

PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader(out);

On peut alors connecter des objets p et c à l'aide de ces flots.

Producteur p = new Producteur(out);
Consommateur c = new Consommateur(in);
L'écriture dans out et la lecture dans in sera réalisée par les objets eux-mêmes, dont on suppose qu'ils ont chacun leur propre thread (l'un des deux pouvant être le thread du programme principal) :

p.start();
c.start();

Dans l'exemple suivant, le producteur écrit le caractère 'a' sur son flot de sortie toutes les 1000 millisecondes ; le consommateur est continuellement en attente d'un caractère sur son flot d'entrée.

 class Producteur extends Thread {
  Writer out;
  
  Producteur(Writer out) {
    this.out = out;
  }

  public void run() {
    while (true) {
      try {
	out.write('a');
	Thread.sleep(1000);
      }
      catch(InterruptedException e) {}
      catch(IOException e) {}
    }
  }
}

class Consommateur extends Thread {
  Reader in;
  
  Consommateur(Reader in) {
    this.in = in;
  }

  public void run() {
    while (true) {
      try {
	char c = (char) in.read();
        System.out.println(c);
      }
      catch(IOException e) {}
    }
  }
}
  


[Cours Java] [Notes 4] [Notes 6]

Mise à jour :