下面的代码抛出一个NullPointerException
。
import java.io.*;
public class NullFinalTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Foo foo = new Foo();
foo.useLock();
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
new ObjectOutputStream(buffer).writeObject(foo);
foo = (Foo) new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())).readObject();
foo.useLock();
}
public static class Foo implements Serializable {
private final String lockUsed = "lock used";
private transient final Object lock = new Object();
public void useLock() {
System.out.println("About to synchronize");
synchronized (lock) { // <- NullPointerException here on 2nd call
System.out.println(lockUsed);
}
}
}
}
下面是输出:
About to synchronize
lock used
About to synchronize
Exception in thread "main" java.lang.NullPointerException
at NullFinalTest$Foo.useLock(NullFinalTest.java:18)
at NullFinalTest.main(NullFinalTest.java:10)
如何lock
可能为空?
A transient final field used as a lock is null
以下是有关瞬态变量几个事实:
-在一个实例变量使用时瞬态关键字,将阻止实例变量被序列化。
-在反序列化,瞬态变量得到它们的默认值 .....
例如:
- 对象引用变量来
null
- 诠释到
0
- 布尔值,
false,
等.......
所以,这就是你得到一个原因NullPointerException
,反序列化时,它...
任何申明场transient
不是序列。 此外,根据这一博客帖子 ,字段值甚至没有初始化,将通过默认构造函数来设置的值。 这将创建一个挑战时, transient
场final
。
根据所述序列化的Javadoc ,反串行化可以通过实施如下方法进行控制:
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException;
我想出了以下解决方案,基于这个优秀的StackOverflow的答案 :
import java.io.*;
import java.lang.reflect.*;
public class NullFinalTestFixed {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Foo foo = new Foo();
foo.useLock();
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
new ObjectOutputStream(buffer).writeObject(foo);
foo = (Foo) new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())).readObject();
foo.useLock();
}
public static class Foo implements Serializable {
private final String lockUsed = "lock used";
private transient final Object lock = new Object();
public void useLock() {
System.out.println("About to synchronize");
synchronized (lock) {
System.out.println(lockUsed);
}
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
initLocks(this, "lock");
}
}
public static void initLocks(Object obj, String... lockFields) {
for (String lockField: lockFields) {
try {
Field lock = obj.getClass().getDeclaredField(lockField);
setFinalFieldValue(obj, lock, new Object());
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
}
public static void setFinalFieldValue(Object obj, Field field, Object value) {
Exception ex;
try {
field.setAccessible(true);
Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(obj, value);
return;
} catch (IllegalAccessException e) {
ex = e;
} catch (NoSuchFieldException e) {
ex = e;
}
throw new RuntimeException(ex);
}
}
运行它产生以下输出(无NullPointerException
):
About to synchronize
lock used
About to synchronize
lock used
正如前面所指出的,下面的声明不起作用正如人们所预料:
transient final Object foo = new Object()
在transient
关键字将防止成员被序列化。 初始化一个默认值反序列化过程中不兑现,因此foo
将成为null
反序列化后。
在final
关键字会阻止你一旦被设定modifiying成员。 这意味着,你不得不拥有null
永远都在反序列化实例。
在任何情况下,你将需要删除final
关键字。 这会牺牲不可改变,但不应通常是一个问题private
成员。
然后,你有两个选择:
选项1:覆盖readObject()
transient Object foo = new Object();
@Override
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
foo = new Object();
}
当创建一个新的实例, foo
将被初始化为它的默认值。 反序列化时,您的自定义readObject()
方法将采取照顾。
这将工作在JRE而不是在Android,因为Android的实现Serializable
缺少readObject()
方法。
选项2:延迟初始化
宣言:
transient Object foo;
在访问:
if (foo == null)
foo = new Object();
doStuff(foo);
你必须要做到这一点无论在你的代码中访问foo
,这可能是更多的工作和容易出错比第一种办法,但它会在JRE和Android一样工作。