Serializable
à une instance de
ObjectOutputStream
pour sérialiser un objet. Si cet objet ne comporte pas de champ trop exotique, comme des connexions à des bases de données, des fichiers ou des threads, cette sérialization se déroulera sans problème. L'interface
Serializable
n'expose aucune méthode, implémenter cette interface consiste donc juste à déclarer cette implémentation.
Des problèmes peuvent se poser pour les objets qui possèdent des champs eux-mêmes non sérializables. C'est le cas pour les trois champs que nous venons de donner en exemple. Dans ce cas, ces champs doivent être marqués avec le mot-clé
transient
. Cela a pour effet de les retirer du flux sérializé. Après désérialization, ces champs seront à
null
.
Exemple 125. Champ
transient
// création d'une classe Serializable public class Marin implements Serializable { // la classe String est Serializable, donc ces champs sont légaux private String nom, prenom ; // en revanche la classe Connection ne l'est pas, // il faut donc retirer ce champ de la serialization private transient Connection con ; }
Serializable
, stocké dans un champ standard. Ce champ standard s'appelle
serialVersionUID
, doit être de type
long
et doit être
private static final
.
Ce champ est systématiquement enregistré dans tout paquet d'octets qui représente un objet sérializé. S'il a été défini explicitement dans la classe de cet objet, c'est cette valeur fournie qui est utilisée. S'il ne l'a pas été, alors la machine Java en détermine un, en fonction des éléments publics et non statiques de la classe.
Toute modification de la classe entraîne une modification de cette valeur par défaut. Lorsque l'on a une classe qui implémente
Serializable
, il peut donc être important d'imposer une valeur pour ce champ, de façon à ne pas perturber la désérialization des objets. On pourra remarquer qu'Eclipse propose de générer ce champ automatiquement, en utilisant le même algorithme que celui utilisé en interne par la JVM.
writeObject(Object)
de la classe
ObjectOutputStream
.
Prenons par exemple la classe
Marin
suivante.
Exemple 126. Classe
Marin
sérializable
public class Marin implements Serializable { private static final long serialVersionUID = 1350092881346723535L; private String nom, prenom ; private int salaire ; public Marin(String nom, String prenom) { this.nom = nom ; this.prenom = prenom ; } public String toString() { StringBuffer sb = new StringBuffer() ; return sb.append(nom).append(" ").append(prenom).toString() ; } }
serialVersionUID
de cet exemple a été générée avec Eclipse. En fait, dès qu'une classe implémente
Serializable
, Eclipse émet une alerte si l'on ne donne pas de valeur explicite à ce nombre.
Le code simplifié de la sérialization est le suivant.
Exemple 127. Sérialization d'un objet
// dans une méthode main // on simplifie le code en retirant la gestion des exceptions File fichier = new File("tmp/marin.ser") ; // ouverture d'un flux sur un fichier ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fichier)) ; // création d'un objet à sérializer Marin m = new Marin("Surcouf", "Robert") ; // sérialization de l'objet oos.writeObject(m) ; // fermeture du flux dans le bloc finally
tmp/marin.ser
, on peut lire dedans les informations suivantes en clair :
Exemple 128. Désérialization d'un objet
// dans une méthode main // on simplifie le code en retirant la gestion des exceptions File fichier = new File("tmp/marin.ser") ; // ouverture d'un flux sur un fichier ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fichier)) ; // désérialization de l'objet Marin m = (Marin)ois.readObject() ; System.out.println(m) ; // fermeture du flux dans le bloc finally
serialVersionUID
a changé, alors l'exception
java.io.InvalidClassException
sera jetée.
Dans certains cas applicatifs, le mécanisme standard de sérialization proposé par Java peut se révéler inadapté, ou tout simplement ne pas convenir. Il est possible de le surcharger de trois manières.
transient
. Cela suppose qu'ils implémentent eux-mêmes
Serializable
, sans quoi des exceptions seront jetées.
La machine Java utilise un mécanisme particulier pour éviter de sérializer plusieurs fois un même objet en relation de plusieurs autres objets. Chaque objet est associé à un numéro de série (d'où l'appellation de "sérialization"), qui est placé dans la relation, et n'est sérializé qu'une seule fois. Lors de la déserialization, chaque objet est désérializé une unique fois, et vient remplacer ce numéro de série.
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {}
: cette méthode est appelée pour reconstituer l'objet à partir d'un flux sérializé. Elle doit avoir exactement cette signature, et être privée.
private void writeObject(ObjectOutptStream oos) throws IOException {}
: cette méthode est appelée pour écrire l'objet sur un flux sérializé. Elle doit avoir exactement cette signature, et être privée.
Serializable
comporte ces deux méthodes, alors elle les appelle plutôt que d'utiliser ses mécanismes internes de sérialization.
La méthode
writeObject()
a la responsabilité d'écrire les champs de l'objet sur le flux sérializé passé en paramètre. On peut choisir de n'écrire qu'une partie des champs, cela ne pose pas de problème.
La méthode
readObject()
a la responsabilité de restaurer les valeurs des champs de l'objet. Son processus de lecture doit correspondre au processus d'écriture utilisé par la méthode
writeObject()
. Si une partie des champs n'a pas été écrite par la méthode d'écriture, alors la méthode
readObject()
peut restaurer ces valeurs à partir d'informations externes.
Il est important de noter que ces deux méthodes fonctionnent en tandem, et doivent donc être compatibles l'une avec l'autre.
Exemple 129. Utilisation de
readObject()
et
writeObject()
public class Marin implements Serializable { private String nom, prenom ; private int salaire ; // suivent les getters / setters // méthode readObject, utilisée pour reconstituer un objet sérializé private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { // l'ordre de lecture doit être le même que l'ordre d'écriture d'un objet this.nom = ois.readUTF() ; this.prenom = ois.readUTF() ; // le salaire n'est pas relu, vu qu'il n'a pas été écrit } // méthode writeObject, utilisée lors de la sérialization private void writeObject(ObjectOutputStream oos) throws IOException { // écriture de toute ou partie des champs d'un objet oos.writeUTF(nom) ; oos.writeUTF(prenom) ; // on choisit de ne pas écrire le salaire, qui ne fait // pas partie de l'état d'une instance de marin } }
Externalizable
plutôt que
Serializable
, et doit obligatoirement posséder un constructeur vide, explicite ou par défaut.
L'interface
Externalizable
impose deux méthodes.
public void writeExternal(ObjectOutput out) throws IOException {}
est appelée pour écrire l'objet sur le flux sérializé passé en paramètre.
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {}
est appelée pour relire cet objet.
readObject()
et
writeObject()
.
Dans la pratique, le flux sérializé généré est assez différent, et souvent beaucoup plus court (moins d'octets) qu'un flux sérializé standard. Il faut noter qu'il est en plus beaucoup plus rapide à écrire, car la machine Java n'a pas besoin d'explorer la classe à sérializer par introspection, comme c'est le cas dans le mécanisme standard.
On prendra garde toutefois au fait que cette technique est moins sécurisée que la sérialization standard. Effectivement, la méthode
readExternal()
est ici publique, alors que
readObject()
est privée. N'importe quel code peut donc l'appeler, sans aucun contrôle. Or, cette méthode permet de modifier l'état interne de l'objet, ce qui peut être indésirable, notamment si l'on souhaite que la classe soit
immutable
.
writeReplace()
qui retourne cet objet. C'est cet objet que l'on appelle
objet proxy
.
Cet objet doit exposer une méthode
readResolve()
, qui a la charge de reconstruire l'objet originel.
Construisons une classe
MarinProxy
, qui enregistre les champs
nom
et
prenom
dans une chaîne de caractère unique.
Exemple 130. Sérialization par objet proxy : classe proxy
// classe sérializée à la place de Marin // elle doit être sérializable public class MarinProxy implements Serializable { // un seul champ à sérializer private String data ; // constructeur utilitaire pour construire un proxy // sur un marin public MarinProxy(Marin marin) { // l'unique champ concatène le nom et le prénom d'un marin this.data = marin.getNom() + ":" + marin.getPrenom() ; } // méthode appelée pour la désérialization // lorsque cette méthode est appelée, le champ data a été désérializé private Object readResolve() throws ObjectStreamException { StringTokenizer st = new StringTokenizer(this.data, ":") ; String nom = st.nextToken() ; String prenom = st.nextToken() ; Marin m = new Marin(nom, prenom) ; return m ; } }
Marin
en reconstituant son
nom
et son
prenom
en interne. Elle aurait pu aussi passer la chaîne de caractères
data
directement à un constructeur particulier de
Marin
.
Voyons maintenant la classe
Marin
.
Exemple 131. Sérialization par objet proxy : classe à sérializer
public class Marin implements Serializable { private String nom, prenom ; private int salaire ; public Marin(String nom, String prenom) { this.nom = nom ; this.prenom = prenom ; } // suivent les getters et setters // méthode standard appelée par la sérialization private Object writeReplace() throws ObjectStreamException { // cette méthode retourne l'objet proxy, qui va être // sérializé à la place de ce marin return new MarinProxy(this) ; } }
String
StringBuffer
et
StringBuilder