E/S objets

images/brownbelt.png

ObjectInputStream, ObjectOutputStream

Les classes DataInputStream, DataOutputStream pemettent de lire/écrire des données java dans un flot: données scalaires (readBoolean, readInt, readDouble, etc.) et des chaînes de caractères UNICODE codées en UTF-8 (readUTF).

Les classes ObjectInputStream et ObjectOutputStream disposent des mêmes fonctionnalités mais permettent en plus de lire ou écrire des instances d’objet :

Ecriture dans un flot objet. 

// voir également utilitaires java.nio.file.Files
try (
   ObjectOutputStream oos =
      new ObjectOutputStream(
         new BufferedOutputStream(
            new FileOutputStream(nomFichier)))) {
   oos.writeObject(new BigDecimal("6.55957")) ;
   oos.flush() ;
   // °°°°
} catch(IOException exc) { /*Au rapport! */}

Lecture depuis un flot objet. 

// voir également utilitaires java.nio.file.Files
try (
   ObjectInputStream ois =
      new ObjectInputStream (
         new BufferedInputStream(
            new FileInputStream(nomFichier))) ) {
   BigDecimal taux = (BigDecimal) ois.readObject() ;
   // °°°°°
}catch(ClassNotFoundException xc) {
   /* Rapport!  */
}catch(IOException exc) {
   /* Rapport! */
}

On notera que comme readObject() est déclarée comme rendant un Object on doit utiliser une opération de transtypage ("cast") pour pouvoir écrire une affectation correcte. D’autre part il n’est pas sûr que la J.V.M. dispose du type effectif de l’objet transmis par le flot d’où la nécessité de capturer l’exception correspondante -ClassNotFound- (par ailleurs la lecture d’un objet dans le flot alors que c’est un primitif qui s’y trouve provoquera un erreur d’entrée/sortie).

Les objets dans un flot

Tous les objets ne sont pas susceptibles d'être envoyés dans un flot (fichier, ligne de communication, etc.).

Quelles sont les données qui sont concernées par le processus de linéarisation de l’instance ("serialization")?

Ici l’objectif est de transmettre l'état de l’instance pour pouvoir la reconstituer, outre le fait que l'émetteur et le récepteur de l’objet doivent avoir un minimum d’accord sur la classe correspondante, il faut comprendre que:

  • Certains objets sont, par nature, non susceptibles d'être transmis parce que leur état réel n’est pas reproductible, et/ou parce que ces instances dépendent d'éléments de contexte qui ne sont pas transmissibles entre J.V.M.s. Citons, par exemple, les Threads ou les dispositifs d’entrée-sortie eux-mêmes.

    Les dispositifs d’entrée/sortie d’objets ont donc besoin d’une information leur indiquant explicitement que la linéarisation est licite. Les objets concernés doivent donc implanter une des interfaces "marqueurs" Serializable (protocole contrôlé par le dispositif d’E/S) ou Externalizable (protocole spécifique défini et contrôlé par le programmeur).

    Une tentative d'écriture d’un objet non-Serializable provoque une NotSerializableException.

  • Il peut être souhaitable de soustraire certaines variables membres de l’instance au processus de linéarisation (par exemple parce que ces variables ne sont pas Serializable; ou parceque l’on souhaite écrire directement un protocole particulier pour ce champ).

    Un modificateur particulier: transient permet d’indiquer que le champ n’est pas transmissible

  • Les variables partagées (membres static) ne sont pas transférées.

    Les autre modificateurs comme private, protected, etc. n’ont aucun effet sur le mécanisme de linéarisation.

Un objet Serializable

import java.io.* ;

public class Salarie implements Serializable {
   public final String id ;
   private String nom ;
   private Adresse adresse ;
   private transient Salarie manager ;
   // °°°°

   public Salarie(String nom, String id) {
      this.id = id ;
      this.nom = nom ;
      // °°°°
   }

   public void associeManager(String managerId) {
      // servira plus tard à reconstituer le champ "manager"
   }
   // °°°
}

Effets de la linéarisation

Lorsqu’on linéarise des objets avec des ObjectStreams on doit bien maîtriser les effets suivants :

  • Les instances référencées par l’instance en cours de linéarisation sont à leur tour linéarisées (par exemple le champ adresse dans Salarie). Attention : les graphes de références sont conservés (y compris s’il y a des cycles!).
  • Un mécanisme particulier (qui assure la propriété ci-dessus) fait que le flot mémorise les instances. Pour que le Stream "oublie" les instances déjà transférées utiliser la méthode reset().
  • Si on veut retransférer des objets dont l'état change (par ex. des data objects) utiliser plutôt la méthode writeUnshared(Object obj).
  • La création d’un ObjectInputStream est un appel bloquant. On ne sortira de cet appel que lorsque le flot aura reçu des informations qui lui permettent de s’assurer que le protocole avec l'ObjectOutputStream correspondant est correct. Ces informations se trouvent dans les données initiales du flot (écrites par l’ouverture de ObjectOutputStream correspondant).
  • L’utilisation la plus simple du mécanisme de linéarisation suppose que les JVM qui écrivent et lisent les instances aient une connaissance a priori de la définition des classes. Il peut se produire des cas où les versions de définition de la classe ne sont pas tout à fait les mêmes entre les JVM : la spécification des E/S objet qui définit précisément les cas où ces versions sont considérés comme compatibles. Voir l’utilitaire serialver pour connaître l’identifiant de serialisation d’une classe (un static final long serialVersionUID).

Modifications des opérations liées aux objets Serializable

images/blackbelt.png

On peut personnaliser de diverses manières la façon dont la linéarisation opère en traitant une instance. Le mode opératoire principal consiste à déclarer dans la classe deux méthodes symétriques de responsabilité private.

Personnalisation du mécanisme de linéarisation. 

public class Salarie implements Serializable {
   public final String id ;
   private String nom ;
   private Adresse adresse ;
   private transient Salarie manager ;
   .... ;
   private void writeObject(ObjectOutputStream oos)
               throws IOException {
      oos.defaultwriteObject() ;
      if( null != manager ) {
         oos.writeUTF(manager.id) ;
      } else {oos.writeUTF("") ;}
   }

   private void readObject(ObjectInputStream ois)
               throws IOException {
      ois.defaultReadObject() ;
      String idManager = ois.readUTF() ;
      associeManager(idManager) ;
   }

Par convention ces deux méthodes writeObject, readObject sont prises en compte par le mécanisme de linéarisation.

Ici les méthodes de ObjectStream defaultRead/Write permettent d’opérer la lecture/écriture "normale" de l’instance, mais ensuite on complète le protocole avec d’autres données.

--exercice--