用作锁定瞬态最后字段为空(A transient final field used as a loc

2019-06-28 00:24发布

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

Answer 1:

A transient final field used as a lock is null

以下是有关瞬态变量几个事实:

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

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

例如:

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

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



Answer 2:

任何申明场transient不是序列。 此外,根据这一博客帖子 ,字段值甚至没有初始化,将通过默认构造函数来设置的值。 这将创建一个挑战时, transientfinal

根据所述序列化的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


Answer 3:

正如前面所指出的,下面的声明不起作用正如人们所预料:

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一样工作。



文章来源: A transient final field used as a lock is null