vuelva variable una que puedo prevenir miembro como java serialization final

java - variable - transient que es



campos transitorios finales y serialización (5)

¿Es posible tener campos final transient 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 campos de Map que no se cambiarán (es decir, el contenido del mapa se cambia, pero el objeto en sí 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 cualquier cosa que no sea null después de la deserialización.

Intenté lo siguiente, sin éxito:

  • inicialización de campo simple (que se muestra en el ejemplo): esto es lo que normalmente hago, pero parece que la inicialización no ocurre después de la deserialización;
  • 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, la cache es public solo para 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:

test$X@1a46e30 {} test$X@190d11 null


Cinco años después, considero que mi respuesta original no es satisfactoria después de encontrar 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 ObjectInputStream#readFields() , que de acuerdo con la especificación de Serialización debe readObject(...) en el readObject(...) 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 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 utilizando la instancia de 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 la deserialización de este objeto falla.

El micro-benchmark incluido asegura que esta solución no sea más lenta que la serialización / deserialización predeterminada. De hecho, está 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; } } }

Nota de precaución: siempre que la clase se refiera a otra instancia de objeto, podría ser posible filtrar la "instancia" temporal 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 para mantener una referencia al objeto temporal. Las clases pueden verificar el uso de tales instancias construidas ilegalmente al verificar que el campo temporal de GetField sea ​​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!


La respuesta corta es "no" desafortunadamente, 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, las finales solo son definitivas la primera vez que se leen, por lo que hay posibles ataques que asignan un valor antes de que se lea, pero para mí esto va demasiado lejos).


La solución general a problemas como este es usar un "proxy serial" (ver Effective 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.


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 y la reflacción).

Por lo tanto, en readObject() , 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: ¡ La final ya no es definitiva!


Sí, esto es fácilmente posible mediante la implementación del método readResolve() aparentemente poco conocido. 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); } } }

Salida: la cadena se conserva pero el mapa transitorio se restablece a un mapa vacío (¡pero no nulo!):

Before: test$X@172e0cc ''This data will be serialized'' {This data=is transient} After: test$X@490662 ''This data will be serialized'' {}