6. TD numéro 6: La Sérialisation en Java

Par sérialiser on entend représenter des données structurées afin de les sauvergarder ou de les transmettre. Jison et XML sont deux formalismes permettant la représentation de données structurées, leurs avantages sont nombreux : une certaine simplicité, leur universalité et le fait que ce soient des standards très répandus.

Java propose un mécanisme de sérialisation natif très simple à utiliser ce qui permet de stocker ou des transmettre des objets. Un des avantages important et que cette sérialisation transmet non seulement les champs de l’objet mais aussi les méthodes de la classe d’appartenance de l’objet. Ainsi on peut assez simplement effecuter la séquence d’actions suivante :

  • Un éméteur sérialise un Objet A.
  • L’éméteur transmet l’objet A vers le recepteur (via un flux d’objets).
  • Le recepteur desérialise l’objet A et récupère une copie \(A^\prime\) de A.
  • Le recepteur applique une des méthodes de \(A^\prime\).

6.1. Limitations et Objets sérialisables

En général on ne sérialise que des types “simples”, en effet sérialiser un fichier ou un flux n’a à priori guère de sens, puisque ces objets sont non seulement dynamiques mais potentiellement infinis. De la même facon sérialiser une référence n’a guère de sens et sérialiser un dictionnaire (.i.e table de hachage) peut être ambigu, enfin sérialiser objet tel que \(sys.out\) est encore plus loufoque et improbable.

Afin de formaliser tout cela Java demande à ce qu’une classe d’objets soit déclarée serialisable afin de pouvoir lui appliquer mes méthodes de sérialisation. La déclaration consiste en une simple déclaration d’interface.

public class LazyCompteur implements Serializable {}
  • Java dispose d’un mécanisme par défaut pour sérialiser un type fini où un type “fini” est soit 1) un type primitif soit 2) une union finie de types finis. Ainsi pour si une classe est de type fini la simple déclaration de l’interface Serializable suffit.

  • Quand une classe contient des champs non finis (.i.e sérialisables par défaut) (p.e. un Fileinputstream) on peut indiquer que ce champ doit être omis lors de la sérialisation. On utilise à cet effet le mot clef transient. Ainsi la déclation ci-dessous rend la classe sérialisable par défaut car l’unique type non fini est déclaré transient.

    import java.io.Serializable;
    
    public class LazyCompteur implements Serializable {
    
    private String Nom;
    private int objectId;
    
    transient private  FileInputStream  myfile=null ;
    

6.2. Flux d’objets

  • On écrit les objets (sérialisables) sur des flux sortant d’objets, qui sont des ObjectOutputStream . On crée ces flux en encapsulant (décorant) des flux sortants, le plus souvent des flux fichier ou des flux associés à une socket. Ces flux sont binaires. On écrit ensuite un objet avec la méthode writeObject. Dans le code ci dessous:

    . le flux fluxObjetsortantA est associé à un fichier ouvert en écriture

    . le flux fluxObjetsortantB est associé au flux de sortie d’une socket (.i.e l’output).

    . l’objet charlus est ecrit dans les deux flux.

    dataFile = new FileOutputStream(dataFilename);
    ObjectOutputStream fluxObjetsortantA = new ObjectOutputStream(dataFile);
    
    la_connection = new Socket(hote, port);
    fluxObjetSortantB=  new ObjectOutputStream (la_connection.getOutputStream());
    
    LazyCompteur charlus
    fluxObjetSortantA.writeObject(charlus)
    fluxObjetSortantB.writeObject(charlus)
    
  • On lit les objets (sérialisables) sur des flux entrant d’objets qui sont des ObjectInputStream . On crée ces flux en encapsulant (décorant) des flux entrants, le plus souvent des flux fichier ou des flux associés à une socket. Ces flux sont binaires. On lit ensuite un objet avec la méthode readObjet . Dans le code ci dessous:

    . le flux fluxObjetsentrantA est associé à un fichier ouvert en écriture

    . le flux fluxObjetentrantB est associé au flux de sortie d’une socket (.i.e l’output).

    . l’objet charlus est lu dans les deux flux.

    indataFile = new FileInputStream(dataFilename,"r");
    ObjectInputStream fluxObjetsEntrantA = new ObjectInputStream(indataFile);
    
    la_connection = new Socket(hote, port);
    ObjectInputStream fluxObjetsEntrantB=  new ObjectInputStream (la_connection.getInputStream());
    
    LazyCompteur charlus
    LazyCompteur charlie
    
    charlus = fluxObjetsEntrantA.readObject()
    charlie = fluxObjetsSortantB.readObject()
    
  • Les méthodes peuvent générer les exceptions d’entrée sortie usuelles. Dans le cas de la lecture une est imporante car la fin de flux (fin de fichier, socket fermée) est detectée via l exception EOFException.

  • Il faut bien entendu fermer les flux (\(flux.close\)) et aussi vider le tampon du flux de sortie (\(fluxSortant.close\)).

6.3. Exercice 1

  • Écrire deux classes élémentaires de votre choix (je les appellerai ClassA et ClassB) munies :

    . de champs permettant de distinguer les objets (nom etc ...)

    . d’une méthode basique lancer() (typiquement lancer peut compter de 1 à N sur la sortie standard).

    . de la méthode spéciale toString()

  • Generer 10 objets différent s de chaque classe.

  • Écrire tout les objets dans un fichier.

  • Examiner le fichier, conclure.

6.4. Exercice 2

  • Ouvrir le fichier et lire les objets qui y sont écrit.
  • Afficher ces objets sur la sortie standard.
  • Pour chaque objet lu exécuter la méthode lancer() sur la sortie standard.

6.5. Exercice 3

  • Reprendre l’exercice 1 en ouvrant cette fois le fichier en mode “Ajout” afin d’ajouter les objets à ceux existants.

  • Vérifier que cela fonctionne en reprennant l’exercice 2.

  • Modifier le code de l’exercice 2 afin de traiter les objets lu de facon différentiée. On utilisera le typage dynamique :

    Object monObjet = fluxObjetsEntrant.readObject()
    if (monObjet  instanceof ClassA) {}
    

6.6. Exercice 4

On veut reprendre les exercices 1 à 3 en utilisant cette fois une socket. Ceci permet d’envoyer les objets à un ordinnateur (une machine Java) distante.

  • Créer un serveur muni d’une méthode Service_Client qui sera appliquée à tout client entrant (i.e la méthode sera appliquée à la Socket retournée par la méthode accept d’un ServerSocket.
  • Définir la méthode Service_Client afin qu’elle :
    1. lise les objets sur le flux d’objets entrant que vous crérer en encapsulant le flux entrant (binaire) de la socket.
    2. Affiche les objets lus.
    3. Se termine si la socket est close ou si un objet spécial est lu.
  • Créer de facon symétrique un client qui
    1. qui ouvre une socket (vers le serveur)
    2. crée un ObjectOutputStream en encapsulant le flux de sortie de la socket.
    3. crée un certain nombre d’objets divers et les écrit sur l’ObjectOutputStream.
  • Ajouter coté servuer une detection du type des objets recus et un peu traitement différencié (selon la classe de l’objet recu).

6.7. Exercice 5

  1. Reprendre l’exercice 4 mais rendre le serveur multi-threadé.
  2. Ajouter des envois d’objets depuis le serveur (plus précisemment depuis le thread gérant le client) vers le client.

6.8. Aspects plus avancés

6.8.1. Classe manquante, ou mauvaise version

  1. Quand un objet apparenant à une certaine classe est écrit, la définition de la classe n est pas incluse dans la représentation binaire de l’objet. Il faut do nc que la machine Java (JVM) lisant le flux d’objet connaisse la classe. C’est en général le cas, mais si la machine est distante (dans le temp ou l’espace) il se peut que la définition de la classe soit manquante. Dans ce cas une Exception particulière ClassNotFound est levée.

  2. Il se peut aussi que la définiton de la classe soit différente de celle utilisée lors de l’écriture de l’objet, par exemple la version de la classe peut être plus récente ou plus ancienne. Il est donc possible d’inclure un identifiant de version de la classe. Cette inclusion est optionnelle, mais les IDE vous indiquent u ne alterte si aucune version n’est définie et vous proposent d’inclure un identifiant qui sera généré par l’ide. Si vous acceptez que l’IDE génère une version le code ressemblera à ceci :

    private static final long serialVersionUID = -7491172301352370428L;
    
  3. Si le numéro de version de la classe dans la machine virtuelle lisant l’objet diffère de celui inclus dans la réprésentation binaire de cet objet une exception sera générée de type InvalidClassException .