6

下面的代码抛出一个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可能是空的?

4

3 回答 3

14

A transient final field used as a lock is null

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

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

-在反序列化时,瞬态变量得到它们的默认值.....

例如:

  • 对象引用变量null
  • 0
  • 布尔值false,等......

所以这就是你NullPointerException在反序列化它时得到一个的原因......

于 2012-09-07T20:03:38.630 回答
4

声明的任何字段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
于 2012-09-07T19:47:57.387 回答
2

如前所述,下面的声明并不像人们预期的那样工作:

transient final Object foo = new Object()

transient关键字将阻止成员被序列化。反序列化期间不支持使用默认值初始化,因此foonull在反序列化之后进行。

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。

于 2016-06-12T15:54:44.027 回答