IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java

  Chapitre 11 - Le système d’E/S de Java

pages : 1 2 3 4 5 6 7 8 9 10 

De toute façon, tout ce qui est défini dans une interface est automatiquement public donc writeObject() et readObject() doivent être private, à ce moment là ils feront partie d'une interface. Puisque vous devez suivre exactement les signatures, l'effet est le même que si vous implémentiez une interface.

Il apparaîtra que lorsque vous appelez ObjectOutputStream.writeObject(), l'objet Serializable que vous lui transmettez est interrogé (utilisant la réflexion, pas de doute) pourvoir si il implémente son propre writeObject(). Si c'est le cas, le processus normale de sérialisation est omis et est le writeObject()appelé. Le même type de situation existe pour readObject().

Il y a une autre entorse. À l'intérieur de votre writeObject( ), vous pouvez choisir d'exécuter l'action writeObject() par défaut en appelant defaultWriteObject(). Également, dans readObject()vous pouvez appeler defaultReadObject(). Voici un exemple simple qui démontre comment vous pouvez contrôler le stockage et la récupération d'un objet Serializable :

//: c11:SerialCtl.java
// Contrôler la sérialisation en ajoutant vos propres
// méthodes writeObject() et readObject().
import java.io.*;

public class SerialCtl implements Serializable {
  String a;
  transient String b;
  public SerialCtl(String aa, String bb) {
    a = "Not Transient: " + aa;
    b = "Transient: " + bb;
  }
  public String toString() {
    return a + "\n" + b;
  }
  private void
    writeObject(ObjectOutputStream stream)
      throws IOException {
    stream.defaultWriteObject();
    stream.writeObject(b);
  }
  private void
    readObject(ObjectInputStream stream)
      throws IOException, ClassNotFoundException {
    stream.defaultReadObject();
    b = (String)stream.readObject();
  }
  public static void main(String[] args)
  throws IOException, ClassNotFoundException {
    SerialCtl sc =
      new SerialCtl("Test1", "Test2");
    System.out.println("Before:\n" + sc);
    ByteArrayOutputStream buf =
      new ByteArrayOutputStream();
    ObjectOutputStream o =      new ObjectOutputStream(buf);
    o.writeObject(sc);
    // Maintenant faites les revenir :
    ObjectInputStream in =      new ObjectInputStream(
        new ByteArrayInputStream(
          buf.toByteArray()));
    SerialCtl sc2 = (SerialCtl)in.readObject();
    System.out.println("After:\n" + sc2);
  }
} ///:~

Dans cet exemple, un champ String est normal et le second est transient, pour prouver que le champ non-transient est sauvé par la méthode defaultWriteObject() et le que le champ transient est sauvé et récupéré explicitement. Les champs sont initialisés dans le constructeur plutôt qu'au point de définition pour prouver qu'ils n'ont pas été initialisés par un certain mécanisme automatique durant la sérialisation.

Si vous employez le mécanisme par défaut pour écrire les parties non-transient de votre objet, vous devrez appeler defaultWriteObject() comme la première action dans writeObject() et defaultReadObject() comme la première action dans readObject(). Ce sont d'étranges appels à des méthodes. Il apparaît, par exemple, que vous appelez defaultWriteObject() pour un ObjectOutputStream et ne lui passez aucun argument, mais il tourne d'une manière ou d'une autre autour et connaît la référence à votre objet et comment écrire toutes les parties non-transient. Étrange.

Le stockage et la récupération des objets transient utilisent un code plus familier. Et cependant, pensez à ce qu'il se passe ici. Dans main(), un objet SerialCtl est créé, puis est sérialisé en un ObjectOutputStream. (Notez dans ce cas qu'un tampon est utilisé à la place d'un fichier — c'est exactement pareil pour tout l' ObjectOutputStream.) La sérialisation survient à la ligne :

o.writeObject(sc);

La méthode writeObject() doit examiner sc pour voir si il possède sa propre méthode writeObject(). (Non pas en contrôlant l'interface — il n'y en a pas — ou le type de classe, mais en recherchant en fait la méthode en utilisant la réflexion.) Si c'est le cas, elle l'utilise. Une approche similaire garde true pour readObject(). Peut-être que c'est la seule réelle manière dont ils peuvent résoudre le problème, mais c'est assurément étrange.

Versioning

Il est possible que vous désiriez changer la version d'une classe sérialisable (les objets de la classe original peuvent être stockés dans un base de donnée, par exemple). Ceci est supporté mais vous devrez probablement le faire seulement dans les cas spéciaux, et cela requiert un profondeur supplémentaire de compréhension qui ne sera pas tenté d'atteindre ici. Les documents HTML du JDK téléchargeables depuis java.sun.com couvrent ce sujet de manière très approfondie.

Vous pourrez aussi noter dans la documentation HTML du JDK que de nombreux commentaires commencent par :

Attention : Les objets sérialisés de cette classe ne seront pas compatibles avec les futures versions de Swing. Le support actuel de la sérialisation est approprié pour le stockage à court terme ou le RMI entre les applications. ...

Ceci parce que le mécanisme de versionning est trop simple pour fonctionner de manière fiable dans toutes les situations, surtout avec les JavaBeans. Ils travaillent sur un correction de la conception, et c'est le propos de l'avertissement.

Utiliser la persistence

Il est plutôt attrayant de faire appel à la technologie de la sérialisation pour stocker certains états de votre programme afin que vous puissiez facilement récupérer le programme dans l'état actuel plus tard. Mais avant de de pouvoir faire cela, il faut répondre à certaines questions. Qu'arrive-t-il si vous sérialisez deux objets qui ont tous les deux une référence à un troisième objet ? Quand vous récupérez ces deux objets depuis leur état sérialisé, aurez vous une seule occurrence du troisième objet ? Que ce passe-t-il si vous sérialisez vos deux objets pour séparer les fichiers et les désérialisez dans différentes parties de votre code ?

Voici un exemple qui montre le problème :

//: c11:MyWorld.java
import java.io.*;
import java.util.*;

class House implements Serializable {}

class Animal implements Serializable {
  String name;
  House preferredHouse;
  Animal(String nm, House h) {
    name = nm;
    preferredHouse = h;
  }
  public String toString() {
    return name + "[" + super.toString() +
      "], " + preferredHouse + "\n";
  }
}

public class MyWorld {
  public static void main(String[] args)
  throws IOException, ClassNotFoundException {
    House house = new House();
    ArrayList  animals = new ArrayList();
    animals.add(
      new Animal("Bosco the dog", house));
    animals.add(
      new Animal("Ralph the hamster", house));
    animals.add(
      new Animal("Fronk the cat", house));
    System.out.println("animals: " + animals);

    ByteArrayOutputStream buf1 =
      new ByteArrayOutputStream();
    ObjectOutputStream o1 =      new ObjectOutputStream(buf1);
    o1.writeObject(animals);
    o1.writeObject(animals); // Écrit un 2éme jeu
    // Écrit vers un flux différent :
    ByteArrayOutputStream buf2 =
      new ByteArrayOutputStream();
    ObjectOutputStream o2 =      new ObjectOutputStream(buf2);
    o2.writeObject(animals);
    // Now get them back:
    ObjectInputStream in1 =      new ObjectInputStream(
        new ByteArrayInputStream(
          buf1.toByteArray()));
    ObjectInputStream in2 =      new ObjectInputStream(
        new ByteArrayInputStream(
          buf2.toByteArray()));
    ArrayList animals1 =
      (ArrayList)in1.readObject();
    ArrayList animals2 =
      (ArrayList)in1.readObject();
    ArrayList animals3 =
      (ArrayList)in2.readObject();
    System.out.println("animals1: " + animals1);
    System.out.println("animals2: " + animals2);
    System.out.println("animals3: " + animals3);
  }
} ///:~

Une chose intéressante ici est qu'il est possible d'utiliser la sérialisation d'objet depuis et vers un tableau de bytes comme une manière de faire une « copie en profondeur » de n'importe quel objet qui estSerializable. (une copie en profondeur veux dire que l'on copie la structure complète des objets, plutôt que seeulement l'objet de base et ses références.) La copie est abordée en profondeur dans l'Annexe A.

Les objets Animal contiennent des champs de type House. Dans main(), une ArrayList de ces Animals est crée et est sérialisée deux fois vers un flux et ensuite vers un flux distinct. Quand ceci est désérialisé et affiché, on obtient les résultats suivant pour une exécution (les objets seront dans des emplacements mémoire différents à chaque exécution) :

animals: [Bosco the dog[Animal@1cc76c], House@1cc769
, Ralph the hamster[Animal@1cc76d], House@1cc769
, Fronk the cat[Animal@1cc76e], House@1cc769
]
animals1: [Bosco the dog[Animal@1cca0c], House@1cca16
, Ralph the hamster[Animal@1cca17], House@1cca16
, Fronk the cat[Animal@1cca1b], House@1cca16
]
animals2: [Bosco the dog[Animal@1cca0c], House@1cca16
, Ralph the hamster[Animal@1cca17], House@1cca16
, Fronk the cat[Animal@1cca1b], House@1cca16
]
animals3: [Bosco the dog[Animal@1cca52], House@1cca5c
, Ralph the hamster[Animal@1cca5d], House@1cca5c
, Fronk the cat[Animal@1cca61], House@1cca5c
]

Bien sur vous vous attendez à ce que les objets déserialisés aient des adresses différentes des originaux. Mais notez que dans animals1 et animals2 les mêmes adresses apparaissent, incluant les références à l'objet House que tous les deux partagent. D'un autre coté, quand animals3 est récupéré le système n'a pas de moyen de savoir que les objets de l'autre flux sont des alias des objets du premier flux, donc il crée un réseau d'objets complétement différent.

Aussi longtemps que vos sérialisez tout dans un flux unique, vous pourrez récupérer le même réseau d'objets que vous avez écrits, sans aucune duplication accidentelle d'objets. Bien sûr, vous pouvez modifier l'état de vos objets entre la période d'écriture du premier et du dernier, mais c'est de votre responsabilité — les objets seront écrit dans l'état où ils sont quel qu'il soit (et avec les connexions quelles qu'elles soient qu'ils ont avec les autres objets) au moment ou vous les sérialiserez.

La chose la plus sûre à faire si vous désirez sauver l'état d'un système est de sérialiser comme une opération « atomique. » Si vous sérialisez quelque chose, faites une autre action, et en sérialisez une autre en plus, etc., alors vous ne stockerez pas le système sûrement. Au lieu de cela, mettez tous les objets qui comprennent l'état de votre système dans un simple conteneur et écrivez simplement ce conteneur à l'extérieur en une seule opération. Ensuite vous pourrez aussi bien le récupérer avec un simple appel à une méthode.

L'exemple suivant est un système imaginaire de conception assistée par ordinateur (CAD) qui démontre cette approche. En plus, il projette dans le sujet des champs static — si vous regardez la documentation vous verrez que Class est Serializable, donc il sera facile de stocker les champs static en sérialisant simplement l'objet Class. Cela semble comme une approche sensible, en tous cas.

//: c11:CADState.java
// Sauve et récupère l'état de la
// simulation d'un système de CAD.
import java.io.*;
import java.util.*;

abstract class Shape implements Serializable {
  public static final int
    RED = 1, BLUE = 2, GREEN = 3;
  private int xPos, yPos, dimension;
  private static Random r = new Random();
  private static int counter = 0;
  abstract public void setColor(int newColor);
  abstract public int getColor();
  public Shape(int xVal, int yVal, int dim) {
    xPos = xVal;
    yPos = yVal;
    dimension = dim;
  }
  public String toString() {
    return getClass() +
      " color[" + getColor() +
      "] xPos[" + xPos +
      "] yPos[" + yPos +
      "] dim[" + dimension + "]\n";
  }
  public static Shape randomFactory() {
    int xVal = r.nextInt() % 100;
    int yVal = r.nextInt() % 100;
    int dim = r.nextInt() % 100;
    switch(counter++ % 3) {
      default:
      case 0: return new Circle(xVal, yVal, dim);
      case 1: return new Square(xVal, yVal, dim);
      case 2: return new Line(xVal, yVal, dim);
    }
  }
}

class Circle extends Shape {
  private static int color = RED;
  public Circle(int xVal, int yVal, int dim) {
    super(xVal, yVal, dim);
  }
  public void setColor(int newColor) {
    color = newColor;
  }
  public int getColor() {
    return color;
  }
}

class Square extends Shape {
  private static int color;
  public Square(int xVal, int yVal, int dim) {
    super(xVal, yVal, dim);
    color = RED;
  }
  public void setColor(int newColor) {
    color = newColor;
  }
  public int getColor() {
    return color;
  }
}

class Line extends Shape {
  private static int color = RED;
  public static void
  serializeStaticState(ObjectOutputStream os)
      throws IOException {
    os.writeInt(color);
  }
  public static void
  deserializeStaticState(ObjectInputStream os)
      throws IOException {
    color = os.readInt();
  }
  public Line(int xVal, int yVal, int dim) {
    super(xVal, yVal, dim);
  }
  public void setColor(int newColor) {
    color = newColor;
  }
  public int getColor() {
    return color;
  }
}

public class CADState {
  public static void main(String[] args)
  throws Exception {
    ArrayList shapeTypes, shapes;
    if(args.length == 0) {
      shapeTypes = new ArrayList();
      shapes = new ArrayList();
      // Ajoute des références aux objets de class :
      shapeTypes.add(Circle.class);
      shapeTypes.add(Square.class);
      shapeTypes.add(Line.class);
      // Fait quelques formes :
      for(int i = 0; i < 10; i++)
        shapes.add(Shape.randomFactory());
      // Établit toutes les couleurs statiques en GREEN:
      for(int i = 0; i < 10; i++)
        ((Shape)shapes.get(i))
          .setColor(Shape.GREEN);
      // Sauve le vecteur d'état :
      ObjectOutputStream out =        new ObjectOutputStream(
          new FileOutputStream("CADState.out"));
      out.writeObject(shapeTypes);
      Line.serializeStaticState(out);
      out.writeObject(shapes);
    } else { // C'est un argument de ligne de commande
      ObjectInputStream in =        new ObjectInputStream(
          new FileInputStream(args[0]));
      // Read in the same order they were written:
      shapeTypes = (ArrayList)in.readObject();
      Line.deserializeStaticState(in);
      shapes = (ArrayList)in.readObject();
    }
    // Affiche les formes :
    System.out.println(shapes);
  }
} ///:~

La classe Shape implemente Serializable, donc tout ce qui est hérité de Shape est aussi automatiquement Serializable. Chaque Shape contient des données, et chaque classe Shape dérivée contient un champ static qui détermine la couleur de tous ces types de Shapes. (Placer un champ static dans la classe de base ne donnera qu'un seul champ, puisque les champs static ne sont pas reproduit dans les classes dérivés.) Les méthodes dans les classes de base peuvent être surpassées [overridden] pour établir les couleurs des types variables (les méthodes static ne sont pas dynamiquement délimitées, donc ce sont des méthodes normales). La méthode randomFactory() crée un Shape différent chaque fois que vous y faites appel, utilisant des valeurs aléatoires pour les données du Shape.

Ce livre a été écrit par Bruce Eckel ( télécharger la version anglaise : Thinking in java )
Ce chapitre a été traduit par Armel Fortun ( groupe de traduction )
télécharger la version francaise (PDF) | Commandez le livre en version anglaise (amazon) | télécharger la version anglaise
pages : 1 2 3 4 5 6 7 8 9 10 
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java