2010-06-03 7 views
53

¿Es posible tener final transient campos que se establecen en cualquier valor no predeterminado después de la serialización en Java? Mi caso de uso es una variable de caché, por eso es transient. También tengo la costumbre de hacer Map campos que no se cambiarán (es decir, se cambia el contenido del mapa, pero el objeto mismo permanece igual) final. Sin embargo, estos atributos parecen ser contradictorios, mientras que el compilador permite dicha combinación, no puedo tener el campo configurado en nada más que null después de la deserialización.campos transitorios finales y serialización

He intentado lo siguiente, sin éxito:

  • inicialización de campo sencilla (que se muestra en el ejemplo): esto es lo que hago normalmente, pero no parece que la inicialización a pasar después unserialization;
  • inicialización en constructor (creo que esto es semánticamente el mismo que el anterior);
  • asignando el campo en readObject() - no se puede hacer dado que el campo es final.

En el ejemplo cache es public solo para realizar pruebas.

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

Salida:

[email protected]e30 {} 
[email protected] null 

Respuesta

30

La respuesta corta es "no" por desgracia - a menudo he querido esto. pero los transitorios no pueden ser finales.

Un campo final se debe inicializar mediante la asignación directa de un valor inicial o en el constructor. Durante la deserialización, ninguno de estos se invoca, por lo que los valores iniciales para los transitorios se deben establecer en el método privado 'readObject()' que se invoca durante la deserialización. Y para que eso funcione, los transitorios deben ser no finales.

(Estrictamente hablando, finales son única final de la primera vez que se leen, por lo que hay cortes que son posibles que asignan un valor antes de que se lee, pero para mí esto va un paso demasiado lejos.)

+0

Gracias. Sospeché que también era así, pero no estaba seguro de no extrañar algo. – doublep

+4

Su respuesta "los transitorios no pueden ser definitivos" es incorrecta: explique el código fuente de Hibernate con 'transitorio final 'por todas partes: https://github.com/hibernate/hibernate-orm/blob/4.3.7.Final/hibernate- core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java –

+12

En realidad, la respuesta es incorrecta. Los campos 'transitorios' pueden ser' finales'. Pero para lograr que funcione para algo distinto de los valores predeterminados ('false' /' 0'/'0.0' /' null'), no solo debe implementar 'readObject()' sino también 'readResolve()', o use * Reflection *. –

14

Puede cambiar el contenido de un campo usando Reflection. Funciona en Java 1.5+. Funcionará, porque la serialización se realiza en un solo hilo. Después de que otro subproceso acceda al mismo objeto, no debería cambiar el campo final (debido a la rareza en el modelo de memoria & reflaction).

Así, en readObject(), se puede hacer algo similar a este ejemplo:

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

} 

Recuerde: Final is not final anymore!

+3

Bueno, parece demasiado desordenado, supongo que es más fácil renunciar a 'final' aquí;) – doublep

+1

También puede implementar un 'TransientMap', que marca' final' pero no 'transient'. Sin embargo, cada propiedad en el mapa debe ser 'transitoria' y, por lo tanto, el mapa no está serializado, pero aún existe en la deserialización (y está vacío). – Pindatjuh

+0

@doublep: en realidad, la deserialización es la razón por la cual existe esta posibilidad.Esa es también la razón por la cual no funciona para los campos 'static final',' static' fields nunca se (des) serializan, por lo tanto, no hay necesidad de tal característica. – Holger

5

La solución general a este tipo de problemas es el uso de un "proxy de serie" (ver eficaz Java 2nd Ed). Si necesita adaptar esto a una clase serializable existente sin romper la compatibilidad en serie, entonces deberá hacer algo de piratería.

+0

Supongo que no podría ampliar esta respuesta, ¿verdad? Me temo que no tengo el libro en cuestión ... – Jules

+0

@ user1803551 Eso no es exactamente útil. Las respuestas aquí se supone que proporcionan una descripción real de cómo resolver el problema, no solo un puntero a una búsqueda en google. – Jules

11

Sí, esto es fácilmente posible mediante la implementación del método (aparentemente poco conocido!) readResolve(). Te permite reemplazar el objeto una vez deserializado. Puede usar eso para invocar un constructor que inicializará un objeto de reemplazo como quiera.Un ejemplo:

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

de salida - la cadena se conserva pero el mapa transitoria se repone en un mapa vacío:

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

No lo llamaría así de fácil. El constructor de copias no es automático, así que si tengo 20 campos, 2 de ellos transitorios, necesito copiar selectivamente 18 campos en el constructor de copias. Sin embargo, esto sí logra lo que yo quería. – doublep

3

Cinco años más tarde, me encuentro con mi original (pero no nulo!) respuesta insatisfactoria después de tropezar con esta publicación a través de Google. Otra solución sería no usar ningún reflejo, y usar la técnica sugerida por Boann.

También hace uso de la clase GetField devuelta por el método ObjectInputStream#readFields(), que de acuerdo con la especificación de serialización debe llamarse en el método privado readObject(...).

La solución hace que la deserialización de campo sea explícita almacenando los campos recuperados en un campo temporal transitorio (llamado FinalExample#fields) de una "instancia" temporal creada por el proceso de deserialización. Todos los campos de objeto se deserializan y se llama al readResolve(...): se crea una nueva instancia pero esta vez usando un constructor, descartando la instancia temporal con el campo temporal. La instancia restaura explícitamente cada campo con la instancia GetField; este es el lugar para verificar cualquier parámetro como lo haría cualquier otro constructor. Si el constructor lanza una excepción, se traduce a InvalidObjectException y falla la deserialización de este objeto.

El micro-índice de referencia incluido garantiza que esta solución no sea más lenta que la serialización/deserialización predeterminada. De hecho, es en mi PC:

Problem: 8.598s Solution: 7.818s 

Entonces aquí está el código:

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 de precaución: cuando la clase se refiere a otra instancia de objeto, podría ser posible fuga del temporal "instancia" creada por el proceso de serialización: la resolución del objeto ocurre solo después de que se leen todos los sub-objetos, por lo tanto, es posible que los subobjetos mantengan una referencia al objeto temporal. Las clases pueden verificar el uso de dichas instancias construidas ilegalmente comprobando que el campo temporal GetField es nulo. Solo cuando es nulo, se creó utilizando un constructor regular y no a través del proceso de deserialización.

Nota para mí: Quizás exista una solución mejor en cinco años. ¡Hasta entonces!

+1

Tenga en cuenta que esto solo parece funcionar para valores primitivos. Después de probar con los valores de Object, se lanza un InternalError porque no se espera que el objeto GetField escape del método readObject. Por lo tanto, esta respuesta se reduce a la respuesta de Boann y no agrega nada nuevo. – Pindatjuh

Cuestiones relacionadas