1.序列化与反序列化的概念
序列化:将对象写入到IO流中
反序列化:从IO流中恢复对象
Serializable接口是一个标记接口,不用实现任何方法,标记当前类对象是可以序列化的,是给JVM看的。
序列化机制允许将这些实现序列化接口的对象转化为字节序列,这些字节序列可以保证在磁盘上或者网络传输后恢复成原来的对象。序列化就是把对象存储在JVM以外的地方,序列化机制可以让对象脱离程序的运行而独立存在。
序列化在业务代码也许用的不多,但是在框架层面用的是很多的。
先给出序列化的例子。
package com.shi;
import java.io.Serializable;
/**
* 机动车
* @author shixiangcheng
* 2021-12-16
*/
public class Motor implements Serializable{
private String name;
public Motor(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Motor{name="+this.name+"}";
}
}
package com.shi;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
/**
* 测试类
* @author shixiangcheng
* 2021-12-16
*/
public class Test {
public static void main(String[] args) throws Exception {
//用于存储序列化的文件,这里的java_下划线仅仅为了说明java序列化对象,没有任何其它含义
File file=new File("D:/tmp/Motor.java_");
if(!file.exists()) {
//先得到文件的上级目录,并创建上级目录
file.getParentFile().mkdirs();
//再创建文件
file.createNewFile();
}
Motor m=new Motor("你好");
//创建一个输出流
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(file));
//输出可序列化对象
oos.writeObject(m);
oos.close();
//反序列化的步骤
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(file));
//得到反序列化对象
Object motor=ois.readObject();
ois.close();
System.out.println(motor);
}
}
运行之后,看到磁盘文件因为序列化而多了一个文件。
控制台中因反序列化输出的对象信息打印如下:
2.子类实现Serializable接口,父类没有实现,子类可以序列化吗?
去掉父类Motor的implements Serializable,让父类不实现序列化接口,子类Car实现序列化接口
package com.shi;
import java.io.Serializable;
/**
* 汽车
* @author shixiangcheng
* 2021-12-16
*/
public class Car extends Motor implements Serializable{
private String owner;
public Car(String owner,String name) {
super(name);
this.owner=owner;
}
}
package com.shi;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
/**
* 测试类
* @author shixiangcheng
* 2021-12-16
*/
public class Test {
public static void main(String[] args) throws Exception {
//用于存储序列化的文件,这里的java_下划线仅仅为了说明java序列化对象,没有任何其它含义
File file=new File("D:/tmp/Motor.java_");
if(!file.exists()) {
//先得到文件的上级目录,并创建上级目录
file.getParentFile().mkdirs();
//再创建文件
file.createNewFile();
}
Car m=new Car("小车","你好");
//创建一个输出流
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(file));
//输出可序列化对象
oos.writeObject(m);
oos.close();
//反序列化的步骤
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(file));
//得到反序列化对象
Object motor=ois.readObject();
ois.close();
System.out.println(motor);
}
}
运行结果
Exception in thread "main" java.io.InvalidClassException: com.shi.Car; no valid constructor
at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(Unknown Source)
at java.io.ObjectStreamClass.checkDeserialize(Unknown Source)
at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
at java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.readObject(Unknown Source)
at com.shi.Test.main(Test.java:34)
结果显示没有有效地构造器,原来是因为父类没有序列化的时候,Object motor=ois.readObject();需要直接调用父类的无参数构造方法,不经过子类的无参构造方法。
结果却发现打印的不是Car,而是父类Motor,因为子类没有实现toString而调用父类的toString,所以打印了Motor对象,至于父类成员变量id为什么是null,原因如下:
一个子类实现了 Serializable 接口,它的父类都没有实现 Serializable接口,序列化该子类对象。要想反序列化后输出父类定义的某变量的数值,就需要让父类也实现Serializable接口或者父类有默认的无参的构造函数。
在父类没有实现Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值,如果在父类无参构造函数中没有对变量赋值,那么父类成员变量值都是默认值,如这里的Long型就是null。
根据以上特性,我们可以将不需要被序列化的字段抽取出来放到父类中,子类实现 Serializable接口,父类不实现Serializable接口但提供一个空构造方法,则父类的字段数据将不被序列化。
总结:
子类实现Serializable接口,父类没有实现,子类可以序列化!!
这种情况父类一定要提供空构造方法,不要忘了子类的toString方法!
3.类中存在引用对象,这个类对象在什么情况下可以实现序列化?
来一个组合对象,里面引用Motor 对象,此时Motor 对象没有实现Serializable接口,能否序列化呢?
package com.shi;
import java.io.Serializable;
/**
* 组合
* @author shixiangcheng
* 2021-12-16
*/
public class Combo implements Serializable{
private Long id;
private Motor motor;
public Combo(Long id, Motor motor) {
super();
this.id = id;
this.motor = motor;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Motor getMotor() {
return motor;
}
public void setMotor(Motor motor) {
this.motor = motor;
}
@Override
public String toString() {
return "Combo{id="+this.id+",motor="+this.motor+"}";
}
}
package com.shi;
import java.io.*;
/**
* 测试类
* @author shixiangcheng
* 2021-12-16
*/
public class Test {
public static void main(String[] args) throws Exception {
//用于存储序列化的文件,这里的java_下划线仅仅为了说明java序列化对象,没有任何其它含义
File file=new File("D:/tmp/Motor.java_");
if(!file.exists()) {
//先得到文件的上级目录,并创建上级目录
file.getParentFile().mkdirs();
//再创建文件
file.createNewFile();
}
Combo m=new Combo(1L,new Motor("你好"));
//创建一个输出流
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(file));
//输出可序列化对象
oos.writeObject(m);
oos.close();
//反序列化的步骤
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(file));
//得到反序列化对象
Object motor=ois.readObject();
ois.close();
System.out.println(motor);
}
}
运行结果:直接爆出异常,说明Motor类没有序列化
Exception in thread "main" java.io.NotSerializableException: com.shi.Motor
at java.io.ObjectOutputStream.writeObject0(Unknown Source)
at java.io.ObjectOutputStream.defaultWriteFields(Unknown Source)
at java.io.ObjectOutputStream.writeSerialData(Unknown Source)
at java.io.ObjectOutputStream.writeOrdinaryObject(Unknown Source)
at java.io.ObjectOutputStream.writeObject0(Unknown Source)
at java.io.ObjectOutputStream.writeObject(Unknown Source)
at com.shi.Test.main(Test.java:28)
总结:
一个类里面所有的属性必须是可序列化的,这个类才能顺利的序列化。比如,类中存在引用对象,那么这个引用对象必须是可序列化的,这个类才能序列化。
4.同一个对象多次序列化之间有属性更新,前后的序列化有什么区别?
下面例子中Motor是可序列化的,每次序列化之前都会把Motor的name值修改了,用来观察看看,多次序列化期间,如果对象属性更新,是否会影响序列化,反序列化有什么区别。
package com.shi;
import java.io.*;
/**
* 测试类
* @author shixiangcheng
* 2021-12-16
*/
public class Test {
public static void main(String[] args) throws Exception {
//用于存储序列化的文件,这里的java_下划线仅仅为了说明java序列化对象,没有任何其它含义
File file=new File("D:/tmp/Motor.java_");
if(!file.exists()) {
//先得到文件的上级目录,并创建上级目录
file.getParentFile().mkdirs();
//再创建文件
file.createNewFile();
}
Motor m=new Motor("A");
//创建一个输出流
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(file));
//未序列化,先修改属性
m.setName("B");
//输出可序列化对象
oos.writeObject(m);
//序列化一次后,再次修改属性
m.setName("C");
oos.writeObject(m);
//序列化两次后,再次修改属性
m.setName("D");
oos.writeObject(m);
oos.close();
//反序列化的步骤
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(file));
//得到反序列化对象
Object m1=ois.readObject();
Object m2=ois.readObject();
Object m3=ois.readObject();
ois.close();
System.out.println(m1);
System.out.println(m2);
System.out.println(m3);
}
}
运行结果
Motor{name=B}
Motor{name=B}
Motor{name=B}
结果发现反序列化读出的值都是一样的。说明当对象第一次序列化成功后,后续这个对象属性即使有修改,也不会对后面的序列化造成成影响。
这其实是序列化算法的原因,所有要序列化的对象都有一个序列化的编码号,当试图序列化一个对象,会检查这个对象是否已经序列化过,若从未序列化过,才会序列化为字节序列去输出。若已经序列化过,则会输出一个编码符号,不会重复序列化一个对象。如下
序列化一次后,后续继续序列化并未重复转换为字节序列,而是输出字符q~
总结:
当第一次序列化之后,不管如何修改这个对象的属性,都不会对后续的序列化产生影响,反序列化的结果都和第一次相同。
欢迎大家积极留言交流学习心得,点赞的人最美丽!