用作锁的瞬态最终字段为空

以下代码抛出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

以下是关于瞬态变量的一些事实:

在实例变量上使用Transient关键字时,将阻止该实例变量被序列化。

在反序列化时,瞬态变量达到其默认值 …..

例如:

  • 对象引用变量为null
  • int为0
  • boolean to false,等等…….

这就是你在反序列化时获得NullPointerException的原因……

声明为transient任何字段都不是序列化的。 此外,根据此博客文章 ,字段值甚至不会初始化为默认构造函数设置的值。 当transient场是final时,这会产生挑战。

根据Serializable 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关键字将阻止您在成员设置后修改该成员。 这意味着你在反序列化的实例上永远处于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。