计算机系统应用教程网站

网站首页 > 技术文章 正文

Java中使用Serializable注意看这个避坑指南!

btikc 2024-09-11 01:59:53 技术文章 13 ℃ 0 评论


一. 父类未支持序列化,子类需要支持序列化,有2个坑!!!

首先我们定义大略如下结构:

static class VoTest implements Serializable {
private int num;
private String name;
private Integer use;
private FieldX fieldX;
public VoTest(int num, String name, Integer use, FieldX fieldX) {
...
}
public VoTest(int num, String name, Integer use) {
...
}
@Override
public String toString() {
return new StringJoiner(", ", VoTest.class.getSimpleName() + "[", "]").add("num=" + num).add("name='" + name + "'").add("use=" + use).add("fieldX=" + fieldX).toString();
}
}
static class FieldX extends FieldParentX implements Serializable{
private String name;
private String value;
public FieldX(int parent) {
super(parent);
}
public FieldX(String name, String value) {
super(-1);
...
}
@Override
public String toString() {
return new StringJoiner(", ", FieldX.class.getSimpleName() + "[", "]").add("name='" + name + "'").add("value='" + value + "'").add("parent=" + parent).toString();
}
}
static class FieldParentX{
protected Integer parent;
public FieldParentX(Integer parent) {
this.parent = parent;
}
@Override
public String toString() {
return new StringJoiner(", ", FieldParentX.class.getSimpleName() + "[", "]").add("parent=" + parent).toString();
}
}


其结构关系图大略如下:

现在我们先来演示第一个坑!

我们来书写如下测试方法:

VoTest vo2 = new VoTest(123,"Test",100);
System.out.println("vo2 = " + vo2);
try(ObjectOutputStream serializable=new ObjectOutputStream(new FileOutputStream("G://vo2.txt"))) {
serializable.writeObject(vo2);
}
System.out.print("vo2-read = ");
try(ObjectInputStream read= new ObjectInputStream(new FileInputStream("G://vo2.txt"))) {
vo2 = (VoTest)read.readObject();
System.out.print(vo2);
}
// 输出如下:
// vo2 = VoTest[num=123, name='Test', use=100, fieldX=null]
// vo2-read = VoTest[num=123, name='Test', use=100, fieldX=null]
VoTest vo3 = new VoTest(345,"Test2",100,new FieldX("F2","V2"));
System.out.println("vo3 = "+vo3);
try(ObjectOutputStream serializable=new ObjectOutputStream(new FileOutputStream("G://vo3.txt"))) {
serializable.writeObject(vo3);
}
System.out.print("vo3-read = ");
try(ObjectInputStream read= new ObjectInputStream(new FileInputStream("G://vo3.txt"))) {
vo3 = (VoTest)read.readObject();
System.out.print(vo3);
}
// 输出如下:
// vo3 = VoTest[num=345, name='Test2', use=100, fieldX=FieldX[name='F2', value='V2', parent=-1]]
// vo3-read =
// java.io.InvalidClassException: com.cargo.boot.test.SerTest$FieldX; no valid constructor

有没有疑惑为什么vo3的反序列化失败了?vo3只是比vo2多了fieldX属性的填充,怎么就序列化失败了呢?

这里的fieldX属性的特殊之处就在于它有一个不支持序列化的父类:FieldParentX,那么可能小伙伴们就有疑问了,难道说父类没有支持序列化,子类序列化就是伪命题了吗?明显不是的。

我们找到Serializable接口的注释部分,有如下描述:

To allow subtypes of non-serializable classes to be serialized, the subtype may assume responsibility for saving and restoring the state of the supertype's public, protected, and (if accessible) package fields. The subtype may assume this responsibility only if the class it extends has an accessible no-arg constructor to initialize the class's state. It is an error to declare a class Serializable if this is not the case. The error will be detected at runtime.

During deserialization, the fields of non-serializable classes will be initialized using the public or protected no-arg constructor of the class. A no-arg constructor must be accessible to the subclass that is serializable. The fields of serializable subclasses will be restored from the stream.

大白话描述就是说呢:

在上述情况下想实现子类正常序列化和反序列化,首先为了保证程序不报错,父类必须有一个可访问的空参构造器!!!

这是第一个坑,大家注意了.在反序列化的时候会用这个空参构造器对父类状态进行初始化.

所以我们在上述定义的类: FieldParentX 添加空参构造器后再尝试测试代码,即可发现运行无碍了。

// 向类FieldParentX添加空参构造器
public FieldParentX() {}
// vo3再次测试输出如下:
// vo3 = VoTest[num=345, name='Test2', use=100, fieldX=FieldX[name='F2', value='V2', parent=-1]]
// vo3-read = VoTest[num=345, name='Test2', use=100, fieldX=FieldX[name='F2', value='V2', parent=null]]


有细心的小伙伴可能已经发现了,此时的fieldX中的parent属性怎么反序列化之后变成null了呢?

我们继续大白话说注释:

子类在序列化反序列化的时候要负责存储和回填父类中可访问的属性,比如上面的parent属性,那么怎么让子类承担对父类中这些属性的序列化和反序列化呢?

这里大致有两个方向来解决这个问题,我们先说最基本也是最简单的方式,那就是我们将父类中的需要序列化和反序列化的属性在子类中也存储一份,这样就避免了属性丢失问题,比如上面的fieldX属性对应的类FieldX我们就可以扩展如下:

static class FieldX extends FieldParentX implements Serializable{
private String name;
private String value;
private Integer parentValue;
public FieldX(String name, String value) {
super(-1);
...
}
public void setParentValue(Integer parentValue) {
this.parentValue = parentValue;
this.parent=parentValue;
}
@Override
public String toString() {
return new StringJoiner(", ", FieldX.class.getSimpleName() + "[", "]").add("name='" + name + "'").add("value='" + value + "'").add("parent=" + parent).toString();
}
}

测试代码如下:

VoTest vo3 = new VoTest(345,"Test2",100,new FieldX("F2","V2"));
vo3.fieldX.setParentValue(300);
System.out.println("vo3 = "+vo3);
try(ObjectOutputStream serializable=new ObjectOutputStream(new FileOutputStream("G://vo3.txt"))) {
serializable.writeObject(vo3);
}
System.out.print("vo3-read = ");
try(ObjectInputStream read= new ObjectInputStream(new FileInputStream("G://vo3.txt"))) {
vo3 = (VoTest)read.readObject();
vo3.fieldX.setParentValue(vo3.fieldX.parentValue);
System.out.print(vo3);
}
// vo3测试输出如下:
// vo3 = VoTest[num=345, name='Test2', use=100, fieldX=FieldX[name='F2', value='V2', parent=300]]
// vo3-read = VoTest[num=345, name='Test2', use=100, fieldX=FieldX[name='F2', value='V2', parent=300]]

另一个更专业的方式则牵扯到Serializable使用时的隐含方法定义了,我们后期再细讲:

private void writeObject(java.io.ObjectOutputStream out) throws IOException
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;

二. 一个类支持序列化后,这个类里面的所有实例属性都要支持序列化,这个可能对很多最开始接触Serializable接口的小伙伴来说,算一个不大不小的坑!!!

那么,如果说我们的类中就是有一些属性不用去序列化和反序列化或者说这些属性就是不能支持序列化,那要怎么处理呢?

现在就轮到修改符: transient 出场了.序列化的对象包含被 transient 修饰的实例变量时,java 虚拟机(JVM)会跳过该特定的变量。

像上面的类定义VoTest中如果说我们不想fieldX变量参与序列化,那我们就可以修改其定义如下:

private transient FieldX fieldX;

其序列化的时候就会忽略fieldX属性,我们会看到反序列化后的结果将变成

vo3-read = VoTest[num=345, name='Test2', use=100, fieldX=null] // fieldX属性没有被持久化.


三. 一个对象被序列化存储后,类结构定义发生了变化,不能被反序列化读取了!!! 这里有一个容易被忽略的坑!!!

可能很多小伙伴都在很多实现接口Serializable的类中都看到过这样一个变量:

private static final long serialVersionUID = 1L;

我们看一下Serializable接口中对这个变量的说明:

The serialization runtime associates with each serializable class a version number, called a serialVersionUID, which is used during deserialization to verify that the sender and receiver of a serialized object have loaded classes for that object that are compatible with respect to serialization. If the receiver has loaded a class for the object that has a different serialVersionUID than that of the corresponding sender's class, then deserialization will result in an InvalidClassException. A serializable class can declare its own serialVersionUID explicitly by declaring a field named "serialVersionUID" that must be static, final, and of type long:

大白话描述就是说呢:

对象序列化时都会关联一个类似于版本号的东西,就是这里的serialVersionUID变量,这个变量必须是(静态的,final的,long类型的)! 在反序列化时如果说序列化结果中的版本号和当前类的版本号不匹配,则会抛出异常(InvalidClassException)。那么如果说我们的类定义的时候忽略了这个属性的定义,就很大概率会发生标题所说的坑! 序列化结果无法被反序列化读取。可能不清楚的小伙伴就会问了,既然我没有定义,又怎么来的匹配不匹配呢?

好,我们再来看一段描述:

If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification. However, it is strongly recommended that all serializable classes explicitly declare serialVersionUID values, since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, and can thus result in unexpected InvalidClassExceptions during deserialization. Therefore, to guarantee a consistent serialVersionUID value across different java compiler implementations, a serializable class must declare an explicit serialVersionUID value. It is also strongly advised that explicit serialVersionUID declarations use the private modifier where possible, since such declarations apply only to the immediately declaring class--serialVersionUID fields are not useful as inherited members. Array classes cannot declare an explicit serialVersionUID, so they always have the default computed value, but the requirement for matching serialVersionUID values is waived for array classes.


大白话描述继续:

如果可序列化的类没有显式声明serialVersionUID,序列化运行时会根据该类的各方面信息来计算得到一个默认的serialVersionUID值,就像Java(TM)规范中描述的那样。但是问题来了,这个计算规则呢,对类的细节极其敏感,这些细节也可能因为编译器实现上的差异而产生差异.所以为了保证不同的Java编译器都能正常的对类进行序列化和反序列化,强烈建议(要求)在可序列化类中一定要显式声明serialVersionUID值,虽然Java本身没有限制serialVersionUID属性的访问级别,但是为了避免一些隐藏的问题,同时强烈建议对其使用私有修饰符(private)。

同时呢,这里有个特例,就是对于数组类型的变量是不存在声明serialVersionUID值的,它们总是使用默认的计算值,对于数组本身来说不需要匹配serialVersionUID值.

现在大家明白前情后事了吧,所以大家在将类可序列化的时候一定要记得定义serialVersionUID值,现在很多编辑器本身也会很好地支持自动生成这些代码。

今天就说到这里了,我们下期重点说一下“那三个神秘的方法”。


Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表