Suite à mon article sur la sérialisation, je souhaite préciser quelques détails sur la sérialisation et parler du champ « serialVersionUID ». Qu’est-ce que ce champ ? A quoi sert-il ? Faut-il le mettre ou non dans une class Serializable ?
Quelques rappels:
Toutes les classes dérivées d’une class sérialisable sont aussi sérialisables. L’interface java.io.Serializable ne sert qu’à marquer la classe comme étant sérialisable comme on l’a vu. Cependant pour permettre à des types dérivés d’être sérialisable alors que leur super classe n’est pas sérialisable, il faut que la super classe déclare au moins un constructeur vide sans argument. De plus, la classe dérivée doit prendre en compte la sérialisation des champs protected ou public de sa super classe.
Lorqu’une classe est désérialisée par Java, les champs de la super-classe qui ne serait pas sérialisable seront initialisés dans le constructeur vide sans argument. D’où son importance. Faute de cela, vous aurez une exception au runtime alors que votre classe fille est marquée comme étant Serializable… Ce n’est pas toujours visible au premier coup d’oeil. Des outils comme PMD sont capables de détecter ce type de problèmes et de vous aider.
Sachez aussi qu’il est possible de prendre en main le mécanisme de sérialisation/désérialisation en définissant 2 méthodes Java dans votre classe. Ce qui est intéressant à retenir, c’est que ces méthodes ne sont pas définies dans une interface… A votre avis pourquoi ? Il y a une raison très précise…
Les 2 méthodes sont:
private void writeObject(java.io.ObjectOutputStream out) private void readObject(java.io.ObjectInputStream in)
Ne modifiez pas la signature de l’une de ses méthodes, Java utilise l’introspection pour les identifier lors de la sérialisation et désérialisation.
En général, le code d’une méthode de sérialisation lorsque la classe contient des membres non sérialisables ressemble à ceci:
private void readObject(ObjectInputStream in) { // Deserialise normalement la classe in.defaultReadObject(); // Instancie un objet qui ne pouvait pas être sérialisé this.socket = new Socket(); }
Quoi d’autre ? Vous pouvez aussi avec Java 5 utiliser un autre mécanisme de sérialisation. Pour cela il faut définir 2 méthodes dans votre class:
public Object writeReplace() throws ObjectStreamException; public Object readResolve() throws ObjectStreamException;
Ce mécanisme permet de retourner un objet autre que le stream par défaut lors de la sérialisation et de la désérialisation. Vous pouvez même marquer ces 2 méthodes en tant que private, voir la javadoc à ce sujet.
A propos de serialVersionUID :
Qu’est-ce que le champ serialVersionUID ?
C’est une clé de hachage SHA qui identifie de manière unique votre Classe. Cela permet lorsqu’elle est sérialisée, de la marquer avec une somme de contrôle (checksum) pour que lors de la désérialisation, votre programme soit certain de la version de la classe Java qu’il manipule. C’est un gestionnaire de version de votre classe si vous préférez.
A quoi sert-il ?
Imaginez un serveur et un client qui s’échangent des objets sérialisés. Ce système permet de s’assurer que les versions des classes envoyées d’un côté, existent bien de l’autre côté. Si la version est différente, Java lève une java.io.InvalidClassException pour vous dire qu’il y a un souci de version. Si je le déclare, est-ce que la sérialisation va plus vite ?Et non… c’est une des légendes urbaines en Java. Avant tout, le calcul de ce serialVersionUID n’est effectué que lors du chargement de la classe par le ClassLoader. Typiquement sur un serveur démarré, je ne pense pas que vous verrez un quelconque gain de performance… La sérialisation d’une classe est lente, ne confondez pas avec le calcul d’un checksum SHA, opération assez rapide.
Comment le déclarer ?
Il faut que cette variable respecte à la lettre la signature afin que Java puisse la voir lors de la sérialisation et de la désérialisation. En général on utilise des Long, en prenant soin de mettre un bon nombre de chiffres, histoire de ne pas tomber sur la même clé que le voisin.
private static final long serialVersionUID = 354054054054L;
Notez le « L » à la fin de la ligne pour indiquer qu’il s’agit d’un long et éviter un cast fantôme.A noter que vous pouvez déclarer ce champ protected. C’est une très mauvaise idée, sauf à savoir ce que vous faîtes.
Faut-il déclarer ce champ ?
Sur la nécessité de le faire, la réponse est non. Java se charge de calculer le serialVersionUID lors du chargement de la class pour sérialiser/désérialiser. C’est donc fait automatiquement et vous n’avez pas à le déclarer pour que la sérialisation fonctionne. C’est pour cela que je m’en suis passé dans l’article d’avant-hier.
Maintenant sur l’opportunité, voici ce que dit la spécification Java 5 sur la sérialisation:Note ¯ It is strongly recommended that all serializable classes explicitly declare serialVersionUID values, since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, and can thus result in unexpected serialVersionUID conflicts during deserialization, causing deserialization to fail.
Que se passe-t-il si je le déclare ?
Je vous recommande de déclarer ce champ si vous savez ce que vous faîtes. En spécifiant ce tag, vous prenez en quelque sorte la responsabilité de gérer la version de votre classe.Il existe pleins de cas que j’ai rencontré où cela facilite la vie. Par exemple avec JBoss. Lorsque vous travaillez avec une arborescence explosé de votre EAR, et que vous ne modifiez qu’une partie, vous êtes souvent obligé de forcer ce tag pour être tranquille. Sans cela, JBoss et le ClassLoader (qui est lui aussi versionné, j’en parlerai une fois) va jeter des NoClassDefFoundError… difficile de faire le lien avec la sérialisation mais pourtant c’est le cas… C’est aussi pratique en phase de développement, lorsque vous travaillez sur la classe qui doit être sérialisée dans une architecture client-serveur avec RMI. Cela permet par exemple de déployer une nouvelle version de la classe sur le client 1 et ne vous force pas à mettre à jour le client 2 distant. Sans cela, à chaque nouvelle version vous seriez obligé de livrer la même version à vos différentes JVM ce qui n’est pas forcément possible. La Javadoc recommande de déclarer ce champ, car sa valeur est très sensible aussi au compilateur qui génère ce nombre. Afin de garantir que la valeur d’un serialVersionUID soit constante quelque soit le compilateur, il est recommandé de le fixer une fois pour toute.
Mini quizz pour tester ses connaissances
Pour terminer, quelques petites questions:
- un champ static est-il sérialisable ?
- un champ volatile est-il serialisable ?
- Soit une classe Player qui implémente Serializable. Est-ce qu’une inner-class dans Player doit aussi implémenter cette interface ? Est-ce que l’inner-class sera aussi sérialisée ?
Références:
Article Java World « Into the mist of serialization »
« Java Object Serialization » version 1.5.0
Plusieurs remarques par rapport à l’affirmation :
« La sérialisation d’une classe est lente, ne confondez pas avec le calcul d’un checksum SHA, opération assez rapide. »
* On sérialise un objet. On ne sérialise pas une classe.
* Pour la comparaison sérialisation d’un objet/SHA, faut pas mélanger les choux et les carottes :
* Le SHA est celui du code de la classe, et vraissemblablement calculé une seule fois au moment de la compilation.
* La sérialisation nécessite d’accéder à tous les champs de l’objet un par un pour sauvegarder leur valeur. Si on devait calculer le SHA de l’objet (et non le SHA de la classe), ce serait forcément plus long que de la sérialiser. A moins bien sûr que le format de sérialisation soit particulièrement couteux (ce qui est le cas si on sérialise en XML… ;o) ).
La première chose à noter, c’est que l’utilsiation de la sérialisation pour échanger des objets n’est pas forcément la meilleure façon de faire.
La deuxième chose, c’est que pour contrôler la sérialisation, plutôt que de déclarer les deux méthodes, il vaut mieux implémenter l’interface Externalizable, qui définit ces deux méthodes dans l’interface, ce qui est plus propre et plus efficace.
Quant à utiliser un grand nombre pour ce serialVersionUID, je suis plus que sceptique …
Personnellmement, je fais démarrer les versions de mes obejts à 1, et j’incrémente pour chaque changement incompatible, et ça marche forcément, puisque le serialVersionUID identife une version de ma classe, et non mon objet par rapport à tous les objets Java de l’univers.
Bon, en gros, il est fortement recommander de déclarer explicitement serialVersionUID. Bien, mais maintenant, quelle valeur lui affectée ? Une valeur calculée ? La valeur 1L à la définition de la classe, puis on incrémente au fur et à mesure des modifications de la classe ?
Site interressant, instructif, clair
Par expérience je ne déclare pas ce champ et je laisse le compilateur s’en charger. Attention à ne pas être trompé par le Warning dans Eclipse qui vous recommande de déclarer ce champ et qui propose 1L…