一、写在开头

在上一篇学习序列化的文章中我们提出了这样的一个问题:

“如果在我的对象中,有些变量并不想被序列化应该怎么办呢?”

当时给的回答是:不想被序列化的变量我们可以使用transientstatic关键字修饰;transient 关键字的作用是阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复;而static关键字修饰的变量并不属于对象本身,所以也同样不会被序列化!

当时没有解释具体为什么static和transient 关键字修饰的变量就不能被序列化了,这个问题实际上在很多大厂的面试中都可能会被问及。我们今天在这篇中进行解释吧。

二、案例演示

我们先通过一个实战案例,去看一看用static和transient 关键字修饰后的变量,序列化与反序列化后的现象。

public class TestService {
    public static void main(String[] args) throws IOException {
        //初始化对象信息
        Person person = new Person();
        person.setName("JavaBuild");
        person.setAge(30);
        System.out.println(person.getName()+" "+person.getAge());

        //序列化过程
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:\\person.txt"));) {
            objectOutputStream.writeObject(person);
        } catch (IOException e) {
            e.printStackTrace();
        }
        person.par1 = "序列化后静态字段";
        //反序列化过程
        try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("E:\\person.txt"));) {
            Person p = (Person) objectInputStream.readObject();
            System.out.println(p);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}
class Person implements Serializable{

    private static final long serialVersionUID = 8711922740433840551L;
    private String name;
    private int age;

    public static String par1 = "静态字段";
    transient String par2 = "临时字段";
    transient int high = 175;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", par1=" + par1 +
                ", high=" + high +
                ", par2='" + par2 + '\'' +
                '}';
    }
}

在Person类中,我们定义了两个正常的属性,姓名与年龄,同时呢,我们也分别定义了一个静态字段和两个临时字段,输出结果为:

JavaBuild 30
Person{name='JavaBuild', age=30, par1=序列化后静态字段, high=0, par2='null'}

对于使用static关键字修饰的par1来说,在整个序列化过程中,它并未参与,原因是:我们在序列化与反序列化之间插入了属性的重新赋值操作,最后输出中打印出的是最新赋值,说明仅是调用了实例对象的属性值,而不是反序列化的结果。

而对于transient 关键字修饰high和par2,在序列化时直接被忽略了。从输出结果看就更加的明了了,int类型直接还原为默认值0,而String类型直接为null。

什么原因呢?咱们继续往下看。

三、源码分析

在之前的文章中,我们已经解释过了,在序列化时Serializable只是作为一种标识接口,告诉程序我这个对象需要序列化,那么真正的实现还要以来序列化流,比如写出到文件时,我们需要用到的ObjectOutputStream,它在序列化的时候会依次调用 writeObject()→writeObject0()→writeOrdinaryObject()→writeSerialData()→invokeWriteObject()→defaultWriteFields()。

然后最后一步的defaultWriteFields()方法中,会去调用ObjectStreamClass对象,里面有个方法为getDefaultSerialFields(),提供了可以被序列化的属性值。

private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
    // 获取该类中声明的所有字段
    Field[] clFields = cl.getDeclaredFields();
    ArrayList<ObjectStreamField> list = new ArrayList<>();
    int mask = Modifier.STATIC | Modifier.TRANSIENT;

    // 遍历所有字段,将非 static 和 transient 的字段添加到 list 中
    for (int i = 0; i < clFields.length; i++) {
        Field field = clFields[i];
        int mods = field.getModifiers();
        if ((mods & mask) == 0) {
            // 根据字段名、字段类型和字段是否可序列化创建一个 ObjectStreamField 对象
            ObjectStreamField osf = new ObjectStreamField(field.getName(), field.getType(), !Serializable.class.isAssignableFrom(cl));
            list.add(osf);
        }
    }

    int size = list.size();
    // 如果 list 为空,则返回一个空的 ObjectStreamField 数组,否则将 list 转换为 ObjectStreamField 数组并返回
    return (size == 0) ? NO_FIELDS :
        list.toArray(new ObjectStreamField[size]);
}

这段源码中,定义一个mask标记变量,用于接收访问修饰符中包含STATIC与TRANSIENT的属性,并在后面的if判断中,将这种mask的过滤掉,从而实现遍历所有字段,将非 static 和 transient 的字段添加到 list 中。

而这段源码就证明了,为什么在对象序列化过程中,static和transient不会被序列化!

四、总结

好啦,今天针对为什么static和transient关键字修饰的变量不能被序列化进行了一个解释,下次大家在面试的时候再被问道就可以这样回答啦,不过,还有的BT面试官会问transient关键字修饰的变量真的不能被序列化吗?这个问题咱们后面继续讨论哈。

posted on 2024-06-23 17:48  JavaBuild  阅读(382)  评论(0编辑  收藏  举报