Пять лет спустя я нахожу свой оригинальный ответ неудовлетворительным после того, как наткнулся на этот пост через Google. Другим решением было бы вообще не использовать отражение и использовать технику, предложенную Boann.
Также используется класс GetField , возвращаемый методом ObjectInputStream#readFields()
, который в соответствии со спецификацией сериализации должен вызываться в закрытом методе readObject(...)
.
Решение делает явную десериализацию поля путем сохранения найденных полей во временном переходном поле (называемом FinalExample#fields
) временного «экземпляра», созданного процессом десериализации. Затем все поля объекта десериализуются и вызывается readResolve(...)
: создается новый экземпляр, но на этот раз с использованием конструктора, отбрасывающего временный экземпляр с помощью временного поля. Экземпляр явно восстанавливает каждое поле, используя экземпляр GetField
; это место для проверки любых параметров, как и любой другой конструктор. Если конструктор выдает исключение, оно преобразуется в InvalidObjectException
, и десериализация этого объекта завершается неудачей.
Микропроцессор, поставляемый в комплекте, гарантирует, что это решение не медленнее, чем сериализация / десериализация по умолчанию. Действительно, это на моем ПК:
Problem: 8.598s Solution: 7.818s
Тогда вот код:
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;
}
}
}
Предупреждение: всякий раз, когда класс ссылается на другой экземпляр объекта, может быть возможно утечка временного «экземпляра», созданного процессом сериализации: разрешение объекта происходит только после того, как все подобъекты прочитаны, следовательно, это возможно, чтобы подобъекты сохраняли ссылку на временный объект. Классы могут проверить использование таких незаконно созданных экземпляров, проверив, что временное поле GetField
является нулевым. Только когда он нулевой, он был создан с использованием обычного конструктора, а не в процессе десериализации.
Примечание для себя: Возможно, лучшее решение существует через пять лет. Увидимся тогда!