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

时间:2012-09-07 19:47:57

标签: java serialization nullpointerexception final transient

以下代码抛出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怎么可能为空?

3 个答案:

答案 0 :(得分:14)

A transient final field used as a lock is null

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

- 在实例变量上使用瞬态关键字时,会阻止该序列化实例变量。

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

<强>例如

  • 对象引用变量null
  • int to 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成员的问题。

然后你有两个选择:

选项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上运行。