2010-06-03 24 views
53

Est-il possible d'avoir des champs final transient qui ont une valeur autre que celle par défaut après la sérialisation en Java? Mon usecase est une variable de cache - c'est pourquoi c'est transient. J'ai également l'habitude de faire des champs Map qui ne seront pas changés (c'est-à-dire que le contenu de la carte est changé, mais l'objet lui-même reste le même) final. Cependant, ces attributs semblent être contradictoires - alors que le compilateur permet une telle combinaison, je ne peux pas mettre le champ à autre chose que null après la désérialisation.champs transitoires définitifs et sérialisation

J'ai essayé ce qui suit, sans succès:

  • simple, l'initialisation du champ (dans l'exemple): ce que je fais normalement, mais l'initialisation ne semble pas se produire après délinéarisation;
  • initialisation dans le constructeur (je crois que c'est sémantiquement le même que ci-dessus);
  • Affectation du champ dans readObject() - impossible car le champ est final.

Dans l'exemple cache est public uniquement pour le test.

import java.io.*; 
import java.util.*; 

public class test 
{ 
    public static void main (String[] args) throws Exception 
    { 
     X x = new X(); 
     System.out.println (x + " " + x.cache); 

     ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 
     new ObjectOutputStream (buffer).writeObject (x); 
     x = (X) new ObjectInputStream (new ByteArrayInputStream (buffer.toByteArray())).readObject(); 
     System.out.println (x + " " + x.cache); 
    } 

    public static class X implements Serializable 
    { 
     public final transient Map <Object, Object> cache = new HashMap <Object, Object>(); 
    } 
} 

Sortie:

[email protected] {} 
[email protected] null 

Répondre

30

La réponse est "non", malheureusement, - je l'ai souvent voulu cela. mais les transitoires ne peuvent pas être définitifs.

Un champ final doit être initialisé soit par affectation directe d'une valeur initiale, soit dans le constructeur. Lors de la désérialisation, aucune de ces options n'est invoquée. Par conséquent, les valeurs initiales des transitoires doivent être définies dans la méthode privée 'readObject()' invoquée lors de la désérialisation. Et pour que cela fonctionne, les transitoires doivent être non-finaux.

(à proprement parler, les finales ne sont définitives que la première fois qu'ils sont lus, donc il y a des hacks qui sont possibles qui attribuent une valeur avant qu'il ne soit lu, mais pour moi cela va un pas trop loin.)

+0

Merci. Je m'en doutais, mais je ne savais pas que je n'avais pas manqué quelque chose. – doublep

+4

Votre réponse «les transitoires ne peuvent pas être définitifs» est incorrecte: veuillez expliquer le code source d'Hibernate avec «final transitoire» partout: https://github.com/hibernate/hibernate-orm/blob/4.3.7.Final/hibernate- core/src/main/java/org/hibernate/interne/SessionFactoryImpl.java –

+12

En fait, la réponse est fausse. Les champs "transitoires" peuvent être "finaux". Mais pour que cela fonctionne pour autre chose que les valeurs par défaut ('false' /' 0'/'0.0' /' null'), vous voulez implémenter non seulement 'readObject()' mais aussi 'readResolve()', ou utilisez * Reflection *. –

14

Vous pouvez modifier le contenu d'un champ à l'aide de Reflection. Fonctionne sur Java 1.5+. Cela fonctionnera, car la sérialisation est effectuée dans un seul thread. Après qu'un autre thread accède au même objet, il ne doit pas changer le champ final (en raison de l'étrangeté dans le reflet du modèle de mémoire &).

Ainsi, readObject(), vous pouvez faire quelque chose de similaire à cet exemple:

import java.lang.reflect.Field; 

public class FinalTransient { 

    private final transient Object a = null; 

    public static void main(String... args) throws Exception { 
     FinalTransient b = new FinalTransient(); 

     System.out.println("First: " + b.a); // e.g. after serialization 

     Field f = b.getClass().getDeclaredField("a"); 
     f.setAccessible(true); 
     f.set(b, 6); // e.g. putting back your cache 

     System.out.println("Second: " + b.a); // wow: it has a value! 
    } 

} 

Rappelez-vous: Final is not final anymore!

+3

Eh bien, ça a l'air trop brouillon, je suppose qu'il est plus facile d'abandonner ici 'final';) – doublep

+1

Vous pouvez également implémenter un 'TransientMap', que vous marquez' final' mais pas 'transitoire'. Cependant, chaque propriété de la carte doit être "transitoire", et donc la carte n'est pas sérialisée, mais elle existe toujours lors de la désérialisation (et vide). – Pindatjuh

+0

@doublep: en fait, la désérialisation est la raison pour laquelle cette possibilité existe.C'est aussi la raison pour laquelle cela ne fonctionne pas pour les champs 'static final', les champs' static' ne sont jamais (dé) sérialisés, par conséquent, il n'y a pas besoin d'une telle fonctionnalité. – Holger

5

La solution générale à des problèmes comme celui-ci est d'utiliser un « proxy série » (voir efficace Java 2ème Ed). Si vous avez besoin de convertir ceci en une classe sérialisable existante sans casser la compatibilité série, alors vous devrez faire du piratage.

+0

Ne supposez pas que vous pourriez développer cette réponse, pourriez-vous? J'ai peur de ne pas avoir le livre en question ... – Jules

+0

@ user1803551 Ce n'est pas vraiment utile. Les réponses ici sont censées fournir une description réelle de la façon de résoudre le problème, pas seulement un pointeur vers une recherche google. – Jules

11

Oui, cela est facilement possible en implémentant la méthode (apparemment peu connue!) readResolve(). Il vous permet de remplacer l'objet après sa désérialisation. Vous pouvez l'utiliser pour appeler un constructeur qui initialisera un objet de remplacement comme vous le souhaitez.Un exemple:

import java.io.*; 
import java.util.*; 

public class test { 
    public static void main(String[] args) throws Exception { 
     X x = new X(); 
     x.name = "This data will be serialized"; 
     x.cache.put("This data", "is transient"); 
     System.out.println("Before: " + x + " '" + x.name + "' " + x.cache); 

     ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 
     new ObjectOutputStream(buffer).writeObject(x); 
     x = (X)new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())).readObject(); 
     System.out.println("After: " + x + " '" + x.name + "' " + x.cache); 
    } 

    public static class X implements Serializable { 
     public final transient Map<Object,Object> cache = new HashMap<>(); 
     public String name; 

     public X() {} // normal constructor 

     private X(X x) { // constructor for deserialization 
      // copy the non-transient fields 
      this.name = x.name; 
     } 

     private Object readResolve() { 
      // create a new object from the deserialized one 
      return new X(this); 
     } 
    } 
} 

sortie - la chaîne est préservée, mais la carte transitoire est remise à un vide Carte:

Before: [email protected] 'This data will be serialized' {This data=is transient} 
After: [email protected] 'This data will be serialized' {} 
+0

Ne serait pas appeler cela facile. Le constructeur de copie n'est pas automatique, donc si j'ai 20 champs, dont 2 transitoires, j'ai besoin de copier sélectivement 18 champs dans le constructeur de copie. Cependant, cela réalise effectivement ce que je voulais. – doublep

3

Cinq ans plus tard, je trouve mon original (mais non nul!) répondre insatisfaisant après je suis tombé sur ce post via Google. Une autre solution consisterait à n'utiliser aucune réflexion et à utiliser la technique suggérée par Boann.

Il utilise également la classe GetField retournée par la méthode ObjectInputStream#readFields(), qui selon la spécification de sérialisation doit être appelée dans la méthode privée readObject(...).

La solution rend la désérialisation de champ explicite en stockant les champs récupérés dans un champ transitoire temporaire (appelé FinalExample#fields) d'une «instance» temporaire créée par le processus de désérialisation. Tous les champs d'objet sont ensuite désérialisés et readResolve(...) est appelée: une nouvelle instance est créée mais cette fois en utilisant un constructeur, en supprimant l'instance temporaire avec le champ temporaire. L'instance restaure explicitement chaque champ à l'aide de l'instance GetField; c'est l'endroit pour vérifier tous les paramètres comme n'importe quel autre constructeur. Si une exception est levée par le constructeur, elle est convertie en InvalidObjectException et la désérialisation de cet objet échoue. Le micro-benchmark inclus garantit que cette solution n'est pas plus lente que la sérialisation/désérialisation par défaut. En effet, il est sur mon PC:

Problem: 8.598s Solution: 7.818s 

Ensuite, voici le code:

import java.io.ByteArrayInputStream; 
import java.io.ByteArrayOutputStream; 
import java.io.IOException; 
import java.io.InvalidObjectException; 
import java.io.ObjectInputStream; 
import java.io.ObjectInputStream.GetField; 
import java.io.ObjectOutputStream; 
import java.io.ObjectStreamException; 
import java.io.Serializable; 

import org.junit.Test; 

import static org.junit.Assert.*; 

public class FinalSerialization { 

    /** 
    * Using default serialization, there are problems with transient final 
    * fields. This is because internally, ObjectInputStream uses the Unsafe 
    * class to create an "instance", without calling a constructor. 
    */ 
    @Test 
    public void problem() throws Exception { 
     ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
     ObjectOutputStream oos = new ObjectOutputStream(baos); 
     WrongExample x = new WrongExample(1234); 
     oos.writeObject(x); 
     oos.close(); 
     ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); 
     ObjectInputStream ois = new ObjectInputStream(bais); 
     WrongExample y = (WrongExample) ois.readObject(); 
     assertTrue(y.value == 1234); 
     // Problem: 
     assertFalse(y.ref != null); 
     ois.close(); 
     baos.close(); 
     bais.close(); 
    } 

    /** 
    * Use the readResolve method to construct a new object with the correct 
    * finals initialized. Because we now call the constructor explicitly, all 
    * finals are properly set up. 
    */ 
    @Test 
    public void solution() throws Exception { 
     ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
     ObjectOutputStream oos = new ObjectOutputStream(baos); 
     FinalExample x = new FinalExample(1234); 
     oos.writeObject(x); 
     oos.close(); 
     ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); 
     ObjectInputStream ois = new ObjectInputStream(bais); 
     FinalExample y = (FinalExample) ois.readObject(); 
     assertTrue(y.ref != null); 
     assertTrue(y.value == 1234); 
     ois.close(); 
     baos.close(); 
     bais.close(); 
    } 

    /** 
    * The solution <em>should not</em> have worse execution time than built-in 
    * deserialization. 
    */ 
    @Test 
    public void benchmark() throws Exception { 
     int TRIALS = 500_000; 

     long a = System.currentTimeMillis(); 
     for (int i = 0; i < TRIALS; i++) { 
      problem(); 
     } 
     a = System.currentTimeMillis() - a; 

     long b = System.currentTimeMillis(); 
     for (int i = 0; i < TRIALS; i++) { 
      solution(); 
     } 
     b = System.currentTimeMillis() - b; 

     System.out.println("Problem: " + a/1000f + "s Solution: " + b/1000f + "s"); 
     assertTrue(b <= a); 
    } 

    public static class FinalExample implements Serializable { 

     private static final long serialVersionUID = 4772085863429354018L; 

     public final transient Object ref = new Object(); 

     public final int value; 

     private transient GetField fields; 

     public FinalExample(int value) { 
      this.value = value; 
     } 

     private FinalExample(GetField fields) throws IOException { 
      // assign fields 
      value = fields.get("value", 0); 
     } 

     private void readObject(ObjectInputStream stream) throws IOException, 
       ClassNotFoundException { 
      fields = stream.readFields(); 
     } 

     private Object readResolve() throws ObjectStreamException { 
      try { 
       return new FinalExample(fields); 
      } catch (IOException ex) { 
       throw new InvalidObjectException(ex.getMessage()); 
      } 
     } 

    } 

    public static class WrongExample implements Serializable { 

     private static final long serialVersionUID = 4772085863429354018L; 

     public final transient Object ref = new Object(); 

     public final int value; 

     public WrongExample(int value) { 
      this.value = value; 
     } 

    } 

} 

Une note de prudence: chaque fois que la classe fait référence à une autre instance d'objet, il pourrait être possible de fuir le temporaire "instance" créée par le processus de sérialisation: la résolution de l'objet ne se produit qu'après la lecture de tous les sous-objets, il est donc possible que les sous-objets conservent une référence à l'objet temporaire. Les classes peuvent vérifier l'utilisation de telles instances construites illégalement en vérifiant que le champ temporaire GetField est nul. Seulement quand il est nul, il a été créé en utilisant un constructeur régulier et pas à travers le processus de désérialisation. Note à l'attention de soi: Peut-être qu'une meilleure solution existe dans cinq ans. À plus tard!

+1

Notez que cela ne semble fonctionner que pour les valeurs primitives. Après un test avec des valeurs Object, une erreur InternalError est levée car l'objet GetField ne devrait pas échapper à la méthode readObject. Par conséquent cette réponse réduit à la réponse de Boann et n'ajoute rien de nouveau. – Pindatjuh