一、写在开头
在上一篇学习序列化的文章中我们提出了这样的一个问题:
“如果在我的对象中,有些变量并不想被序列化应该怎么办呢?”
当时给的回答是:不想被序列化的变量我们可以使用transient
或static
关键字修饰;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关键字修饰的变量真的不能被序列化吗?这个问题咱们后面继续讨论哈。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步