2010-06-03 22 views
53

È possibile avere campi final transient impostati su qualsiasi valore non predefinito dopo la serializzazione in Java? Il mio caso è una variabile di cache - ecco perché è transient. Ho anche l'abitudine di creare campi Map che non verranno modificati (vale a dire il contenuto della mappa è cambiato, ma l'oggetto stesso rimane lo stesso) final. Tuttavia, questi attributi sembrano contraddittori: mentre il compilatore consente una tale combinazione, non posso impostare il campo su un valore diverso da null dopo la non serializzazione.campi transitori finali e serializzazione

Ho provato quanto segue, senza successo:

  • semplice inizializzazione campo (mostrato nell'esempio): questo è ciò che faccio normalmente, ma l'inizializzazione non sembra accadere dopo deserializzazione;
  • inizializzazione nel costruttore (credo che questo sia semanticamente lo stesso di sopra però);
  • assegnazione del campo in readObject() - impossibile eseguire poiché il campo è final.

Nell'esempio cache è public solo per il 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>(); 
    } 
} 

uscita:

[email protected] {} 
[email protected] null 

risposta

30

La risposta è "no" purtroppo - Ho spesso voluto questo. ma i transienti non possono essere definitivi.

Un campo finale deve essere inizializzato tramite assegnazione diretta di un valore iniziale o nel costruttore. Durante la deserializzazione, nessuno di questi viene invocato, quindi i valori iniziali per i transienti devono essere impostati nel metodo privato 'readObject()' richiamato durante la deserializzazione. E affinché funzioni, i transienti devono essere non-finali.

(A rigor di termini, le finali sono solo finale la prima volta che vengono letti, quindi non ci sono hack che sono possibili che assegnano un valore prima si legge, ma per me questo sta andando un passo troppo lontano.)

+0

Grazie. Sospettavo che fosse così anche, ma non ero sicuro di non aver perso qualcosa. – doublep

+4

La tua risposta "i transitori non possono essere definitivi" non è corretta: spiega il codice sorgente di Hibernate con "transitorio finale" dappertutto: https://github.com/hibernate/hibernate-orm/blob/4.3.7.Final/hibernate- core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java –

+12

In realtà la risposta è sbagliata. i campi 'transient' possono essere' final'. Ma per farlo funzionare per qualcosa di diverso dai valori predefiniti ('false' /' 0'/'0.0' /' null'), si desidera implementare non solo 'readObject()' ma anche 'readResolve()', o usare * Riflessione *. –

14

È possibile modificare il contenuto di un campo utilizzando Reflection. Funziona su Java 1.5+. Funzionerà, perché la serializzazione viene eseguita in un singolo thread. Dopo che un altro thread accede allo stesso oggetto, non dovrebbe cambiare il campo finale (a causa della stranezza nel reflaction del modello di memoria &).

Così, in readObject(), si può fare qualcosa di simile a questo esempio:

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! 
    } 

} 

Ricorda: Final is not final anymore!

+3

Beh, sembra troppo disordinato, immagino che sia più facile rinunciare a 'final' qui;) – doublep

+1

Puoi anche implementare un 'TransientMap', che contrassegni' final' ma non 'transient'. Ogni proprietà, tuttavia, nella mappa deve essere 'transient', e quindi la mappa non è serializzata, ma esiste ancora in unserialization (e vuota). – Pindatjuh

+0

@doublep: in realtà, la deserializzazione è la ragione per cui esiste questa possibilità.Questo è anche il motivo per cui non funziona per i campi 'static final', i campi' static' non sono mai (de-) serializzati, quindi, non c'è bisogno di tale funzione. – Holger

5

La soluzione generale ai problemi di questo tipo è quello di utilizzare un "proxy di serie" (vedi efficace Java 2nd Ed). Se è necessario eseguire il retrofit su una classe serializzabile esistente senza rompere la compatibilità seriale, è necessario eseguire alcuni attacchi.

+0

Non pensare di poter ampliare questa risposta, vero? Ho paura di non avere il libro in questione ... – Jules

+0

@ user1803551 Non è esattamente d'aiuto. Le risposte qui dovrebbero fornire una descrizione reale di come risolvere il problema, non solo un puntatore a una ricerca su google. – Jules

11

Sì, questo è facilmente possibile implementando il metodo (apparentemente poco conosciuto!) readResolve(). Ti consente di sostituire l'oggetto dopo che è stato deserializzato. Puoi usarlo per invocare un costruttore che inizializzerà un oggetto sostitutivo come vuoi tu.Un esempio:

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); 
     } 
    } 
} 

uscita - la stringa viene conservato, ma la mappa transitoria è riportato a un vuoto carta:

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

Non chiamerei così facile. Il costruttore di copie non è automatico, quindi se ho 20 campi, 2 dei quali transitori, ho bisogno di copiare selettivamente 18 campi nel costruttore di copie. Tuttavia, questo effettivamente raggiunge ciò che volevo. – doublep

3

Cinque anni più tardi, ho trovato il mio originale (ma non nullo!) risposta insoddisfacente dopo che mi sono imbattuto in questo post tramite Google. Un'altra soluzione sarebbe l'assenza di riflessione e l'uso della tecnica suggerita da Boann.

Utilizza inoltre la classe GetField restituita dal metodo ObjectInputStream#readFields(), che in base alla specifica di serializzazione deve essere richiamata nel metodo privato readObject(...).

La soluzione rende esplicita la deserializzazione dei campi archiviando i campi recuperati in un campo temporaneo temporaneo (chiamato FinalExample#fields) di una "istanza" temporanea creata dal processo di deserializzazione. Tutti i campi oggetto vengono quindi deserializzati e viene chiamato readResolve(...): viene creata una nuova istanza, ma questa volta utilizzando un costruttore, scartando l'istanza temporanea con il campo temporaneo. L'istanza ripristina esplicitamente ogni campo utilizzando l'istanza GetField; questo è il posto dove controllare qualsiasi parametro come qualsiasi altro costruttore. Se viene generata un'eccezione dal costruttore, viene convertita in InvalidObjectException e la deserializzazione di questo oggetto non riesce.

Il micro-benchmark incluso garantisce che questa soluzione non sia più lenta della serializzazione/deserializzazione predefinita. Infatti, è sul mio PC:

Problem: 8.598s Solution: 7.818s 

Poi qui è il codice:

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; 
     } 

    } 

} 

Una nota di cautela: ogni volta che la classe fa riferimento a un'altra istanza dell'oggetto, potrebbe essere possibile perdita temporanea "istanza" creata dal processo di serializzazione: la risoluzione dell'oggetto si verifica solo dopo aver letto tutti i sottooggetti, quindi è possibile che i sottooggetti mantengano un riferimento all'oggetto temporaneo. Le classi possono verificare l'utilizzo di tali istanze costruite illegalmente controllando che il campo temporaneo GetField sia nullo. Solo quando è nullo, è stato creato utilizzando un normale costruttore e non attraverso il processo di deserializzazione.

Nota per sé: forse una soluzione migliore esiste in cinque anni. Ci vediamo!

+1

Si noti che questo sembra funzionare solo per i valori primitivi. Dopo aver eseguito il test con i valori Object, viene generato un InternalError poiché non è previsto che l'oggetto GetField sfugga al metodo readObject. Quindi questa risposta si riduce alla risposta di Boann e non aggiunge nulla di nuovo. – Pindatjuh

Problemi correlati