【问题标题】:A transient final field used as a lock is null用作锁的瞬态最终字段为空
【发布时间】:2012-09-01 17:06:55
【问题描述】:

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

【问题讨论】:

  • @nicholas.hauschild 不仅允许而且鼓励自答问题。

标签: java serialization nullpointerexception final transient


【解决方案1】:

如前所述,下面的声明并不像预期的那样起作用:

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。

【讨论】:

    【解决方案2】:

    声明为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
    

    【讨论】:

    • 您引用的博客文章根本没有说明默认值,更不用说您在上面声明的内容了。这甚至没有意义:它们会被初始化成什么else?您的代码解决方案也过于复杂:您不需要对此进行反思。
    • 博客文章说,“声明为 final 的实例成员字段也可能是临时的,但如果是这样,您将面临一个难以解决的问题......当您反序列化您的对象时将不得不手动初始化该字段,[但编译器抱怨因为它是最终的]...现在,当您反序列化该类时,您的记录器将是一个空对象,因为它是瞬态的。”这是我的答案解决的问题。如果没有反思,you 如何建议我们设置final 字段的值?你的评论也太刻薄了:你不需要为此开玩笑。
    • 博客没有说出你所说的。时期。我的评论也没有说出您的主张:其余部分可以简化为“过度”一词,这几乎不是“刻薄的”或“刻薄的”。这让我很困惑。
    • 抱歉,听起来我们对“默认”这个重载的词有误解。在我上面的帖子中,我指的是在类定义中设置的值,即在构造之前设置的值。
    • 请注意,Serializable 的 Android 实现缺少 readObject() 方法,因此该解决方案仅适用于 JRE(和完全兼容的实现),但无法在 Android 上编译。
    【解决方案3】:

    A transient final field used as a lock is null

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

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

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

    例如:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-04-22
      • 1970-01-01
      • 2023-03-10
      • 1970-01-01
      • 1970-01-01
      • 2020-07-20
      • 2011-06-01
      • 2010-10-28
      相关资源
      最近更新 更多