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!
Grazie. Sospettavo che fosse così anche, ma non ero sicuro di non aver perso qualcosa. – doublep
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 –
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 *. –