Développons en Java
v 2.40   Copyright (C) 1999-2023 .   
16. NIO 2 18. L'interaction avec le réseau Imprimer Index Index avec sommaire Télécharger le PDF

 

17. La sérialisation

 

chapitre    1 7

 

Niveau : niveau 4 Supérieur 

 

La sérialisation est un procédé introduit dans le JDK version 1.1 qui permet de rendre un objet ou un graphe d'objets de la JVM persistant pour stockage ou échange et vice versa. Cet objet est mis sous une forme sous laquelle il pourra être reconstitué à l'identique. Ainsi il pourra être stocké sur un disque dur ou transmis au travers d'un réseau pour le créer dans une autre JVM. C'est le procédé qui est utilisé, par exemple, par RMI. La sérialisation est aussi utilisée par les beans pour sauvegarder leurs états.

Au travers de ce mécanisme, Java fournit une façon facile, transparente et standard de réaliser cette opération : ceci permet de facilement mettre en place un mécanisme de persistance. Il est de ce fait inutile de créer un format particulier pour sauvegarder et relire un objet. Le format utilisé est indépendant du système d'exploitation. Ainsi, un objet sérialisé sur un système peut être réutilisé par un autre système pour récréer l'objet.

La sérialisation peut s'appliquer facilement à quasiment tous les objets. Cependant, toutes les classes du JDK ne sont pas sérialisables : notamment les classes qui sont liées à des éléments du système ne le sont pas (Thread, OutputStream et ses sous-classes, Socket, Image, ...)

L'implémentation de son propre mécanisme de sérialisation est probablement complexe et coûteux. A contrario, le mécanisme de sérialisation proposé par défaut en Java est relativement simple à mettre en oeuvre. Cette relative simplicité de mise en oeuvre permet d'utiliser la sérialisation pour différents besoins :

La sérialisation n'est cependant pas spécialement conçue pour persister un objet durant une longue période même si cela peut être fait : au contraire elle est plutôt conçue pour être utilisée sur des périodes temporelles courtes (échange réseau, mise en cache, persistance temporaire, ...)

Différentes solutions sont proposées pour sérialiser/désérialiser un objet.

Ce chapitre contient plusieurs sections :

 

17.1. La sérialisation standard

La sérialisation d'un objet permet d'envoyer dans un flux les informations sur la classe et l'état d'un objet pour permettre de le récréer ultérieurement. Ces informations permettent de restaurer l'état de l'objet même dans une classe différente qui dans ce cas doit être compatible. Elle permet donc de transformer l'état d'un objet pour permettre sa persistance en dehors de la JVM ou de l'échanger en utilisant le réseau.

L'opération inverse qui consiste à créer une nouvelle instance à partir du résultat d'une sérialisation s'appelle la désérialisation.

La sérialisation doit faire face à plusieurs problématiques notamment :

Il existe plusieurs formats de sérialisation appartenant à deux grandes familles :

L'ajout d'un attribut à l'objet est automatiquement pris en compte lors de la sérialisation. Attention toutefois, la désérialisation de l'objet doit se faire avec la classe qui a été utilisée pour la sérialisation ou une classe compatible.

Tous les objets ne sont pas sérialisables : généralement ce sont des objets qui ont des références sur des éléments du système d'exploitation (threads, fichiers, ...).

Le mécanisme de sérialisation par défaut ignore les champs static ou transient.

La sérialisation utilise un mécanisme qui ne sérialise qu'une fois un objet même si le graphe d'objets à sérialiser possède plusieurs références sur celui-ci. Ceci permet lors de la désérialisation que toutes les références sur l'objet dans le graphe pointent bien sur la bonne instance.

Pour pouvoir être sérialisée, une classe doit implémenter l'interface java.io.Serializable ou l'interface java.io.Externalizable

 

17.1.1. La sérialisation binaire

La sérialisation binaire est une fonctionnalité introduite à partir de Java 1.1. Le format de la sérialisation est spécifique à la JVM mais il ne dépend pas du système d'exploitation.

La sérialisation de membres de types primitifs est facile puisqu'il suffit de prendre leur valeur. Lorsque le membre est un objet c'est plus compliqué car il n'est pas possible de simplement prendre la référence. Lorsque l'objet sera désérialisé, les références vont changer : sérialiser la référence est inutile dans la mesure où la référence n'a de sens que dans le contexte d'exécution d'une seule instance d'une JVM. Il est donc nécessaire de sérialiser récursivement chaque objet du graphe.

Une énumération est sérialisée uniquement sous la forme de son nom.

Plusieurs éléments de la plate-forme Java agissent durant la mise en oeuvre de la sérialisation :

Le choix de rendre une classe sérialisable est facile à court terme par contre cela peut impliquer des difficultés à long terme notamment en limitant les possibilités de changement dans la classe. La sérialisation de la classe devient publique comme son interface : l'ajout ou la suppression d'un champ peut modifier le comportement du mécanisme de sérialisation par défaut.

Par exemple, par défaut si une classe Serializable ne déclare pas explicitement un champ serialVersionUID alors le compilateur va l'ajouter et calculer une valeur qui prend en compte les différents éléments qui composent la classe.

Exemple :
import java.io.Serializable;

public class MonBean implements Serializable {

  private String champ1;

  public String getChamp1() {
    return this.champ1;
  }

  public void setChamp1(final String champ1) {
    this.champ1 = champ1;
  }

  @Override
  public String toString() {
    return "MonBean [champ1=" + this.champ1 + "]";
  }
}

Si la classe est modifiée, le champ serialVersionUID sera recalculé avec une version différente. Les données des sérialisations précédentes lèveront une InvalidClassException

Exemple :
import java.io.Serializable;

public class MonBean implements Serializable {

  private String champ1;
  private String champ2;

  public String getChamp1() {
    return this.champ1;
  }

  public String getChamp2() {
    return this.champ2;
  }

  public void setChamp2(final String champ2) {
    this.champ2 = champ2;
  }

  public void setChamp1(final String champ1) {
    this.champ1 = champ1;
  }

  @Override
  public String toString() {
    return "MonBean [champ1=" + this.champ1 + ", champ2=" + this.champ2 + "]";
  }
}

Résultat :
java.io.InvalidClassException: MonBean; local class incompatible: stream classdesc
serialVersionUID = -386157042668049345, local class serialVersionUID = -608800936009283713
        at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:562)
        at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1582)
        at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1495)
        at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1731)
        at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1328)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java:350)
        at SerDeserMonBean.main(SerDeserMonBean.java:22)

La sérialisation d'une classe interne n'est pas recommandée car elle possède une référence implicite sur son instance englobante. Cette référence est créée par le compilateur de manière dépendante de l'implémentation par son fournisseur. Ceci peut nuire à la portabilité. De plus, comme les classes internes ne peuvent pas avoir de contructeur par défaut, elles ne peuvent pas implémenter l'interface Externalizable.

 

17.1.1.1. L'interface Serializable

Cette interface ne définit aucune méthode mais permet simplement de marquer une classe comme pouvant être sérialisée.

Tout objet qui doit être sérialisé grâce au mécanisme par défaut doit implémenter cette interface ou une de ses classes mères doit l'implémenter.

Si l'on tente de sérialiser un objet qui n'implémente pas l'interface Serializable, une exception java.io.NotSerializableException est levée.

La classe ci-dessous sera utilisée dans certains exemples de ce chapitre.

Exemple :
public class Personne implements java.io.Serializable {
  private String nom    = "";
  private String prenom = "";
  private int    taille = 0;

  public Personne(final String nom, final String prenom, final int taille) {
    this.nom = nom;
    this.taille = taille;
    this.prenom = prenom;
  }

  public String getNom() {
    return this.nom;
  }

  public void setNom(final String nom) {
    this.nom = nom;
  }

  public int getTaille() {
    return this.taille;
  }

  public void setTaille(final int taille) {
    this.taille = taille;
  }

  public String getPrenom() {
    return this.prenom;
  }

  public void setPrenom(final String prenom) {
    this.prenom = prenom;
  }
}

Tous les objets du graphe doivent être sérializable. Si ce n'est pas le cas, plusieurs solutions sont possibles :

Tous les objets ne peuvent pas être sérialisés : c'est notamment le cas de certains objets qui sont dépendants du système (exemple : les threads, les flux, ... ). Ceci explique pourquoi la classe Object n'implémente pas l'interface Serializable.

 

17.1.1.2. La classe ObjectOutputStream

La classe ObjectOutputStream permet de sérialiser un objet ou un graphe d'objets. Cette sérialisation parcours le graphe d'objets pour les sérialiser les uns après les autres en tenant compte des éventuelles références déjà sérialisées. Par défaut, chaque objet qui est référencé par l'objet sérialisé est aussi sérialisé.

Elle implémente plusieurs interfaces : Closeable (depuis Java 5), DataOutput, Flushable (depuis Java 5), ObjectOutput, ObjectStreamConstants et AutoCloseable (depuis Java 7).

Elle ne possède qu'un constructeur public :

Constructeur

Rôle

ObjectOutputStream(OutputStream out)

Créer une instance qui va écrire le résultat de la sérialisation dans le flux fourni en paramètre


Le constructeur de la classe ObjectOutputStream attend en paramètre un flux de type OutputStream dans lequel les données de la sérialisation seront envoyées.

Exemple :
    ObjectOutputStream oos = null;

    try {
      final FileOutputStream fichier = new FileOutputStream("mon_objet.ser");
      oos = new ObjectOutputStream(fichier);
      // ...
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } finally {
      try {
        if (oos != null) {
          oos.flush();
          oos.close();
        }
      } catch (final IOException ex) {
        ex.printStackTrace();
      }
    }

La classe ObjectOutputStream propose de nombreuses méthodes pour permettre d'ajouter au flux de la sérialisation des objets sérialisables, des types primitifs, des tableaux, des chaînes de caractères, ... :

Méthode

Rôle

public final void writeObject(Object obj)

Sérialiser un objet

public void writeUnshared(Object obj)

 

public void defaultWriteObject()

Sérialiser un objet en utilisant les règles par défaut. Ne peut être invoquée que dans le corps de la méthode writeObject() sinon une exception de type NotActiveException est levée

public PutField putFields()

Obtenir les différents champs qui seront inclus dans le flux

public writeFields()

 

public void reset()

Réinitialiser l'état des objets utilisés par la classe

protected void annotateClass(Class cl)

 

protected void writeClassDescriptor(ObjectStreamClass desc)

Ecrire dans le flux une représentation de l' ObjectStreamClass fournie en paramètre

protected Object replaceObject(Object obj)

Permettre une substitution de l'objet par une autre instance de remplacement

protected boolean enableReplaceObject(boolean enable)

Activer la possibilité de remplacer l'objet écrit par une autre instance

protected void writeStreamHeader()

Ecrire les premiers octets du flux notamment la valeur du magic stream et le numéro de version du protocole

public void write(int data)

Ecrire un octet

public void write(byte b[])

Ecrire un ensemble d'octets

public void write(byte b[], int off, int len)

Ecrire un ensemble d'octets

public void flush()

Vider le tampon

protected void drain()

Similaire à flush mais les actions ne vont pas jusqu'au système

public void close()

Fermer le flux

public void writeBoolean(boolean data)
public void writeByte(int data)
public void writeShort(int data)
public void writeChar(int data)
public void writeInt(int data)
public void writeLong(long data)
public void writeFloat(float data)
public void writeDouble(double data)
public void writeBytes(String data)
public void writeChars(String data)

Ecrire une donnée primitive dans le bloc de données (block data)

public void writeUTF(String data)

Ecrire une chaîne de caractères encodée en UTF-8 modifié

public void useProtocolVersion(int version)

Préciser le numéro de version du protocole de sérialisation utilisé

protected writeObjectOverride()

 


La méthode writeObject() sérialise le graphe d'objets dont l'objet racine est fourni en paramètre.

Chaque classe fille d'une classe sérialisable peut redéfinir la méthode writeObject() : dans ce cas, les traitements de la méthode ne doivent généralement concerner que les champs de la classe elle-même.

Pour être sérialisée, la classe d'un objet doit implémenter l'interface Serializable.

Par défaut, le mécanisme de sérialisation d'un objet écrit dans le flux binaire :

Par défaut, tous les champs d'un objet sont sérialisés sauf :

Tous les champs doivent pouvoir être sérialisés (si le type d'un champ est une classe, celle-ci doit implémenter l'interface Serializable). Les champs qui ne peuvent pas être sérialisés doivent être marqués avec le mot clé transient.

Exemple :
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializerPersonne {

  public static void main(final String argv[]) {
    final Personne personne = new Personne("Dupond", "Jean", 175);
    ObjectOutputStream oos = null;

    try {
      final FileOutputStream fichier = new FileOutputStream("personne.ser");
      oos = new ObjectOutputStream(fichier);
      oos.writeObject(personne);
      oos.flush();
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } finally {
      try {
        if (oos != null) {
          oos.flush();
          oos.close();
        }
      } catch (final IOException ex) {
        ex.printStackTrace();
      }
    }
  }
}

On définit un fichier avec la classe FileOutputStream. On instancie un objet de la classe ObjectOutputStream en lui fournissant en paramètre le fichier : ainsi, le résultat de la sérialisation sera envoyé dans le fichier.

On appelle la méthode writeObject() en lui passant en paramètre l'objet à sérialiser. On appelle la méthode flush() pour vider le tampon dans le fichier et la méthode close() pour terminer l'opération.

Lors de ces opérations une exception de type IOException peut être levée si un problème intervient avec le fichier.

Après l'exécution de cet exemple, un fichier nommé « personne.ser » est créé. On peut visualiser son contenu mais surtout ne pas le modifier car sinon il serait corrompu. En effet, les données contenues dans ce fichier ne sont pas toutes au format caractères.

La classe ObjectOutputStream contient aussi plusieurs méthodes qui permettent de sérialiser des types élémentaires : writeInt(), writeDouble(), writeFloat(), ...

Il est possible dans un même flux d'écrire plusieurs objets les uns à la suite des autres. Ainsi plusieurs objets peuvent être sérialisés. Dans ce cas, il faut faire attention de relire les objets dans leur ordre d'écriture.

Exemple :
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializerDonnees {

  public static void main(final String argv[]) {
    final Personne personne = new Personne("Dupond", "Jean", 175, "1234");
    ObjectOutputStream oos = null;

    try {
      final FileOutputStream fichier = new FileOutputStream("donnees.ser");
      oos = new ObjectOutputStream(fichier);
      oos.writeObject(new java.util.Date());
      oos.writeObject(personne);
      final int[] tableau = { 1, 2, 3 };
      oos.writeObject(tableau);
      oos.writeUTF("ma chaine en UTF8");
      oos.writeLong(123456789);
      oos.writeObject("ma chaine de caracteres");

      oos.flush();
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } finally {
      try {
        if (oos != null) {
          oos.flush();
          oos.close();
        }
      } catch (final IOException ex) {
        ex.printStackTrace();
      }
    }
  }
}

Certaines informations apparaissent en clair dans le fichier contenant les données de la sérialisation visualisé dans un éditeur hexadécimal :

A partir des informations lues, il est possible de réécrire du code qui sera capable de lire le fichier.

 

17.1.1.3. La classe ObjectInputStream

La classe ObjectInputStream a pour but de désérialiser un objet précédemment sérialisé.

Elle implémente plusieurs interfaces : Closeable (depuis Java 5), DataInput (depuis Java 5), ObjectInput, ObjectStreamConstants et AutoCloseable (depuis Java 7).

Elle ne possède qu'un seul constructeur public qui attend en paramètre un objet de type InputStream qui encapsule le flux dans lequel les données sérialisées seront lues.

Elle possède de nombreuses méthodes :

Méthode

Rôle

Object readObject()

Désérialiser un objet

Object readUnshared()

 

void defaultReadObject()

Désérialiser un objet en utilisant les règles par défaut. Ne peut être invoquée que dans le corps de la méthode readObject() sinon une exception de type NotActiveException est levée

GetField readFields()

Lire les champs de l'objet sérialisé

void registerValidation(ObjectInputValidation obj, int priority)

Enregistrer un callback qui validera l'objet désérialisé. La priorité permet de gérer l'ordre d'exécution des callbacks : ils sont exécutés dans leur ordre de priorité décroissante.

Cette méthode ne peut être invoquée que dans une méthode readObject() sinon une exception de type NotActiveException est levée

ObjectStreamClass readClassDescriptor()

 

Class resolveClass(ObjectStreamClass v)

 

Object resolveObject(Object obj)

Renvoyer une autre instance que celle créée lors de la désérialisation

boolean enableResolveObject(boolean enable)

Activer la possibilité de remplacer l'objet lu par une autre instance

void readStreamHeader()

Lire les premiers octets du flux pour vérifier la valeur du magic stream et le numéro de version du protocole. Si ceux-ci sont erronés alors une exception de type StreamCorruptedMismatch est levée

int read()

Lire un octet

int read(byte[] data, int offset, int length)

Lire un ensemble d'octets

int available()

Retourner le nombre d'octets qui peuvent être lus dans le flux

void close()

Fermer le flux

boolean readBoolean()
byte readByte()
int readUnsignedByte()
short readShort()
int readUnsignedShort()
char readChar()
int readInt()
long readLong()
float readFloat()
double readDouble()

Lecture d'une donnée primitive dans le bloc de données (block data)

void readFully(byte[] data)

 

void readFully(byte[] data, int offset, int size)

 

int skipBytes(int len)

Ignorer dans le flux les prochains octets dont le nombre est précisé en paramètre

String readLine()

Deprecated

String readUTF()

Lire une chaîne de caractères encodée en UTF-8 modifié


La méthode readObject() renvoie une instance de type Object qu'il est nécessaire de caster vers le type présumé. Pour être sûr du type, il est possible d'effectuer un test sur le type de l'objet retourné en utilisant l'opérateur instanceof.

La JVM ne peut désérialiser un objet que si elle peut charger la classe pour en créer une instance : c'est la raison pour laquelle la méthode readObject() peut lever l'exception ClassNotFoundException.

Exemple :
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class DeSerializerPersonne {
  public static void main(final String argv[]) {

    ObjectInputStream ois = null;

    try {
      final FileInputStream fichier = new FileInputStream("personne.ser");
      ois = new ObjectInputStream(fichier);
      final Personne personne = (Personne) ois.readObject();
      System.out.println("Personne : ");
      System.out.println("nom : " + personne.getNom());
      System.out.println("prenom : " + personne.getPrenom());
      System.out.println("taille : " + personne.getTaille());
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } catch (final ClassNotFoundException e) {
      e.printStackTrace();
    } finally {
      try {
        if (ois != null) {
          ois.close();
        }
      } catch (final IOException ex) {
        ex.printStackTrace();
      }
    }
  }
}

Résultat :
Personne : 
nom : Dupond
prenom : Jean
taille : 175

On crée un objet de la classe FileInputStream qui représente le fichier contenant l'objet sérialisé puis un objet de type ObjectInputStream en lui passant le fichier en paramètre. Un appel à la méthode readObject() retourne l'objet avec un type Object. Un cast est nécessaire pour obtenir le type de l'objet. La méthode close() permet de terminer l'opération et libérer les ressources.

Si la classe a changé entre le moment où elle a été sérialisée et le moment où elle est désérialisée, une exception est levée. Exemple : la classe Personne est modifiée et recompilée

Résultat :
java.io.InvalidClassException: Personne;
local class incompatible: stream classdesc serialVersionUID =
503025058333454277, local class serialVersionUID = 1565267035218362193
      at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:562)
      at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1582)
      at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1495)
      at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1731)
      at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1328)
      at java.io.ObjectInputStream.readObject(ObjectInputStream.java:350)
      at DeSerializerPersonne.main(DeSerializerPersonne.java:13)

Une exception de type StreamCorruptedException peut être levée si le fichier a été corrompu par exemple en le modifiant avec un éditeur.

Exemple : les 2 premiers octets du fichier personne.ser ont été modifiés avec un éditeur hexa

Résultat :
java.io.StreamCorruptedException:
InputStream does not contain a serialized object
        at java.io.ObjectInputStream.readStreamHeader(ObjectInputStream.java:731)
        at java.io.ObjectInputStream.<init>(ObjectInputStream.java:165)
        at DeSerializerPersonne.main(DeSerializerPersonne.java:8)

Exemple : le nom est modifié dans le fichier personne.ser

Résultat :
java.io.StreamCorruptedException: invalid type code: 64
        at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1355)
        at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:1946)
        at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1870)
        at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1752)
        at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1328)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java:350)
        at DeSerializerPersonne.main(DeSerializerPersonne.java:13)

Une exception de type ClassNotFoundException peut être levée si l'objet est désérialisé vers une classe qui n'existe plus ou pas au moment de l'exécution.

Résultat :
java.lang.ClassNotFoundException: Personne
        at java.io.ObjectInputStream.inputObject(ObjectInputStream.java:981)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java:369)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java:232)
        at DeSerializerPersonne.main(DeSerializerPersonne.java:9)

La classe ObjectInputStream possède de la même façon que la classe ObjectOutputStream des méthodes pour lire des données de types primitives : readInt(), readDouble(), readFloat(), ...

Lors de la désérialisation, le constructeur de l'objet n'est jamais utilisé.

Exemple :
public class Personne implements java.io.Serializable {
  private String nom    = "";
  private String prenom = "";
  private int    taille = 0;

  public Personne(final String nom, final String prenom, final int taille) {
    this.nom = nom;
    this.taille = taille;
    this.prenom = prenom;
    System.out.println("invocation du constructeur");
  }

  public String getNom() {
    return this.nom;

  }

  public int getTaille() {
    return this.taille;
  }

  public String getPrenom() {
    return this.prenom;
  }
}

Résultat :
Personne : 
nom : Dupond
prenom : Jean
taille : 175

 

17.1.1.4. Le mot clé transient

Le mot clé transient permet de préciser qu'une variable d'instance ne doit pas être prise en compte lors de la sérialisation de l'état d'un objet.

Le contenu des attributs est visible dans le flux dans lequel est sérialisé l'objet. Il est ainsi possible pour toute personne ayant accès au flux de voir le contenu de chaque attribut même si ceux-ci sont private, ce qui peut poser des problèmes de sécurité surtout si les données sont sensibles.

Java introduit le mot clé transient qui précise que l'attribut qu'il qualifie ne doit pas être inclus dans un processus de sérialisation et donc de désérialisation.

Exemple :
public class Personne implements java.io.Serializable {
  private String           nom        = "";
  private String           prenom     = "";
  private int              taille     = 0;
  private transient String codeSecret = "";

  public Personne(final String nom, final String prenom, final int taille,
    final String codeSecret) {
    this.nom = nom;
    this.taille = taille;
    this.prenom = prenom;
    this.codeSecret = codeSecret;
  }

  public String getNom() {
    return this.nom;
  }

  public int getTaille() {
    return this.taille;
  }

  public String getPrenom() {
    return this.prenom;
  }

  public String getCodeSecret() {
    return this.codeSecret;
  }
}

Exemple :
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerDeserPersonne {

  public static void main(final String[] args) {
    Personne personne = new Personne("Dupond", "Jean", 175, "1234");
    ObjectOutputStream oos = null;
    ObjectInputStream ois = null;

    try {
      final FileOutputStream fichierOut = new FileOutputStream("personne.ser");
      oos = new ObjectOutputStream(fichierOut);
      oos.writeObject(personne);
      oos.flush();
      final FileInputStream fichierIn = new FileInputStream("personne.ser");
      ois = new ObjectInputStream(fichierIn);
      personne = (Personne) ois.readObject();
      System.out.println("Personne : ");
      System.out.println("nom : " + personne.getNom());
      System.out.println("prenom : " + personne.getPrenom());
      System.out.println("taille : " + personne.getTaille());
      System.out.println("codeSecret : " + personne.getCodeSecret());
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } catch (final ClassNotFoundException e) {
      e.printStackTrace();
    } finally {
      try {
        if (ois != null) {
          ois.close();
        }
        if (oos != null) {
          oos.close();
        }
      } catch (final IOException ex) {
        ex.printStackTrace();
      }
    }
  }
}

Résultat :
Personne : 
nom : Dupond
prenom : Jean
taille : 175
codeSecret : null

Lors de la désérialisation, les champs transient sont initialisés avec la valeur null. L'objet recréé doit donc gérer cet état pour éviter d'avoir des exceptions de type NullPointerException.

Il est aussi pratique de marquer transient des champs d'un type qui n'est pas sérialisable.

Exemple :
public class Personne implements java.io.Serializable {
  private String nom        = "";
  private String prenom     = "";
  private int    taille     = 0;
  private Thread monThread;

  public Personne() {
  }

  public Personne(final String nom, final String prenom, final int taille) {
    this.nom = nom;
    this.taille = taille;
    this.prenom = prenom;
    this.monThread = new Thread();
  }

  public String getNom() {
    return this.nom;
  }

  public int getTaille() {
    return this.taille;
  }

  public String getPrenom() {
    return this.prenom;
  }
}

Résultat :
java.io.NotSerializableException: java.lang.Thread
        at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1164)
        at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1518)
        at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1483)
        at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1400)
        at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1158)
        at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:330)
        at SerializerPersonne.main(SerializerPersonne.java:14)

Si une propriété de classe n'est pas sérialisable alors, on peut la marquer avec le mot clé transient pour qu'elle soit ignorée lors de la sérialisation.

Exemple :
public class Personne implements java.io.Serializable {
  private String           nom        = "";
  private String           prenom     = "";
  private int              taille     = 0;
  private transient Thread monThread;

  public Personne() {
  }

  public Personne(final String nom, final String prenom, final int taille) {
    this.nom = nom;
    this.taille = taille;
    this.prenom = prenom;
    this.monThread = new Thread();
  }

  public String getNom() {
    return this.nom;
  }

  public int getTaille() {
    return this.taille;
  }

  public String getPrenom() {
    return this.prenom;
  }
}

 

17.1.1.5. La gestion des versions d'une classe sérialisable

La gestion de la version d'une classe est importante car il est possible que la classe dans laquelle les données sont désérialisées soit différente de la classe de l'instance sérialisée.

La sérialisation inclut le numéro de version de la classe. Lors de la désérialisation, le numéro de version sérialisé est comparé au numéro de version de la classe : si ces numéros sont différents alors une exception de type InvalidClassException est levée.

Chaque classe qui implémente l'interface Serializable possède un numéro de version stocké dans un champ de type long nommé serialVersionUID.

Ce champ doit être déclaré static et final. Il est préférable de déclarer le champ serialVersionUID private car il y a normalement peut d'intérêt à être hérité puisqu'il concerne la déclaration de la classe elle-même.

Exemple :
private static final long serialVersionUID = 1L;

Si le champ serialVersionUID n'est pas explicitement précisé dans la classe, alors le compilateur va l'ajouter automatiquement en calculant une valeur. La spécification du langage Java ne précise pas comment cette valeur doit être calculée : l'algorithme utilisé peut être différent selon l'implémentation du compilateur proposé par le fournisseur du JDK. Il est donc recommandé de gérer explicitement le numéro de version pour maximiser la compatibilité.

Par exemple, avec le compilateur de Sun/Oracle, le serialVersionUID est le résultat du calcul de la signature avec SHA-1 d'un ensemble d'octets qui reprend les principales caractéristiques de la classe (nom de la classe, modificateurs d'accès, noms des interfaces implémentées, constructeurs non privés, méthodes non privées, champs non static ni transient, ...). Un décalage de bits est appliqué sur les deux premiers octets de la signature pour obtenir la valeur du serialVersionUID.

A partir de Java 5, le compilateur signale un warning pour les classes qui implémentent l'interface Serializable et qui ne définissent pas explicitement le serialVersionUID.

La valeur du champ serialVersionUID est toujours incluse dans les données contenant le résultat de la sérialisation. Il est utilisé lors de la désérialisation pour vérifier que les données de la sérialisation sont compatibles, relativement aux règles de la sérialisation, avec la classe chargée. Lors de la désérialisation cette valeur est comparée avec celle du champ serialVersionUID de la classe correspondante. Si les deux valeurs sont différentes alors la désérialisation lève une exception de type InvalidClassException.

Il est possible que la classe ait été modifiée entre le moment où une de ses instances a été sérialisée et le moment où une nouvelle instance est crée par désérialisation. C'est donc une bonne pratique de définir explicitement un champ serialVersionUID dans toutes les classes Serializable : ceci permet de garder le control sur la compatibilité des changements dans la classe.

Si une classe sérialisable ne déclare pas explicitement le champ serialVersionUID, une valeur par défaut utilisant différents éléments de la classe est calculée par le compilateur et lui est affectée. Comme la valeur par défaut est relativement sensible aux changements dans la classe, il est préférable de définir explicitement la valeur du serialVersionUID pour garder le contrôle sur la compatibilité :

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.Serializable;

public class NumeroVersion implements Serializable {
  private final String majeur;
  private final String mineur;

  public NumeroVersion(final String majeur, final String mineur) {
    this.majeur = majeur;
    this.mineur = mineur;
  }

  public String getVersion() {
    return this.majeur + "." + this.mineur;
  }
}

La classe est sérialisable mais ne définit pas explicitement le serialVersionUID.

Il n'y a aucun souci pour sérialiser une instance de cet objet.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializerNumeroVersion {

  public static void main(final String argv[]) {
    final NumeroVersion version = new NumeroVersion("1", "0");
    ObjectOutputStream oos = null;

    try {
      final FileOutputStream fichier = new FileOutputStream("numeroversion.ser");
      oos = new ObjectOutputStream(fichier);
      oos.writeObject(version);
      oos.flush();
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } finally {
      try {
        if (oos != null) {
          oos.flush();
          oos.close();
        }
      } catch (final IOException ex) {
        ex.printStackTrace();
      }
    }
  }
}

La classe est modifiée pour ajouter un champ revision.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.Serializable;

public class NumeroVersion implements Serializable {
  private final String majeur;
  private final String mineur;
  private final String revision;

  public NumeroVersion(final String majeur, final String mineur) {
    this(majeur, mineur, null);
  }

  public NumeroVersion(final String majeur, final String mineur, final String revision) {
    this.majeur = majeur;
    this.mineur = mineur;
    this.revision = revision;
  }

  public String getVersion() {
    return this.majeur + "." + this.mineur + this.revision;
  }
}

Si le fichier contenant la sérialisation de la précédente version de la classe est désérialisé, alors une exception de type InvalidClassException est levée.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class DeSerializerNumeroVersion {
  public static void main(final String argv[]) {

    ObjectInputStream ois = null;

    try {
      final FileInputStream fichier = new FileInputStream("numeroversion.ser");
      ois = new ObjectInputStream(fichier);
      final NumeroVersion version = (NumeroVersion) ois.readObject();

      System.out.println("version : " + version.getVersion());

    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } catch (final ClassNotFoundException e) {
      e.printStackTrace();
    } finally {
      try {
        if (ois != null) {
          ois.close();
        }
      } catch (final IOException ex) {
        ex.printStackTrace();
      }
    }
  }
}

Résultat :
java.io.InvalidClassException: fr.jmdoudoux.dej.serialisation.NumeroVersion; 
local class incompatible: stream classdesc serialVersionUID = -3291283991370239935, 
local class serialVersionUID = 5449821435975484475
        at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:560)
        at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1580)
        at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1493)
        at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1729)
        at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1326)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java:348)
        at fr.jmdoudoux.dej.serialisation.DeSerializerNumeroVersion.main
(DeSerializerNumeroVersion.java:15)

Pour gérer ce type de cas, en supposant que les deux versions de la classe soient compatibles, il est nécessaire de définir dans la première version une valeur explicite pour le champ serialVersionUID.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.Serializable;

public class NumeroVersion implements Serializable {

  private static final long serialVersionUID = 1L;

  private String            majeur;
  private String            mineur;

  public NumeroVersion(final String majeur, final String mineur) {
    this.majeur = majeur;
    this.mineur = mineur;
  }

  public String getVersion() {
    return this.majeur + "." + this.mineur;
  }

  public String getMajeur() {
    return this.majeur;
  }

  public String getMineur() {
    return this.mineur;
  }

  public void setMajeur(final String majeur) {
    this.majeur = majeur;
  }

  public void setMineur(final String mineur) {
    this.mineur = mineur;
  }
}

Dans la nouvelle version de la classe, il faut définir la même valeur pour le champ serialVersionUID et gérer le fait que la valeur du champ révision soit null.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.Serializable;

public class NumeroVersion implements Serializable {

  private static final long serialVersionUID = 1L;

  private final String      majeur;
  private final String      mineur;
  private final String      revision;

  public NumeroVersion(final String majeur, final String mineur) {
    this(majeur, mineur, null);
  }

  public NumeroVersion(final String majeur, final String mineur, final String revision) {
    this.majeur = majeur;
    this.mineur = mineur;
    this.revision = revision;
  }

  public String getVersion() {
    return this.majeur + "." + this.mineur + (this.revision == null ? "" : "."
       + this.revision);
  }
}

Il ne faut changer la valeur explicite d'un serialVersionUID que si les modifications faites dans la classe la rendent incompatible avec la version précédente.

La plupart des IDE propose une fonctionnalité pour calculer et ajouter la propriété serialVersionUID dans le code source de la classe.

Les JDK de Sun/Oracle proposent l'outil serialver qui permet d'obtenir le serialVersionIUD d'une classe.

Résultat :
C:\java\eclipse\workspace\TestSerialisation\bin>serialver
use: serialver [-classpath classpath] [-show] [classname...]

L'option - show permet d'ouvrir une petite interface graphique qui permet d'afficher le serialVersionUID d'une classe

Résultat :
C:\java\eclipse\workspace\TestSerialisation\bin>serialver
-show

Il suffit de passer le nom pleinement qualifié de la classe. Elle doit se trouver dans le classpath : l'option -classpath permet de le préciser au besoin.

Résultat :
C:\java\eclipse\workspace\TestSerialisation\bin>serialver
fr.jmdoudoux.dej.serialisation.MonBean
fr.jmdoudoux.dej.serialisation.MonBean:    static final long serialVersionUID =
-6364919964919905476L;

 

17.1.1.6. La compatibilité des versions de classes sérialisées

Il est fréquent de devoir gérer la compatibilité ascendante voire descendante pour permettre à une nouvelle version de la classe d'un objet d'être créée à partir des données sérialisées d'une précédente version ou éventuellement dans des cas plus rares de pouvoir créer une instance d'une précédente version à partir des données sérialisées d'une version plus récente de la classe de l'objet.

Certaines évolutions d'une classe peuvent avoir un impact sur la compatibilité des données sérialisées avec le mécanisme par défaut et ainsi ne plus garantir l'interopérabilité entre deux versions.

Il est donc important, lors de l'utilisation de la sérialisation, de prendre en compte la compatibilité des versions de la classe.

La sérialisation par défaut de Java peut gérer automatiquement plusieurs évolutions dans une classe sans que cela empêche la désérialisation de données de la version précédente de la classe (sous réserve que le serialVersionUID reste le même dans les deux versions de la classe)

Les changements qui impliquent une incompatibilité lors de l'utilisation de la sérialisation par défaut sont :

Les changements qui maintiennent la compatibilité sont :

 

17.1.1.7. Des points particuliers

Plusieurs points particuliers doivent être pris en compte lors de la mise en oeuvre de la sérialisation/désérialisation.

Les membres static d'un objet ne sont jamais sérialisés car ils ne concernent pas l'état d'un objet mais de tous les objets d'une même classe. Par exemple, une classe définit un membre static. Trois instances de cette classe sont sérialisées à des moments où la valeur du membre static est différente. Lors de la désérialisation, qu'elle serait la bonne valeur pour le membre static ?

Attention, lorsqu'un objet est désérialisé, le constructeur de sa classe n'est pas invoqué et les variables d'instances ne sont pas initialisées avec les valeurs qui pourraient leur être assignées.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.Serializable;

public class MaClasseTransient implements Serializable {

  transient int valeur = 1234;

  public MaClasseTransient() {
    super();
    System.out.println("Invocation du constructeur");
  }

  public int getValeur() {
    return this.valeur;
  }

  public void setValeur(final int valeur) {
    this.valeur = valeur;
  }
}

Dans ce cas, la valeur du champ transient est celle par défaut selon son type.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerDeserMaClasseTransient {
  public static void main(final String[] args) {
    MaClasseTransient maClasse = new MaClasseTransient();
    ObjectOutputStream oos = null;
    ObjectInputStream ois = null;

    try {
      System.out.println("Serialisation");
      final FileOutputStream fichierOut = new FileOutputStream("maclassetransient.ser");
      oos = new ObjectOutputStream(fichierOut);
      oos.writeObject(maClasse);
      oos.flush();

      System.out.println("Deserialisation");
      final FileInputStream fichierIn = new FileInputStream("maclassetransient.ser");
      ois = new ObjectInputStream(fichierIn);
      maClasse = (MaClasseTransient) ois.readObject();
      System.out.println("MaClasseTransient valeur : " + maClasse.getValeur());
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } catch (final ClassNotFoundException e) {
      e.printStackTrace();
    } finally {
      try {
        if (ois != null) {
          ois.close();
        }
        if (oos != null) {
          oos.close();
        }
      } catch (final IOException ex) {
        ex.printStackTrace();
      }
    }
  }
}

Résultat :
Invocation du constructeur
Serialisation
Deserialisation
MaClasseTransient valeur : 0

Si une classe mère implémente l'interface Serializable alors toutes les classes filles en héritent : il est donc inutile d'implémenter explicitement l'interface Serializable pour une classe fille. De fait, il n'est pas possible de facilement savoir si une classe est sérialisable ou non uniquement en regardant son code source : il est nécessaire de savoir si une classe mère implémente l'interface Serializable. La seule exception est si la classe hérite directement de la classe Object puisque celle-ci n'implémente pas l'interface Serializable.

L'héritage peut aussi avoir un impact sur la sérialisation notamment lorsqu'une classe fille est sérialisable mais qu'aucune de ses classes mères l'est.

Exemple :
package fr.jmdoudoux.dej.serialisation;

public class MaClasseMere {

  protected String nom = null;

  public String getNom() {
    return this.nom;
  }

  public void setNom(final String nom) {
    this.nom = nom;
  }

  public MaClasseMere() {
    System.out.println("Invocation contructeur MaClasseMere");
  }

  public MaClasseMere(final String nom) {
    super();
    this.nom = nom;
  }
}

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.Serializable;

public class MaClasseFille extends MaClasseMere implements Serializable {

  private int valeur = 0;

  public MaClasseFille() {
    System.out.println("Invocation contructeur MaClasseFille");
  }

  public MaClasseFille(final String nom, final int valeur) {
    super(nom);
    this.valeur = valeur;
  }

  public int getValeur() {
    return this.valeur;
  }

  public void setValeur(final int valeur) {
    this.valeur = valeur;
  }
}

Dans ce cas, l'état des propriétés de la classe mère est ignoré lors de la sérialisation : la valeur par défaut de ces propriétés est alors celle par défaut selon leur type.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerDeserMaClasseFille {
  public static void main(final String[] args) {

    MaClasseFille maClasseFille = new MaClasseFille("nom1", 123);
    ObjectOutputStream oos = null;
    ObjectInputStream ois = null;

    try {
      System.out.println("Serialisation");
      final FileOutputStream fichierOut = new FileOutputStream("maclassefille.ser");
      oos = new ObjectOutputStream(fichierOut);
      oos.writeObject(maClasseFille);
      oos.flush();

      System.out.println("Deserialisation");
      final FileInputStream fichierIn = new FileInputStream("maclassefille.ser");
      ois = new ObjectInputStream(fichierIn);
      maClasseFille = (MaClasseFille) ois.readObject();
      System.out.println("MaClasseFille : ");
      System.out.println("nom : " + maClasseFille.getNom());
      System.out.println("taille : " + maClasseFille.getValeur());
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } catch (final ClassNotFoundException e) {
      e.printStackTrace();
    } finally {
      try {
        if (ois != null) {
          ois.close();
        }
        if (oos != null) {
          oos.close();
        }
      } catch (final IOException ex) {
        ex.printStackTrace();
      }
    }
  }
}

Résultat :
MaClasseFille : 
nom : null
taille
: 123

Comme la classe mère n'est pas sérialisable, son état n'est pas pris en compte lors de la sérialisation/désérialisation même si ses champs sont hérités et accessibles dans la classe fille. Lors de la désérialisation, le constructeur de la classe mère est invoqué puisqu'elle n'est pas sérialisable : les valeurs des champs de la classe mère sont alors ceux par défaut selon leur type ou leur initialisation. Si la classe mère ne possède pas de constructeur par défaut alors une exception de type InvalidClassException est levée.

Dans ce cas, si la classe mère ne peut pas être modifiée pour implémenter l'interface Serializable, il est nécessaire de personnaliser la sérialisation pour inclure manuellement l'état des propriétés de la classe mère.

Une fois qu'une des classes de la hiérarchie implémente l'interface Serializable, toutes ses classes filles sont obligatoirement sérializable. Pour rendre une de ces classes filles non sérialisable, il faut personnaliser la sérialisation pour cette classe en définissant les méthodes writeObject() et readObject() pour qu'elle lève une exception de type NotSerializableException.

Les interfaces de l'API Collection ne sont pas sérialisables mais les classes concrètes qui les implémentent le sont. Il est très important que tous les éléments qui sont ajoutés à une collection qui doit être sérialisée soient sérialisables.

 

17.1.1.8. La vérification d'un objet désérialisé

Il est possible d'effectuer une validation des champs d'un objet désérialisé en faisant implémenter l'interface ObjectInputValidation par sa classe.

L'interface ObjectInputValidation ne définit qu'une seule méthode :

Méthode

Rôle

void validateObject()

Valider les valeurs des champs de l'objet


Si les traitements de validation échouent, alors la méthode validateObject() doit lever une exception de type InvalidObjectException.

Pour être invoquée, une instance de type ObjectInputValidation doit être enregistrée en utilisant la méthode registerValidation() de la classe ObjectInputStream. Celle-ci attend en paramètres l'instance de type ObjectInputValidation et un entier qui correspond à la priorité d'invocation.

Les règles de gestion des priorités sont :

Exemple :
package fr.jmdoudoux.dej.serialisation;
			
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectInputValidation;
import java.io.Serializable;

public class MaClasseValidation implements Serializable, ObjectInputValidation {
  private String nom;

  public String getNom() {
    return this.nom;
  }

  public void setNom(final String nom) {
    this.nom = nom;
  }

  @Override
  public void validateObject() throws InvalidObjectException {
    if (this.nom == null) {
      throw new InvalidObjectException("Le champ nom ne doit pas être vide");
    }
  }

  private void readObject(final ObjectInputStream in) throws IOException,
    ClassNotFoundException {
    in.registerValidation(this, 0);
    in.defaultReadObject();
  }
}

Résultat :
Serialisation
Deserialisation
java.io.InvalidObjectException: Le champ nom ne doit pas être vide
        at fr.jmdoudoux.dej.serialisation.MaClasseValidation.validateObject
(MaClasseValidation.java:23)
        at java.io.ObjectInputStream$ValidationList$1.run(ObjectInputStream.java:2210)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.io.ObjectInputStream$ValidationList.doCallbacks(ObjectInputStream.java:2206)
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java:355)
        at fr.jmdoudoux.dej.serialisation.SerDeserMaClasseValidation.main
(SerDeserMaClasseValidation.java:26)

 

17.1.1.9. Les exceptions liées à la sérialisation

L'API Serialization définit et utilise plusieurs exceptions qui héritent toutes de la classe ObjectStreamException. Cette classe est elle-même une classe fille de la classe IOException.

Exception

Rôle

ObjectStreamException

Classe mère des exceptions liées à la sérialisation

InvalidClassException

Levée lorsque qu'un objet ne peut pas être désérialisé à cause de sa classe :

  • le numéro de version de la classe ne correspond pas à celui des données sérialisées
  • le type d'un champ primitif ne correspond pas à celui d'une donnée sérialisée
  • la classe implémente l'interface Externalizable mais ne possède pas de constructeur par défaut accessible
  • La classe implémente l'interface Serializable mais une classe mère n'est pas sérialisable et ne possède pas de constructeur par défaut accessible

NotSerializableException

La classe n'est pas sérialisable

StreamCorruptedException

Le flux qui contient les données sérialisées est corrompu ou invalide (lecture de données sérialisées au format v2 avec un JDK inférieur à 1.1.5)

NotActiveException

La sérialisation n'est pas active

InvalidObjectException

La validation d'un objet désérialisé a échoué

OptionalDataException

La lecture de données primitives a échoué

WriteAbortedException

Une erreur est survenue durant l'écriture du flux

 

17.1.2. La sérialisation personnalisée

Le mécanisme de sérialisation par défaut de Java peut ne pas répondre à tous les besoins : dans ce cas, il est possible de personnaliser la façon dont un objet est sérialisé/désérialisé.

Dans certains cas, il est nécessaire d'utiliser cette personnalisation, par exemple :

Plusieurs mécanismes sont proposés en standard pour personnaliser la sérialisation/désérialisation :

Remarque : la sérialisation/désérialisation d'une énumération ne peut pas être personnalisée : les méthodes writeObject(), readObject(), readObjectNoData(), writeReplace() et readResolve() sont ignorées par le mécanisme de sérialisation. Les champs serialPersistentFields et serialVersionUID sont ignorés d'autant que ce dernier a toujours la valeur 0L.

 

17.1.2.1. La définition des champs à sérialiser

Le mécanisme de sérialisation par défaut ignore les champs transient et static.

A partir de Java 1.2, il est possible de préciser explicitement la liste des champs qui devront être pris en compte lors de la sérialisation/désérialisation en définissant un champ qui est un tableau de type java.io. ObjectStreamField.

Ce champ doit obligatoirement se nommer serialPersistentFields et doit être déclaré avec les modificateurs private, static et final.

Une instance de type ObjectStreamField est créée en passant le nom du champ et son type en paramètre du constructeur.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.ObjectStreamField;
import java.io.Serializable;

public class MonBean implements Serializable {

  private String                           champ1;
  private String                           champ2;

  private static final ObjectStreamField[] serialPersistentFields = 
    { new ObjectStreamField("champ1", String.class) };

  public String getChamp1() {
    return this.champ1;
  }

  public String getChamp2() {
    return this.champ2;
  }

  public void setChamp2(final String champ2) {
    this.champ2 = champ2;
  }

  public void setChamp1(final String champ1) {
    this.champ1 = champ1;
  }

  @Override
  public String toString() {
    return "MonBean [champ1=" + this.champ1 + ", champ2=" + this.champ2 + "]";
  }
}

Les champs qui ne sont pas inclus dans la sérialisation seront initialisés avec leur valeur par défaut lors de la désérialisation.

Résultat :
MonBean [champ1=valeur1, champ2=null]

Attention : l'utilisation d'un champ serialPersistentFields remplace purement et simple le mécanisme de recherche par défaut des champs à sérialiser. Ainsi un champ marqué transient mais défini dans le champ serialPersistentFields sera sérialisé.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.ObjectStreamField;
import java.io.Serializable;

public class MonBean implements Serializable {

  private transient String                 champ1;
  private transient String                 champ2;

  private static final ObjectStreamField[] serialPersistentFields = { 
      new ObjectStreamField("champ1", String.class),
      new ObjectStreamField("champ2", String.class)};

  public String getChamp1() {
    return this.champ1;
  }

  public String getChamp2() {
    return this.champ2;
  }

  public void setChamp2(final String champ2) {
    this.champ2 = champ2;
  }

  public void setChamp1(final String champ1) {
    this.champ1 = champ1;
  }

  @Override
  public String toString() {
    return "MonBean [champ1=" + this.champ1 + ", champ2=" + this.champ2 + "]";
  }
}

Résultat :
MonBean [champ1=valeur1, champ2=valeur2]

Si le champ serialPersistentFields est null, n'est pas un tableau de type ObjectStreamField ou n'est pas déclaré private static final alors il est ignoré par le mécanisme de sérialisation.

L'utilisation du champ serialPersistentFields n'est pas possible dans une classe interne pour des champs qui ne sont pas static.

 

17.1.2.2. Les méthodes writeObject() et readObject()

Les mécanismes de sérialisation/désérialisation par défaut ne sont pas toujours adaptés à certains besoins spécifiques qui requièrent une personnalisation des actions réalisées.

Pour cela, il est possible de définir les méthodes writeObject() et readObject() dans la classe à sérialiser.

Comme défini dans les spécifications de l'API Serialization, la signature de ces deux méthodes doit obligatoirement être :

Lors de la sérialisation/désérialisation d'un objet de la classe, ces deux méthodes seront invoquées en remplacement du mécanisme standard.

Cette section propose plusieurs cas d'utilisation de la personnalisation en utilisant les méthodes writeObject() et readObject().

Le premier cas permet de ne pas sérialiser un champ qui peut par exemple contenir des données sensibles comme un mot de passe.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class MaClasse implements Serializable {

  private String champ1;
  private String champ2;

  public String getChamp1() {
    return this.champ1;
  }

  public String getChamp2() {
    return this.champ2;
  }

  public void setChamp2(final String champ2) {
    this.champ2 = champ2;
  }

  public void setChamp1(final String champ1) {
    this.champ1 = champ1;
  }

  @Override
  public String toString() {
    return "MaClasse [champ1=" + this.champ1 + ", champ2=" + this.champ2 + "]";
  }

  private void readObject(final ObjectInputStream ois) throws IOException, 
    ClassNotFoundException {
    this.champ1 = (String) ois.readObject();
  }

  private void writeObject(final ObjectOutputStream oos) throws IOException {
    oos.writeObject(this.champ1);
  }

}

Les traitements réalisés par ces deux méthodes doivent être synchronisés : l'ordre d'ajout des attributs de l'objet dans la méthode writeObject() doit être le même que l'ordre de lecture des attributs dans la méthode readObject(). Si ce n'est pas le cas, généralement une exception de type java.io.OptionalDataException est levée.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class TestReadWriteObject {

  public static void main(final String[] args) {
    MaClasse maClasse = new MaClasse();
    maClasse.setChamp1("valeur1");
    maClasse.setChamp2("valeur2");

    ObjectOutputStream oos = null;
    ObjectInputStream ois = null;

    try {
      final FileOutputStream fichierOut = new FileOutputStream("maclasse.ser");
      oos = new ObjectOutputStream(fichierOut);
      oos.writeObject(maClasse);
      oos.flush();

      final FileInputStream fichierIn = new FileInputStream("maclasse.ser");
      ois = new ObjectInputStream(fichierIn);
      maClasse = (MaClasse) ois.readObject();

      System.out.println("" + maClasse);
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } catch (final ClassNotFoundException e) {
      e.printStackTrace();
    } finally {
      try {
        if (ois != null) {
          ois.close();
        }
        if (oos != null) {
          oos.close();
        }
      } catch (final IOException ex) {
        ex.printStackTrace();
      }
    }
  }
}

Résultat :
MaClasse [champ1=valeur1, champ2=null]

Les champs des classes mères qui implémentent l'interface Serializable seront utilisés par le mécanisme de sérialisation. Les méthodes readObject() et writeObject() ne doivent dans ce cas prendre en compte que les champs de la classe elle-même.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.Serializable;

public class MaClasseParent implements Serializable {

  private String champ3;

  public String getChamp3() {
    return this.champ3;
  }

  public void setChamp3(final String champ3) {
    this.champ3 = champ3;
  }
}

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class MaClasse extends MaClasseParent {

  private String champ1;
  private String champ2;

  public String getChamp1() {
    return this.champ1;
  }

  public String getChamp2() {
    return this.champ2;
  }

  public void setChamp2(final String champ2) {
    this.champ2 = champ2;
  }

  public void setChamp1(final String champ1) {
    this.champ1 = champ1;
  }

  @Override
  public String toString() {
    return "MaClasse [champ1=" + this.champ1 + ", champ2=" + this.champ2 + ",
    champ3=" + getChamp3() + "]";
  }

  private void readObject(final ObjectInputStream ois) throws IOException, 
    ClassNotFoundException {
    this.champ1 = (String) ois.readObject();
    this.champ2 = (String) ois.readObject();
  }

  private void writeObject(final ObjectOutputStream oos) throws IOException {
    oos.writeObject(this.champ1);
    oos.writeObject(this.champ2);
  }

}

Résultat :
MaClasse [champ1=valeur1, champ2=valeur2, champ3=valeur3]

Il peut être, par exemple, utile d'utiliser le mécanisme de personnalisation pour permettre de sérialiser des champs privés d'une classe mère qui n'est pas sérialisable.

Exemple :
package fr.jmdoudoux.dej.serialisation;


public class MaClasseParent {

  private String champ3;

  public String getChamp3() {
    return this.champ3;
  }

  public void setChamp3(final String champ3) {
    this.champ3 = champ3;
  }
}

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.Serializable;

public class MaClasse extends MaClasseParent implements Serializable {

  private String champ1;
  private String champ2;

  public String getChamp1() {
    return this.champ1;
  }

  public String getChamp2() {
    return this.champ2;
  }

  public void setChamp2(final String champ2) {
    this.champ2 = champ2;
  }

  public void setChamp1(final String champ1) {
    this.champ1 = champ1;
  }

  @Override
  public String toString() {
    return "MaClasse [champ1=" + this.champ1 + ", champ2=" + this.champ2 + ", 
    champ3=" + getChamp3() + "]";
  }
}

Si on sérialise/désérialise cette classe avec les mécanismes par défaut, le champ privé de la classe mère n'est pas pris en compte

Résultat :
MaClasse [champ1=valeur1, champ2=valeur2, champ3=null]

Pour forcer la sérialisation de ce champs, il faut personnaliser l'opération de sérialisation en implémentant les méthodes readObject() et writeObject().

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class MaClasse extends MaClasseParent implements Serializable {

  private String champ1;
  private String champ2;

  public String getChamp1() {
    return this.champ1;
  }

  public String getChamp2() {
    return this.champ2;
  }

  public void setChamp2(final String champ2) {
    this.champ2 = champ2;
  }

  public void setChamp1(final String champ1) {
    this.champ1 = champ1;
  }

  @Override
  public String toString() {
    return "MaClasse [champ1=" + this.champ1 + ", champ2=" + this.champ2 + ",
    champ3=" + getChamp3() + "]";
  }

  private void readObject(final ObjectInputStream ois) throws IOException, 
    ClassNotFoundException {
    this.champ1 = (String) ois.readObject();
    this.champ2 = (String) ois.readObject();
    setChamp3((String) ois.readObject());
  }

  private void writeObject(final ObjectOutputStream oos) throws IOException {
    oos.writeObject(this.champ1);
    oos.writeObject(this.champ2);
    oos.writeObject(getChamp3());
  }

}

Résultat :
MaClasse
[champ1=valeur1, champ2=valeur2, champ3=valeur3]

Pour utiliser le mécanisme de sérialisation par défaut, il est possible d'invoquer les méthodes :

L'utilisation de ces méthodes est par exemple pratique lorsque la personnalisation de la sérialisation/désérialisation consiste simplement à vérifier l'intégrité des données désérialisées ou à réaliser des opérations pré/post sérialisation/désérialisation.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class MaPosition implements Serializable {

  private final int x;
  private final int y;

  public MaPosition(final int x, final int y) {
    this.x = x;
    this.y = y;
    verifierIntegrite();
  }

  public int getX() {
    return this.x;
  }

  public int getY() {
    return this.y;
  }

  private void readObject(final ObjectInputStream o) throws IOException, 
    ClassNotFoundException {
    o.defaultReadObject();
    verifierIntegrite();
  }

  private void writeObject(final ObjectOutputStream o) throws IOException {
    o.defaultWriteObject();
  }

  protected void verifierIntegrite() {
    if ((this.x < 0) || (this.y < 0)) {
      throw new IllegalArgumentException(
        "Violation des contraintes d'integrite x=" + this.x + ", y=" + this.y);
    }
  }
}

Exemple :
Exception in thread "main" java.lang.IllegalArgumentException: Violation des contraintes
 d'integrite x=-1, y=-1
        at fr.jmdoudoux.dej.serialisation.MaPosition.verifierIntegrite(MaPosition.java:38)
        at fr.jmdoudoux.dej.serialisation.MaPosition.<init>(MaPosition.java:16)
        at fr.jmdoudoux.dej.serialisation.SerDeserMaPosition.main(SerDeserMaPosition.java:12)

Ceci peut permettre de valider les données au cas où celles-ci auraient été modifiées. Il est aussi possible d'utiliser l'interface ObjectInputValidation qui est la solution standard pour valider des données désérialisées.

 

17.1.2.3. La méthode readObjectNoData()

Depuis Java 1.4, il est possible de définir la méthode readObjectNoData() dans une classe qui implémente l'interface Serializable. Celle-ci sera invoquée si des données à désérialiser ne comportent rien concernant cette classe : c'est par exemple le cas si la hiérarchie de classes de l'objet sérialisé a changé entre la sérialisation d'un objet de cette classe et sa désérialisation.

Par exemple, une classe est sérialisable et une de ses instances est sérialisée dans un fichier.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.Serializable;

public class ClasseFille implements Serializable {

  private int valeur = 0;

  public int getValeur() {
    return this.valeur;
  }

  public void setValeur(final int valeur) {
    this.valeur = valeur;
  }

}

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.Serializable;

public class ClasseFille implements Serializable {

  private int valeur = 0;

  public int getValeur() {
    return this.valeur;
  }

  public void setValeur(final int valeur) {
    this.valeur = valeur;
  }

}

Le fichier contient les données de la classe sérialisées. Il n'y a aucun souci pour désérialiser le contenu de ce fichier.

Exemple :
package fr.jmdoudoux.dej.serialisation;
			
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class DeSerializerClasseFille {

  public static void main(final String argv[]) {
    ObjectInputStream ois = null;
    try {
      final FileInputStream fichier = new FileInputStream("classefille.ser");
      ois = new ObjectInputStream(fichier);
      final ClasseFille classeFille = (ClasseFille) ois.readObject();
      System.out.println(classeFille);
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } catch (final ClassNotFoundException e) {
      e.printStackTrace();
    } finally {
      try {
        if (ois != null) {
          ois.close();
        }
      } catch (final IOException ex) {
        ex.printStackTrace();
      }
    }
  }
}

La classe ClasseFille est modifiée pour hériter d'une classe ClasseMere. La désérialisation ne pose pas de problème : dans ce cas, les champs de la classe mère seront initialisées avec leurs valeurs par défaut.

Si elle est sérialisable, il est possible de définir la méthode readObjectNoData() qui alors sera invoquée lors de la désérialisation.

La signature de cette méthode doit être :

  private void readObjectNoData() throws ObjectStreamException;
Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.InvalidObjectException;
import java.io.Serializable;

public class ClasseMere implements Serializable {
  private String nom;

  public String getNom() {
    return this.nom;
  }

  public void setNom(final String nom) {
    this.nom = nom;
  }

  private void readObjectNoData() throws InvalidObjectException {
    throw new InvalidObjectException("Donnees serialisee manquante");
  }
}

Résultat :
java.io.InvalidObjectException: Donnees serialisee manquante
    at fr.jmdoudoux.dej.serialisation.ClasseMere.readObjectNoData(ClasseMere.java:18)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at java.io.ObjectStreamClass.invokeReadObjectNoData(ObjectStreamClass.java:999)
    at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1886)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1756)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1326)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:348)
    at fr.jmdoudoux.dej.serialisation.DeSerializerClasseFille.main
        (DeSerializerClasseFille.java:15)

Dans l'exemple ci-dessus, si les données de l'objet sont manquantes alors une exception est levée. Il est aussi possible d'initialiser les données des champs de la classe.

Ceci est particulièrement utile par exemple si les champs de la classe mère possèdent des invariants qui ne seraient pas respectés à cause des valeurs par défaut.

 

17.1.2.4. Les méthodes writeReplace() et readResolve()

Lors de l'utilisation du mécanisme de sérialisation, il est parfois nécessaire d'avoir un contrôle sur l'instance obtenue ou à sérialiser. C'est notamment le cas si la classe est un singleton.

Ce cas d'utilisation met en oeuvre les méthodes writeReplace et readResolve().

La méthode readResolve() permet d'avoir un contrôle direct sur le type et l'instance retournés lors de la désérialisation. Elle doit avoir la signature suivante :

Object readResolve() throws ObjectStreamException;

La classe ObjectInputStream vérifie si la classe possède une méthode readResolve() avec cette signature et si c'est le cas elle l'invoque à la place du mécanisme standard. L'objet retourné par la méthode readResolve() doit cependant être compatible avec la classe de l'objet sérialisé sinon une exception de type ClassCastException est levée.

La méthode writeReplace() permet de remplacer l'instance de l'objet qui sera sérialisé. Elle doit avoir la signature suivante :

Object writeReplace() throws ObjectStreamException;

La classe ObjectInputStream vérifie si la classe possède une méthode writeReplace() avec cette signature et si c'est le cas elle l'invoque pour obtenir l'instance à sérialiser. Le type de cette instance doit être compatible avec la classe de l'objet sérialisé sinon une exception de type ClassCastException est levée.

Ces deux méthodes sont utilisables pour des classes qui implémentent Serializable ou Externalizable.

Les méthodes readResolve() et WriteReplace() peuvent avoir n'importe quel modificateur d'accès.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.Serializable;

public final class MonSingleton implements Serializable {

  private static final long   serialVersionUID = -1572447373762477721L;

  private static MonSingleton instance         = new MonSingleton();

  public static MonSingleton getlnstance() {
    return MonSingleton.instance;
  }

  private MonSingleton() {
  }
}

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerializerSingleton {

  public static void main(final String[] args) {
    try {
      final MonSingleton singleton1 = MonSingleton.getlnstance();
      System.out.println(singleton1);
      final FileOutputStream fos = new FileOutputStream("singleton.ser");
      final ObjectOutputStream oos = new ObjectOutputStream(fos);
      try {
        oos.writeObject(singleton1);
        oos.flush();
      } finally {
        try {
          oos.close();
        } finally {
          fos.close();
        }
      }

      final FileInputStream fis = new FileInputStream("singleton.ser");
      final ObjectInputStream ois = new ObjectInputStream(fis);
      try {
        final MonSingleton singleton2 = (MonSingleton) ois.readObject();
        System.out.println(singleton2);
      } finally {
        try {
          ois.close();
        } finally {
          fis.close();
        }
      }
    } catch (final ClassNotFoundException cnfe) {
      cnfe.printStackTrace();
    } catch (final IOException ioe) {
      ioe.printStackTrace();
    }
  }
}

Résultat :
fr.jmdoudoux.dej.serialisation.MonSingleton@addbf1
fr.jmdoudoux.dej.serialisation.MonSingleton@c17164

Lors de la désérialisation, une nouvelle instance du singleton est créée ce qui rompt le contrat posé par le motif de conception puisque deux instances existent dans la JVM.

Pour résoudre ce problème, il faut définir la méthode readResolve() dans la classe à sérialiser.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.ObjectStreamException;
import java.io.Serializable;

public final class MonSingleton implements Serializable {

  private static final long   serialVersionUID = -1572447373762477721L;

  private static MonSingleton instance         = new MonSingleton();

  public static MonSingleton getlnstance() {
    return MonSingleton.instance;
  }

  private MonSingleton() {
  }

  protected Object readResolve() throws ObjectStreamException {
    return MonSingleton.getlnstance();
  }
}

Résultat :
fr.jmdoudoux.dej.serialisation.MonSingleton@addbf1
fr.jmdoudoux.dej.serialisation.MonSingleton@addbf1

Ce mécanisme peut aussi permettre d'utiliser des proxies au lieu de la classe à sérialiser/désérialiser.

 

17.1.2.5. L'interface Externalizable

L'implémentation de l'interface Externalizable permet d'avoir un contrôle très fin sur les opérations de sérialisation et désérialisation lorsque les mécanismes de sérialisation par défaut ne répondent pas au besoin.

L'interface Externalizable hérite de l'interface Serializable. Elle définit deux méthodes :

Méthode

Rôle

void readExternal(ObjectInput in)

Désérialiser de manière personnalisée l'objet à partir du flux passé en paramètre. Les méthodes de l'objet de type DataInput permettent de lire des valeurs primitives et la méthode readObject() de la classe ObjectInput permet de lire et créer des objets

void writeExternal(ObjectOutput out)

Sérialiser de manière personnalisée l'état de l'objet dans le flux passé en paramètre. Les méthodes de l'objet de type DataOutput permettent d'écrire des valeurs primitives et la méthode writeObject() de la classe ObjectOutput permet d'écrire des objets


Exemple :
package fr.jmdoudoux.dej.serialisation;
			
import java.i o.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Date;

public class Personne implements java.io.Externalizable {
  private String           nom        = "";
  private String           prenom     = "";
  private int              taille     = 0;
  private Date             dateNaiss  = null;
  private transient String codeSecret = "";

  public Personne() {
  }

  public Personne(final String nom, final String prenom, final int taille, final String
codeSecret, final Date dateNaiss) {
    this.nom = nom;
    this.taille = taille;
    this.prenom = prenom;
    this.codeSecret = codeSecret;
    this.dateNaiss = dateNaiss;
  }

  public String getNom() {
    return this.nom;
  }

  public void setNom(final String nom) {
    this.nom = nom;
  }

  public int getTaille() {
    return this.taille;
  }

  public void setTaille(final int taille) {
    this.taille = taille;
  }

  public String getPrenom() {
    return this.prenom;
  }

  public void setPrenom(final String prenom) {
    this.prenom = prenom;
  }

  public String getCodeSecret() {
    return this.codeSecret;
  }

  public Date getDateNaiss() {
    return this.dateNaiss;
  }

  public void setDateNaiss(final Date dateNaiss) {
    this.dateNaiss = dateNaiss;
  }

  @Override public void writeExternal(final ObjectOutput out) throws IOException {
  }

  @Override public void readExternal(final ObjectInput in) throws IOException, 
  ClassNotFoundException {
  }
}

La sérialisation et la désérialisation se font en utilisant les classes ObjetOutputStream et ObjectInputStream.

Résultat :
Personne : 
nom : 
prenom : 
taille : 0
codeSecret :

Par défaut, la sérialisation d'un objet qui implémente cette interface ne prend en compte aucun attribut de l'objet. Seul le type de la classe est par défaut écrit dans le flux lors de la sérialisation : l'écriture de l'état de l'objet et sa restauration sont de la responsabilité du développeur.

Exemple :
package fr.jmdoudoux.dej.serialisation;
			
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Date;

public class Personne implements java.io.Externalizable {
  private String           nom        = "";
  private String           prenom     = "";
  private int              taille     = 0;
  private Date             dateNaiss  = null;
  private transient String codeSecret = "";

  public Personne() {
  }

  // ... getters et setters

  @Override
  public void writeExternal(final ObjectOutput out) throws IOException {
    out.writeUTF(this.nom);
    out.writeUTF(this.prenom);
    out.writeObject(this.dateNaiss);
    out.writeInt(this.taille);
    out.writeUTF(this.codeSecret);
  }

  @Override
  public void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException {
    this.nom = in.readUTF();
    this.prenom = in.readUTF();
    this.dateNaiss = (Date) in.readObject();
    this.taille = in.readInt();
    this.codeSecret = in.readUTF();
  }
}

Remarque : le mot clé transient est inutile avec une classe qui implémente l'interface Externalizable

Résultat :
Personne : 
nom : Dupond
prenom : Jean
taille : 175
date naissance :
Fri Jun 13 00:00:00 CET 1975
codeSecret : 1234

Les données du flux de sérialisation sont facilement exploitables même si c'est un format binaire. Dans l'exemple, ci-dessus le code secret apparait en clair puisque c'est une chaîne de caractères.

Le fait d'avoir le contrôle total sur les opérations de sérialisation/désérialisation permet par exemple de modifier les valeurs de certaines données de l'état de l'objet.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Date;

public class Personne implements java.io.Externalizable {
  private String           nom        = "";
  private String           prenom     = "";
  private int              taille     = 0;
  private Date             dateNaiss  = null;
  private transient String codeSecret = "";

  public Personne() {
  }

  // ... getters et setters

  @Override
  public void writeExternal(final ObjectOutput out) throws IOException {
    out.writeUTF(this.nom);
    out.writeUTF(this.prenom);
    out.writeObject(this.dateNaiss);
    out.writeInt(this.taille);
    out.writeUTF(new StringBuilder(this.codeSecret).reverse().toString());
  }

  @Override
  public void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException {
    this.nom = in.readUTF();
    this.prenom = in.readUTF();
    this.dateNaiss = (Date) in.readObject();
    this.taille = in.readInt();
    this.codeSecret = new StringBuilder(in.readUTF()).reverse().toString();
  }
}

Le résultat est le même mais la valeur contenue dans le flux n'est plus utilisable directement.

Cet exemple est simpliste car il inverse simplement d'ordre des caractères de la chaîne : en réalité, il serait nécessaire d'utiliser un mécanisme de chiffrement/déchiffrement beaucoup plus robuste.

Lors de la désérialisation d'un objet Externalizable, une nouvelle instance est créée en invoquant le constructeur par défaut puis la méthode readExternal() est invoquée. Une classe qui implémente l'interface Externalizable doit donc obligatoirement proposer un constructeur par défaut, sinon une exception de type InvalidClassException est levée.

Résultat :
Caused by: java.io.InvalidClassException: Personne; no valid constructor
        at java.io.ObjectStreamClass.<init>(ObjectStreamClass.java:471)
        at java.io.ObjectStreamClass.lookup(ObjectStreamClass.java:310)
        at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1114)
        at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:330)
        at SerDeserPersonne.main(SerDeserPersonne.java:16)

L'obligation d'avoir un constructeur par défaut explique la raison pour laquelle une classe interne ne peut pas mettre en oeuvre le mécanisme utilisant l'interface Externalizable.

Lors de l'utilisation de l'interface Externalizable, les mécanismes de sérialisation/désérialisation mis en oeuvre doivent tenir compte de l'état des membres hérités, des valeurs par défaut, des membres transient et static , ...

Par exemple, une classe mère possède un champ et implémente l'interface Externalizable.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class MaClasseMere implements Externalizable {

  public static final long serialVersionUID = 1;

  private String nom = null;

  public MaClasseMere() {
  }

  public MaClasseMere(final String nom) {
    super();
    this.nom = nom;
  }

  public String getNom() {
    return this.nom;
  }

  public void setNom(final String nom) {
    this.nom = nom;
  }

  @Override
  public void writeExternal(final ObjectOutput out) throws IOException {
    out.writeUTF(this.nom);
  }

  @Override
  public void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException {
    this.nom = in.readUTF();
  }
}

La classe fille hérite de la classe mère et ajoute un nouveau champ de type int.

Exemple :
package fr.jmdoudoux.dej.serialisation;

public class MaClasseFille extends MaClasseMere {

  public static final long serialVersionUID = 1;

  private int valeur = 0;

  public MaClasseFille() {
  }

  public MaClasseFille(final String nom, final int valeur) {
    super(nom);
    this.valeur = valeur;
  }

  public int getValeur() {
    return this.valeur;
  }

  public void setValeur(final int valeur) {
    this.valeur = valeur;
  }
}

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerDeserMaClasseFille {

  public static void main(final String[] args) {

    MaClasseFille maClasseFille = new MaClasseFille("nom1", 123);
    ObjectOutputStream oos = null;
    ObjectInputStream ois = null;

    try {
      final FileOutputStream fichierOut = new FileOutputStream("maclassefille.ser");
      oos = new ObjectOutputStream(fichierOut);
      oos.writeObject(maClasseFille);
      oos.flush();

      final FileInputStream fichierIn = new FileInputStream("maclassefille.ser");
      ois = new ObjectInputStream(fichierIn);
      maClasseFille = (MaClasseFille) ois.readObject();
      System.out.println("MaClasseFille : ");
      System.out.println("nom : " + maClasseFille.getNom());
      System.out.println("taille : " + maClasseFille.getValeur());
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } catch (final ClassNotFoundException e) {
      e.printStackTrace();
    } finally {
      try {
        if (ois != null) {
          ois.close();
        }
        if (oos != null) {
          oos.close();
        }
      } catch (final IOException ex) {
        ex.printStackTrace();
      }
    }
  }
}

Résultat :
MaClasseFille : 
nom : nom1
taille : 0

La classe fille doit redéfinir les méthodes writeExternal() et readExternal() en invoquant leur équivalent de la classe mère.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class MaClasseFille extends MaClasseMere {

  public static final long serialVersionUID = 1;

  private int valeur = 0;

  public MaClasseFille() {
  }

  public MaClasseFille(final String nom, final int valeur) {
    super(nom);
    this.valeur = valeur;
  }

  public int getValeur() {
    return this.valeur;
  }

  public void setValeur(final int valeur) {
    this.valeur = valeur;
  }

  @Override
  public void writeExternal(final ObjectOutput out) throws IOException {
    super.writeExternal(out);
    out.writeInt(this.valeur);
  }

  @Override
  public void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException {
    super.readExternal(in);
    this.valeur = in.readInt();
  }
}

Résultat :
MaClasseFille : 
nom : nom1
taille : 123

Il est possible que la classe mère n'implémente pas l'interface Externalizable et qu'il ne soit pas possible de la modifier.

Exemple :
package fr.jmdoudoux.dej.serialisation;

public class MaClasseMere {

  public static final long serialVersionUID = 1;

  protected String nom = null;

  public String getNom() {
    return this.nom;
  }

  public void setNom(final String nom) {
    this.nom = nom;
  }

  public MaClasseMere() {
  }

  public MaClasseMere(final String nom) {
    super();

    this.nom = nom;
  }
}

Dans ce cas, la classe fille doit implémenter l'interface Externalizable et redéfinir les méthodes writeExternal() et readExternal() pour tenir compte de l'état des membres de la classe fille et de la classe mère.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class MaClasseFille extends MaClasseMere implements Externalizable {

  public static final long serialVersionUID = 1;

  private int valeur = 0;

  public MaClasseFille() {
  }

  public MaClasseFille(final String nom, final int valeur) {
    super(nom);
    this.valeur = valeur;
  }

  public int getValeur() {
    return this.valeur;
  }

  public void setValeur(final int valeur) {
    this.valeur = valeur;
  }

  @Override
  public void writeExternal(final ObjectOutput out) throws IOException {
    out.writeUTF(this.nom);
    out.writeInt(this.valeur);
  }

  @Override
  public void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException {
    this.nom = in.readUTF();
    this.valeur = in.readInt();
  }
}

Lors de la sérialisation, les méthodes writeExternal() et readExternal() sont utilisées prioritairement aux méthodes writeObjet() et readObject().

La sérialisation en utilisant un Externalizable est généralement plus performante que la sérialisation standard car cette dernière requièrt l'utilisation de l'introspection pour déterminer les éléments à inclure.

En contrepartie, l'utilisation d'un Externalizable est moins sécurisée car les méthodes à redéfinir sont publiques alors que les méthodes de la sérialisation standard sont privées.

 

17.1.2.6. Les différences entre Serializable et Externalizable

Bien que ces deux interfaces soient utilisées pour sérialiser/désérialiser un objet, il existe plusieurs différences entre l'utilisation des interfaces Serializable et Externalizable :

 

17.2. La documentation d'une classe sérialisable

L'outil javadoc propose de prendre en compte la documentation d'une classe sérialisable.

Son utilisation est importante pour permettre de documenter comment la classe est sérialisée actuellement : cela facilite le travail lors de la création d'une sous-classe ou lors d'évolutions dans la classe.

Javadoc propose, depuis sa version 1.2, trois tags dédiés à la documentation de la manière avec laquelle une classe est sérialisée :

Tag

Rôle

@serial

Ce tag s'utilise dans les commentaires Javadoc d'un champ qui est sérialisé par défaut

@serialField

Ce tag s'utilise dans les commentaires Javadoc du champ serialPersistentFields de type ObjectStreamField[] pour décrire les différentes données qui sont sérialisées. Il faut utiliser un tag par données

@serialData

Ce tag s'utilise dans les commentaires Javadoc des méthodes writeObject(), readObject(), writeExternal(), readExternal(), writeReplace() et readResolve() pour décrire quelles données sont sérialisées et dans quel ordre


Le tag @serial s'utilise dans les commentaires Javadoc d'un champ sérialisé par défaut.

Sa syntaxe est :

@serial  [ field-description ] [ include | exclude ]

La description est optionnelle : elle permet d'expliquer le sens du champ et sa liste de valeurs acceptables.

Les arguments include et exclude permettent de préciser pour une classe ou package s'il doit être ajouté dans la page serialized-form. Par défaut :

Le tag @serial au niveau de la classe est prioritaire sur celui au niveau du package.

Le doclet standard émet un warning sur les champs private d'une classe sérialisable qui ne sont pas marqués @serial.

Le tag @serialField permet de décrire un champ sérialisé encapsulé dans un objet de type ObjectStreamField. Il s'utilise dans les commentaires du champ serialPersistentFields de type ObjectStreamField[]. Il faut utiliser un tag @serialField pour chaque champ.

Sa syntaxe est :

@serialField field-name field-type field-description

Exemple :
  /**
   * @serialField champ1 String description du champ1
   * @serialField champ2 String description du champ2
   */
  private static final ObjectStreamField[] serialPersistentFields = 
    { new ObjectStreamField("champ1", String.class),
      new ObjectStreamField("champ2", String.class)};

@serialData data-description

Le tag @serialData s'utilise pour décrire les données qui sont manuellement ajoutées dans les données sérialisées.

Il s'utilise sur les méthodes writeObject(), readObject(), writeExternal(), readExternal(), writeReplace() et readResolve().

La description permet de préciser les données et l'ordre dans lequel elles sont ajoutées dans les données optionnelles.

Si un nouveau champ sérialisable est ajouté, il est possible d'utiliser le tag Javadoc @since pour préciser depuis quelle version.

Lors de la génération de la documentation Javadoc, une page dédié à la sérialisation, nommée serialized-form.html est générée. Elle contient toutes les classes qui implémentent Serializable ou Externalizable et pour chacune propose une description des champs sérialisés, des données optionnelles et des méthodes relatives à la sérialisation.

Il n'y a pas de lien proposé dans la barre de navigation vers cette page : il faut ouvrir la page d'une classe sérialisable et cliquer sur le lien « Serialized form » dans la section « See also ».

Le doclet par défaut utilise les métadonnées de la classe et les informations fournies par les tags @serial, @serialFields et @serialData pour générer la page.

 

17.3. La sérialisation et la sécurité

La sérialisation n'est pas sécurisée : même si le format par défaut est un format binaire, ce format est connu et documenté. Le contenu des données binaires peut assez facilement permettre de définir une classe qui permettra de lire le contenu du résultat de la sérialisation.

Un simple éditeur hexadécimal permet d'obtenir les valeurs des différents champs sérialisés même ceux qui sont déclarés privés.

Il ne faut pas sérialiser de données sensibles par le processus de sérialisation standard car cela rend public ces données. Une fois un objet sérialisé, les mécanismes d'encapsulation de Java ne sont plus mis en oeuvre : il est possible d'accéder aux champs private par exemple. Il faut soit :

Le mécanisme de désérialisation permet de créer de nouvelles instances : tous les contrôles qui sont faits dans le constructeur doivent aussi être faits lors de la désérialisation.

 

17.4. La sérialisation en XML

A partir de Java 1.4, le JDK permet de sérialiser un objet Java en XML plutôt que sous un format binaire en utilisant les classes XMLEncoder et XMLDecoder.

La sérialisation binaire requiert une analogie entre les méthodes readObject() et la méthode writeObject() correspondante. La sérialisation XML repose sur une approche différente qui se base sur l'API publique d'une classe. C'est la raison pour laquelle l'utilisation de cette fonctionnalité ne peut s'appliquer par défaut que sur des objets qui respectent la convention JavaBeans.

La sérialisation XML présente quelques avantages :

Elle a aussi plusieurs inconvénients :

Attention, en Java 5, les énumérations ne sont pas supportées par les classes XMLEncoder et XMLDecoder.

 

17.4.1. La classe XMLEncoder

La classe java.beans.XMLEncoder permet de sérialiser un objet en XML.

La classe XMLEncoder permet de sérialiser l'état d'un JavaBean dans un document XML encodé en UTF-8. La sérialisation XML ne prend en compte que les champs pour lesquels il existe un getter et un setter public. Le mécanisme de sérialisation XML optimise le contenu du document XML en omettant les champs dont la valeur est celle par défaut.

La classe XMLEncoder possède deux constructeurs :

Constructeur

Rôle

XMLEncoder(OutputStream out)

Créer une nouvelle instance qui utilise le flux en paramètre pour écrire le résultat de la sérialisation

XMLEncoder(OutputStream out, String charset, boolean declaration, int indentation)

Créer une nouvelle instance qui utilise le flux en paramètre pour écrire le résultat de la sérialisation en précisant le charset, un booléen qui précise si le document XML doit contenir la déclaration et la taille de l'indentation.


Le constructeur qui attend plusieurs paramètres a été ajouté dans Java 7

Exemple ( code Java 1.4 ) :
package fr.jmdoudoux.dej.serialisation;

import java.beans.XMLEncoder;
import java.io.FileOutputStream;
import java.util.Date;

public class SerializerPersonneXML {

  public static void main(final String argv[]) {
    final Personne personne = new Personne("Dupond", "Jean", 175, "1234", new Date());
    XMLEncoder encoder = null;

    try {
      encoder = new XMLEncoder(new BufferedOutputStream(
        new FileOutputStream("personne.xml")));
      encoder.writeObject(personne);
      encoder.flush();
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } finally {
      if (encoder != null) {
        encoder.close();
      }
    }
  }
}

Résultat :
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.6.0_45" class="java.beans.XMLDecoder"> 
 <object class="fr.jmdoudoux.dej.serialisation.Personne"> 
  <void property="dateNaiss"> 
   <object class="java.util.Date"> 
    <long>1391419419836</long> 
   </object> 
  </void> 
  <void property="nom"> 
   <string>Dupond</string> 
  </void> 
  <void property="prenom"> 
   <string>Jean</string> 
  </void> 
  <void property="taille"> 
   <int>175</int> 
  </void> 
 </object> 
</java>

La structure du document XML généré utilise plusieurs conventions :

La classe XMLEncoder clone le graphe d'objets à sérialiser pour enregistrer les opérations nécessaires et ainsi pouvoir créer le document XML Lors de la sérialisation, la classe XMLEncoder applique un algorithme qui permet d'éviter de sérialiser les propriétés qui ont leur valeur par défaut : ceci permet de rendre le document XML plus compact.

Exemple ( code Java 1.4 ) :
package fr.jmdoudoux.dej.serialisation;

import java.beans.XMLEncoder;
import java.io.FileOutputStream;

public class SerializerPersonneXML {

  public static void main(final String argv[]) {
    final Personne personne = new Personne();

    XMLEncoder encoder = null;

    try {
      encoder = new XMLEncoder(new BufferedOutputStream(new FileOutputStream("personne.xml")));
      encoder.writeObject(personne);
      encoder.flush();
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } finally {
      if (encoder != null) {
        encoder.close();
      }
    }
  }
}

Résultat :
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.6.0_45" class="java.beans.XMLDecoder"> 
 <object class="fr.jmdoudoux.dej.serialisation.Personne"/> 
</java>

 

17.4.2. La classe XMLDecoder

La classe java.beans.XMLDecoder permet de désérialiser un objet à partir d'un document XML généré avec la classe XMLEncoder.

Elle possède plusieurs constructeurs :

Constructeur

Rôle

XMLDecoder(InputStream in)

Créer un nouveau décodeur pour désérialiser le document XML lu du flux en paramètre

XMLDecoder(InputStream in, Object owner)

Créer un nouveau décodeur pour désérialiser le document XML lu du flux en paramètre

XMLDecoder(InputStream in, Object owner, ExceptionListener exceptionListener)

Créer un nouveau décodeur pour désérialiser le document XML lu du flux en paramètre

XMLDecoder(InputStream in, Object owner, ExceptionListener exceptionListener, ClassLoader cl)

Créer un nouveau décodeur pour désérialiser le document XML lu du flux en paramètre (depuis Java 1.5)

XMLDecoder(InputSource is)

Créer un nouveau décodeur pour désérialiser le document XML lu du flux en paramètre (depuis Java 7)


Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.beans.XMLDecoder;
import java.io.BufferedInputStream;
import java.io.FileInputStream;

public class DeserializerPersonneXML {

  public static void main(final String argv[]) {
    XMLDecoder decoder = null;

    try {
      decoder = new XMLDecoder(new BufferedInputStream(new FileInputStream("personne.xml")));
      final Personne personne = (Personne) decoder.readObject();
      System.out.println(personne);
    } catch (final Exception e) {
      e.printStackTrace();
    } finally {
      if (decoder != null) {
        decoder.close();
      }
    }
  }
}

Résultat :
Personne
[nom=Dupond, prenom=Jean, taille=175, dateNaiss=Wed Feb 12 20:45:45 CET 2013]

L'utilisation de la sérialisation XML n'est possible par défaut que sur des objets dont la classe respecte la convention Javabeans.

Notamment une exception est levée si la classe ne possède pas de constructeur par défaut.

Exemple :
java.lang.InstantiationException: fr.jmdoudoux.dej.serialisation.Personne
Continuing ...
java.lang.Exception: XMLEncoder: discarding statement XMLEncoder.writeObject(Personne);
Continuing ...

Dans ce cas, le fichier XML est créé mais il contient uniquement le prologue et le tag racine :

Exemple :
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.6.0_45" class="java.beans.XMLDecoder"> 
</java>

Seuls les champs qui possèdent un getter et un setter sont pris en compte lors des opérations de sérialisation et désérialisation.

 

17.4.3. La personnalisation de la sérialisation

La classe XMLEncoder utilise une instance de type PersistenceDelegate pour sérialiser un objet en XML. Si aucune instance de type PersistenceDelegate n'est explicitement fournie, alors la classe XMLEncoder utilise une instance de type DefaultPersistenceDelegate qui implémente la stratégie de sérialisation par défaut. 

 

17.4.3.1. La classe PersistenceDelegate

La classe abstraite java.beans.PersistenceDelegate a pour rôle d'exprimer l'état d'une instance au travers de l'utilisation de ses méthodes publiques. Au lieu de réaliser ses actions dans la classe de l'instance à sérialiser, comme c'est fait pour la sérialisation binaire, la classe XMLEncoder délègue ces traitements à une instance de type PersitanceDelegate.

La classe PersistenceDelegate permet de contrôler certaines étapes de la sérialisation :

Elle possède plusieurs méthodes :

Méthode

Rôle

protected void initialize(Class<?> type, Object oldInstance, Object newInstance, Encoder out)

Initialiser la nouvelle instance éventuellement à partir des données de la première instance fournie en paramètre

protected abstract Expression instantiate(Object oldInstance, Encoder out)

Renvoyer une instance de type Expression correspondant à l'objet fourni en paramètre

protected boolean mutatesTo(Object oldInstance, Object newInstance)

Renvoyer un booléen qui précise s'il est possible de créer une copie de la première instance et appliquer des opérations pour obtenir la seconde instance

void writeObject(Object oldInstance, Encoder out)

Réaliser les opérations déléguées relatives à la sérialisation de l'instance fournie en paramètre

 

17.4.3.2. La classe DefaultPersistenceDelegate

La classe java.beans.DefaultPersistenceDelegate hérite de la classe java.beans.PersistenceDelegate pour en proposer une implémentation concrète.

Cette classe est utilisée par défaut pour des classes qui doivent respecter la convention Javabeans notamment la présence d'un constructeur par défaut et la présence de getter/setter pour les propriétés.

Elle possède deux constructeurs :

Constructeur

Rôle

DefaultPersistenceDelegate()

 

DefaultPersistenceDelegate(String[] constructorPropertyNames)

Créer une instance qui par défaut va invoquer le constructeur avec les valeurs des propriétés de l'objet à traiter en paramètre


Elle redéfinit plusieurs méthodes :

Méthode

Rôle

protected void initialize(Class<?> type, Object oldInstance, Object newInstance, Encoder out)

Initialiser les valeurs des champs de la nouvelle instance avec celles de l'instance fournie en paramètres en utilisant leurs getter/setter grâce à l'introspection

protected Expression instantiate(Object oldInstance, Encoder out)

Envoyer une instance de type Expression dont le nom de méthode est "new" pour préciser que c'est le constructeur qui doit être invoqué

protected boolean mutatesTo(Object oldInstance, Object newInstance)

Si au moins un constructeur est défini et la méthode equals() redéfinie alors renvoie la valeur de l'invocation de oldInstance.equals(newInstance)

 

17.4.3.3. Empêcher la sérialisation d'un attribut

Pour empêcher la sérialisation en XML d'un champ, il est inutile de lui ajouter le modificateur transient car il n'est pas pris en compte dans ce cas.

Il faut soit :

Il faut utiliser l'API Introspection pour obtenir l'instance de type BeanInfo de la classe du bean. La méthode getPropertyDescriptors() permet d'obtenir un tableau de type PropertyDescriptor, une occurrence pour chaque champ. Il faut itérer sur ce tableau pour trouver l'occurrence du champ concerné. Enfin, il faut invoquer sa méthode setValue() en lui passant en paramètre « transient » et Boolean.TRUE.

Exemple ( code Java 5.0 ) :
package fr.jmdoudoux.dej.serialisation;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.beans.XMLEncoder;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;

public class SerializerMonBean {

  public static void main(final String[] args) {
    final MonBean monBean = new MonBean();
    monBean.setChamp1("valeur1");
    monBean.setChamp2("valeur2");
    XMLEncoder encoder = null;

    try {
      final BeanInfo info = Introspector.getBeanInfo(MonBean.class);
      final PropertyDescriptor[] propertyDescriptors = info.getPropertyDescriptors();
      for (final PropertyDescriptor descriptor : propertyDescriptors) {
        if (descriptor.getName().equals("champ2")) {
          descriptor.setValue("transient", Boolean.TRUE);
          break;
        }
      }

      encoder = new XMLEncoder(new BufferedOutputStream(new FileOutputStream("monbean.xml")));
      encoder.writeObject(monBean);
      encoder.flush();
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } catch (final IntrospectionException e) {
      e.printStackTrace();
    } finally {
      if (encoder != null) {
        encoder.close();
      }
    }
  }
}

Résultat :
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.6.0_43" class="java.beans.XMLDecoder"> 
 <object class="fr.jmdoudoux.dej.serialisation.MonBean"> 
  <void property="champ1"> 
   <string>valeur1</string> 
  </void> 
 </object> 
</java>

Plutôt que de modifier le BeanInfo par défaut associé à la classe, il est possible de définir explicitement cette classe.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.beans.SimpleBeanInfo;

public class MonBeanBeanInfo extends SimpleBeanInfo {

  private PropertyDescriptor[] descriptors;

  public MonBeanBeanInfo() {
    try {
      final PropertyDescriptor champ1Descriptor = new PropertyDescriptor("champ1", 
        MonBean.class);
      final PropertyDescriptor champ2Descriptor = new PropertyDescriptor("champ2", 
        MonBean.class);
      champ2Descriptor.setValue("transient", Boolean.TRUE);

      this.descriptors = new PropertyDescriptor[] { champ1Descriptor, champ2Descriptor };
    } catch (final IntrospectionException ex) {
      ex.printStackTrace();
    }
  }

  @Override
  public PropertyDescriptor[] getPropertyDescriptors() {
    return this.descriptors;
  }
}

Les traitements pour sérialiser une instance sont alors plus simple.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.beans.XMLEncoder;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;

public class SerializerMonBean {

  public static void main(final String[] args) {
    final MonBean monBean = new MonBean();
    monBean.setChamp1("valeur1");
    monBean.setChamp2("valeur2");
    XMLEncoder encoder = null;

    try {
      encoder = new XMLEncoder(new BufferedOutputStream(new FileOutputStream("monbean.xml")));
      encoder.writeObject(monBean);
      encoder.flush();
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } finally {
      if (encoder != null) {
        encoder.close();
      }
    }
  }
}

 

17.4.3.4. Sérialiser des attributs sans accesseur/modificateur standard

Parfois, un champ possède un accesseur et un modificateur mais leurs noms ne respectent pas la convention JavaBean.

Exemple :
package fr.jmdoudoux.dej.serialisation;

public class MonBean {

  private String champ1;
  private String champ2;

  public String getChamp1() {
    return this.champ1;
  }

  public String obtenirChamp2() {
    return this.champ2;
  }

  public void modifierChamp2(final String champ2) {
    this.champ2 = champ2;
  }

  public void setChamp1(final String champ1) {
    this.champ1 = champ1;
  }

  @Override
  public String toString() {
    return "MonBean [champ1=" + this.champ1 + ", champ2=" + this.champ2 + "]";
  }
}

Lors de la sérialisation en XML d'une instance de cette classe, le champ2 est ignoré puisse qu'il ne possède pas de getter/setter standard.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.beans.XMLEncoder;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;

public class SerializerMonBean {

  public static void main(final String[] args) {
    final MonBean monBean = new MonBean();
    monBean.setChamp1("valeur1");
    monBean.modifierChamp2("valeur2");
    XMLEncoder encoder = null;

    try {
      encoder = new XMLEncoder(new BufferedOutputStream(new FileOutputStream("monbean.xml")));
      encoder.writeObject(monBean);
      encoder.flush();
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } finally {
      if (encoder != null) {
        encoder.close();
      }
    }
  }
}

Résultat :
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.6.0_43" class="java.beans.XMLDecoder"> 
 <object class="fr.jmdoudoux.dej.serialisation.MonBean"> 
  <void property="champ1"> 
   <string>valeur1</string> 
  </void> 
 </object> 
</java> 

Pour forcer l'API à utiliser l'accesseur et le modificateur non standard, il faut les préciser dans le PropertyDescriptor du champ correspondant encapsulé dans la classe de type BeanInfo liée à la classe du bean.

La classe doit implémenter l'interface BeanInfo ou hériter de la classe SimpleBeanInfo. Son nom doit obligatoirement être composé du nom de la classe suivi de BeanInfo

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.beans.SimpleBeanInfo;

public class MonBeanBeanInfo extends SimpleBeanInfo {

  private PropertyDescriptor[] descriptors;

  public MonBeanBeanInfo() {
    try {

      final PropertyDescriptor champ1Descriptor = new PropertyDescriptor("champ1", 
        MonBean.class);
      final PropertyDescriptor champ2Descriptor = new PropertyDescriptor("champ2", 
        MonBean.class, "obtenirChamp2", "modifierChamp2");

      this.descriptors = new PropertyDescriptor[] { champ1Descriptor, champ2Descriptor };
    } catch (final IntrospectionException ex) {
      ex.printStackTrace();
    }
  }

  @Override
  public PropertyDescriptor[] getPropertyDescriptors() {
    return this.descriptors;
  }
}

Les traitements pour sérialiser en XML une instance de la classe reste classique.

Exemple :
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.6.0_43" class="java.beans.XMLDecoder"> 
 <object class="fr.jmdoudoux.dej.serialisation.MonBean"> 
  <void property="champ1"> 
   <string>valeur1</string> 
  </void> 
  <void method="modifierChamp2"> 
   <string>valeur2</string> 
  </void> 
 </object> 
</java> 

Dans le fichier XML contenant le résultat de la sérialisation, le tag qui correspond au champ champ2 contient une propriété method dont la valeur est le nom de la méthode définie comme étant le modificateur du champ. C'est cette méthode qui sera invoquée lors de la désérialisation pour initialiser la valeur du champ.

 

17.4.3.5. Sérialiser une classe sans constructeur par défaut

Par défaut la sérialisation XML requiert un constructeur par défaut dans la classe de l'instance à traiter. La convention JavaBean impose elle-même un tel constructeur. Cependant toutes les classes n'en sont pas forcement pourvues soit parce qu'elles définissent explicitement d'autres constructeurs soit parce que l'obtention d'une nouvelle instance passe par une fabrique.

Exemple :
package fr.jmdoudoux.dej.serialisation;

public class MonBean {

  private String champ1;
  private String champ2;

  private MonBean() {
  }

  public static MonBean creerInstance() {
    return new MonBean();
  }

  public String getChamp1() {
    return this.champ1;
  }

  public String getChamp2() {
    return this.champ2;
  }

  public void setChamp2(final String champ2) {
    this.champ2 = champ2;
  }

  public void setChamp1(final String champ1) {
    this.champ1 = champ1;
  }

  @Override
  public String toString() {
    return "MonBean [champ1=" + this.champ1 + ", champ2=" + this.champ2 + "]";
  }
}

Dans ce cas, la sérialisation XML par défaut échoue car elle ne trouve pas le constructeur par défaut en utilisant l'introspection.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.beans.XMLEncoder;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;

public class SerializerMonBean {

  public static void main(final String[] args) {
    final MonBean monBean = MonBean.creerInstance();
    monBean.setChamp1("valeur1");
    monBean.setChamp2("valeur2");
    XMLEncoder encoder = null;

    try {
      encoder = new XMLEncoder(new BufferedOutputStream(new FileOutputStream("monbean.xml")));
      encoder.writeObject(monBean);
      encoder.flush();
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } finally {
      if (encoder != null) {
        encoder.close();
      }
    }
  }
}

Résultat :
java.lang.IllegalAccessException: Class sun.reflect.misc.Trampoline can not access a member 
of class fr.jmdoudoux.dej.serialisation.MonBean with modifiers "private"
Continuing ...
java.lang.Exception: XMLEncoder: discarding statement XMLEncoder.writeObject(MonBean);
Continuing ...

Pour indiquer qu'il faut utiliser autre chose que le constructeur par défaut pour obtenir une instance, il faut définir un PersistenceDelegate personnalisé.

Il faut redéfinir la méthode instanciate() pour qu'elle renvoie une instance de type java.beans.Expression. à laquelle on a précisé le nom de la méthode à invoquer pour obtenir une nouvelle instance.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.beans.DefaultPersistenceDelegate;
import java.beans.Encoder;
import java.beans.Expression;
import java.beans.XMLEncoder;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;

public class SerializerMonBean {

  public static void main(final String[] args) {
    final MonBean monBean = MonBean.creerInstance();
    monBean.setChamp1("valeur1");
    monBean.setChamp2("valeur2");
    XMLEncoder encoder = null;

    try {
      encoder = new XMLEncoder(new BufferedOutputStream(new FileOutputStream("monbean.xml")));
      encoder.setPersistenceDelegate(MonBean.class, new DefaultPersistenceDelegate() {
        @Override
        protected Expression instantiate(final Object oldInstance, final Encoder out) {
          return new Expression(oldInstance, MonBean.class, "creerInstance", null);
        }
      });

      encoder.writeObject(monBean);
      encoder.flush();
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } finally {
      if (encoder != null) {
        encoder.close();
      }
    }
  }
}

Exemple :
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.6.0_43" class="java.beans.XMLDecoder"> 
 <object class="fr.jmdoudoux.dej.serialisation.MonBean" method="creerInstance"> 
  <void property="champ1"> 
   <string>valeur1</string> 
  </void> 
  <void property="champ2"> 
   <string>valeur2</string> 
  </void> 
 </object> 
</java> 

Dans le fichier XML contenant le résultat de la sérialisation, le nom de la méthode à invoquer est fourni comme valeur à l'attribut method.

Il est fréquent que la fabrique ou le constructeur à invoquer requière des paramètres.

Exemple :
package fr.jmdoudoux.dej.serialisation;

public class MonBean {

  private String champ1;
  private String champ2;

  private MonBean() {
  }

  public static MonBean creerInstance(final String champ1, final String champ2) {
    final MonBean resultat = new MonBean();
    resultat.setChamp1(champ1);
    resultat.setChamp2(champ2);
    return resultat;
  }

  public String getChamp1() {
    return this.champ1;
  }

  public String getChamp2() {
    return this.champ2;
  }

  public void setChamp2(final String champ2) {
    this.champ2 = champ2;
  }

  public void setChamp1(final String champ1) {
    this.champ1 = champ1;
  }

  @Override
  public String toString() {
    return "MonBean [champ1=" + this.champ1 + ", champ2=" + this.champ2 + "]";
  }
}

Dans cas, il faut extraire les valeurs à passer en paramètre et les fournir sous la forme d'un tableau d'objets en paramètre du constructeur de la classe Expression.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.beans.DefaultPersistenceDelegate;
import java.beans.Encoder;
import java.beans.Expression;
import java.beans.XMLEncoder;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;

public class SerializerMonBean {

  public static void main(final String[] args) {
    final MonBean monBean = MonBean.creerInstance("valeur1", "valeur2");
    XMLEncoder encoder = null;

    try {
      encoder = new XMLEncoder(new BufferedOutputStream(
        new FileOutputStream("monbean.xml")));
      encoder.setPersistenceDelegate(MonBean.class, new DefaultPersistenceDelegate() {
        @Override
        protected Expression instantiate(final Object oldInstance, final Encoder out) {
          final String valeur1 = ((MonBean) oldInstance).getChamp1();
          final String valeur2 = ((MonBean) oldInstance).getChamp2();
          return new Expression(oldInstance, MonBean.class, "creerInstance", 
            new Object[] { valeur1, valeur2 });
        }

      });

      encoder.writeObject(monBean);
      encoder.flush();
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } finally {
      if (encoder != null) {
        encoder.close();
      }
    }
  }
}

Exemple :
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.6.0_43" class="java.beans.XMLDecoder"> 
 <object class="fr.jmdoudoux.dej.serialisation.MonBean" method="creerInstance"> 
  <string>valeur1</string> 
  <string>valeur2</string> 
 </object> 
</java> 

 

17.4.3.6. Les arguments du constructeur sont des champs de la classe

Parfois, la classe à sérialiser ne possède que des constructeurs qui attendent des paramètres.

Exemple :
package fr.jmdoudoux.dej.serialisation;

public class MonBean {

  private String champ1;
  private String champ2;

  public MonBean(final String champ1, final String champ2) {
    this.champ1 = champ1;
    this.champ2 = champ2;
  }

  public String getChamp1() {
    return this.champ1;
  }

  public String getChamp2() {
    return this.champ2;
  }

  public void setChamp2(final String champ2) {
    this.champ2 = champ2;
  }

  public void setChamp1(final String champ1) {
    this.champ1 = champ1;
  }

  @Override
  public String toString() {
    return "MonBean [champ1=" + this.champ1 + ", champ2=" + this.champ2 + "]";
  }
}

Pour sérialiser en XML une instance de cette classe, il est nécessaire de fournir une nouvelle instance de DefaultPersistenceDelegate en lui passant en paramètres un tableau de chaînes de caractères qui va contenir le nom de chacun des champs à fournir en paramètre du constructeur.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.beans.DefaultPersistenceDelegate;
import java.beans.XMLEncoder;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;

public class SerializerMonBean {

  public static void main(final String[] args) {
    final MonBean monBean = new MonBean("valeur1", "valeur2");
    XMLEncoder encoder = null;

    try {
      encoder = new XMLEncoder(new BufferedOutputStream(
        new FileOutputStream("monbean.xml")));
      encoder.setPersistenceDelegate(MonBean.class, 
        new DefaultPersistenceDelegate(new String[] { "champ1", "champ2" }));

      encoder.writeObject(monBean);
      encoder.flush();
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } finally {
      if (encoder != null) {
        encoder.close();
      }
    }
  }
}

Exemple :
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.6.0_43" class="java.beans.XMLDecoder"> 
 <object class="fr.jmdoudoux.dej.serialisation.MonBean"> 
  <string>valeur1</string> 
  <string>valeur2</string> 
 </object> 
</java> 

 

17.4.3.7. Les arguments du constructeur ne sont pas des champs de la classe

Dans le cas où les arguments à passer au constructeur ne sont pas des attributs de la classe, il faut définir un objet de type PersistenceDelegate dans lequel la méthode instantiate() est redéfinie.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.beans.Encoder;
import java.beans.Expression;
import java.beans.PersistenceDelegate;
import java.beans.XMLEncoder;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;

public class SerializerMonBean {

  public static void main(final String[] args) {
    final MonBean monBean = new MonBean("valeur1", "valeur2");
    XMLEncoder encoder = null;

    try {
      encoder = new XMLEncoder(new BufferedOutputStream(
        new FileOutputStream("monbean.xml")));
      encoder.setPersistenceDelegate(MonBean.class, new PersistenceDelegate() {
        @Override
        protected Expression instantiate(final Object oldInstance, final Encoder out) {
          return new Expression(oldInstance, oldInstance.getClass(), "new", 
            new Object[] { "valeur3", "valeur4" });
        }
      });

      encoder.writeObject(monBean);
      encoder.flush();
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } finally {
      if (encoder != null) {
        encoder.close();
      }
    }
  }
}

Il faut préciser la chaîne de caractères « new » comme nom de méthode pour indiquer que c'est le constructeur qui doit être invoqué.

Résultat :
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.6.0_43" class="java.beans.XMLDecoder"> 
 <object class="fr.jmdoudoux.dej.serialisation.MonBean"> 
  <string>valeur3</string> 
  <string>valeur4</string> 
 </object> 
</java>

 

17.4.3.8. La gestion des exceptions

Par défaut, les classes XMLEncoder et XMLDecoder interceptent les exceptions levées durant leurs traitements.

Il peut cependant être nécessaire d'être informé de la levée d'une exception pour permettre sa gestion.

La méthode setExceptionListener() de la classe XMLEncoder permet d'enregistrer un listener pour la gestion des exceptions.

Le listener est de type ExceptionListener : il ne déclare qu'une seule méthode exceptionThrown() qui prend en paramètre l'exception levée.

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.beans.ExceptionListener;
import java.beans.XMLEncoder;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.util.Date;

public class SerializerPersonneXML {

  public static void main(final String argv[]) {
    final Personne personne = new Personne("Dupond", "Jean", 175, "1234", new Date());
    XMLEncoder encoder = null;

    try {
      encoder = new XMLEncoder(new BufferedOutputStream(new FileOutputStream("personne.xml")));
      encoder.setExceptionListener(new ExceptionListener() {
        @Override
        public void exceptionThrown(final Exception ex) {
          ex.printStackTrace();
        }
      });

      encoder.writeObject(personne);
      encoder.flush();
    } catch (final java.io.IOException e) {
      e.printStackTrace();
    } finally {
      if (encoder != null) {
        encoder.close();
      }
    }
  }
}

Résultat :
java.lang.InstantiationException: fr.jmdoudoux.dej.serialisation.Personne
    at java.lang.Class.newInstance0(Class.java:340)
    at java.lang.Class.newInstance(Class.java:308)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at sun.reflect.misc.Trampoline.invoke(MethodUtil.java:37)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at sun.reflect.misc.MethodUtil.invoke(MethodUtil.java:244)
    at java.beans.Statement.invokeInternal(Statement.java:239)
    at java.beans.Statement.access$000(Statement.java:39)
    at java.beans.Statement$2.run(Statement.java:140)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.beans.Statement.invoke(Statement.java:137)
    at java.beans.Expression.getValue(Expression.java:98)
    at java.beans.Encoder.getValue(Encoder.java:85)
    at java.beans.Encoder.get(Encoder.java:200)
    at java.beans.PersistenceDelegate.writeObject(PersistenceDelegate.java:94)
    at java.beans.Encoder.writeObject(Encoder.java:54)
    at java.beans.XMLEncoder.writeObject(XMLEncoder.java:257)
    at java.beans.Encoder.writeExpression(Encoder.java:279)
    at java.beans.XMLEncoder.writeExpression(XMLEncoder.java:372)
    at java.beans.PersistenceDelegate.writeObject(PersistenceDelegate.java:97)
    at java.beans.Encoder.writeObject(Encoder.java:54)
    at java.beans.XMLEncoder.writeObject(XMLEncoder.java:257)
    at java.beans.Encoder.writeObject1(Encoder.java:206)
    at java.beans.Encoder.cloneStatement(Encoder.java:219)
    at java.beans.Encoder.writeStatement(Encoder.java:250)
    at java.beans.XMLEncoder.writeStatement(XMLEncoder.java:331)
    at java.beans.XMLEncoder.writeObject(XMLEncoder.java:260)
    at fr.jmdoudoux.dej.serialisation.SerializerPersonneXML.main 
(SerializerPersonneXML.java:24)
java.lang.Exception: XMLEncoder: discarding statement XMLEncoder.writeObject(Personne);
    at java.beans.XMLEncoder.writeStatement(XMLEncoder.java:344)
    at java.beans.XMLEncoder.writeObject(XMLEncoder.java:260)
    at fr.jmdoudoux.dej.serialisation.SerializerPersonneXML.main
(SerializerPersonneXML.java:24)
Caused by: java.lang.RuntimeException: failed to evaluate: <unbound>=Class.new();
    at java.beans.Encoder.getValue(Encoder.java:89)
    at java.beans.Encoder.get(Encoder.java:200)
    at java.beans.PersistenceDelegate.writeObject(PersistenceDelegate.java:94)
    at java.beans.Encoder.writeObject(Encoder.java:54)
    at java.beans.XMLEncoder.writeObject(XMLEncoder.java:257)
    at java.beans.Encoder.writeExpression(Encoder.java:279)
    at java.beans.XMLEncoder.writeExpression(XMLEncoder.java:372)
    at java.beans.PersistenceDelegate.writeObject(PersistenceDelegate.java:97)
    at java.beans.Encoder.writeObject(Encoder.java:54)
    at java.beans.XMLEncoder.writeObject(XMLEncoder.java:257)
    at java.beans.Encoder.writeObject1(Encoder.java:206)
    at java.beans.Encoder.cloneStatement(Encoder.java:219)
    at java.beans.Encoder.writeStatement(Encoder.java:250)
    at java.beans.XMLEncoder.writeStatement(XMLEncoder.java:331)
        ... 2 more

Pour associer un gestionnaire d'exceptions à une instance de type XMLDecoder, il faut soit :

Exemple :
package fr.jmdoudoux.dej.serialisation;

import java.beans.ExceptionListener;
import java.beans.XMLDecoder;
import java.io.BufferedInputStream;
import java.io.FileInputStream;

public class DeserializerPersonneXML {

  public static void main(final String argv[]) {
    XMLDecoder decoder = null;

    try {
      decoder = new XMLDecoder(new BufferedInputStream(new FileInputStream("personne.xml")));
      decoder.setExceptionListener(new ExceptionListener() {
        @Override
        public void exceptionThrown(final Exception ex) {
          ex.printStackTrace();
        }
      });
      final Personne personne = (Personne) decoder.readObject();
      System.out.println(personne);
    } catch (final Exception e) {
      e.printStackTrace();
    } finally {
      if (decoder != null) {
        decoder.close();
      }
    }
  }
}

 


16. NIO 2 18. L'interaction avec le réseau Imprimer Index Index avec sommaire Télécharger le PDF    
Développons en Java
v 2.40   Copyright (C) 1999-2023 .