以下代码抛出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
怎么可能为空?
答案 0 :(得分:14)
A transient final field used as a lock is null
以下是关于瞬态变量的一些事实:
- 在实例变量上使用瞬态关键字时,会阻止该序列化实例变量。
- 在反序列化时,瞬态变量达到默认值 .....
<强>例如强>
null
0
false,
等....... 这就是你在反序列化时得到NullPointerException
的原因......
答案 1 :(得分:4)
声明为transient
的任何字段都未序列化。此外,根据this blog post,字段值甚至不会初始化为默认构造函数设置的值。当transient
字段为final
时,这会带来挑战。
根据the Serializable javadoc,可以通过实施以下方法来控制反序列化:
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException;
我提出了以下解决方案,基于this excellent StackOverflow answer:
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
答案 2 :(得分:0)
如前所述,下面的声明并不像人们预期的那样有效:
transient final Object foo = new Object()
transient
关键字将阻止该成员被序列化。 反序列化期间不会使用默认值进行初始化,因此反序列化后foo
将为null
。
final
关键字会阻止您在成员设置后修改该成员。这意味着您将永远停留在反序列化实例上的null
。
在任何情况下,您都需要删除final
关键字。这将牺牲不变性,但通常不应成为private
成员的问题。
然后你有两个选择:
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()
方法。
声明:
transient Object foo;
访问时间:
if (foo == null)
foo = new Object();
doStuff(foo);
您必须在代码中的任何地方执行此操作foo
,这可能比第一个选项更有效,更容易出错,但它可以在JRE和Android上运行。