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 esfinal
.
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'' {}