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

Posted

技术标签:

【中文标题】用作锁的瞬态最终字段为空【英文标题】: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 不仅允许而且鼓励自答问题。 【参考方案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 *** 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 的原因...

【讨论】:

以上是关于用作锁的瞬态最终字段为空的主要内容,如果未能解决你的问题,请参考以下文章

JPA 的瞬态关键字未按预期工作

判断字符串为空为 null 为 whitespace 工具类

linux字符测试

对象引用未保存的瞬态实例 在刷新错误之前保存瞬态实例

HIbernate - 对象引用未保存的瞬态实例 - 在刷新之前保存瞬态实例

Grails Gorm:Object引用未保存的瞬态实例