文章目录
一、 序列化的含义、意义、使用场景
- 序列化:将堆内存中对象持久化,写入IO流中
- 反序列化:从IO流中恢复对象
- 意义:序列化机制允许将实现序列化的对象转换为字节序列,这些字节序列可以保存在磁盘上,还可以通过网络传输,以达到以后可以恢复为原始对象的目的。序列化机制使得对象可以脱离程序的允许而独立存在
- 使用场景:
- 所有在网络上传输的对象都必须是可序列化的。比如RMI(Remote Method Invoke)远程方法调用,传入的参数或返回的对象都必须要是可序列化的。
- 所有需要保存在磁盘上的Java对象都必须要是可序列化的
- 使用建议:程序创建的每个JavaBean类都实现
Serializable
接口
二、序列化的实现方式
两个相关的重要的接口
Serializable
和Externalizable
1. Serializable
1.1 普通序列化和反序列化
- 前提条件:首先需要一个类实现接口
Serializable
- 序列化步骤:
- 创建一个
ObjectOutputStream
输出流 - 调用
ObjectOutputStream
输出流对象的writeObject
方法输出可序列化对象
- 创建一个
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" + "name=" + name + '\'' + ",age = " + age + "}";
}
}
- 序列化代码:
public class WriteObject {
public static void main(String[] args) {
try {
//0. 创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
//1. 将对象序列化到文件s
Person person = new Person("xiaozhang", 12);
oos.writeObject(person);
}catch (Exception e){
e.printStackTrace();
}
}
}
-
运行结果:在项目的根目录下生成了序列化文件
-
反序列化步骤:
- 创建一个
ObjectInputStream
输入流对象,传入指定的序列化后的文件 - 调用
ObjectInputStream
对象的readObject()
方法得到序列化后的对象
- 创建一个
public class ReadObject {
public static void main(String[] args) {
try {
//0. 创建ObjectInputStream
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
Person person = (Person) ois.readObject();
System.out.println(person);
}catch (Exception e){
e.printStackTrace();
}
}
}
-
输出结果
-
问题:反序列化的过程,对象是如何创建的?是由对应的类的构造方法创建的吗?
- 通过在类的构造方法添加一行输出,再次运行上述代码来验证一下
- 通过在类的构造方法添加一行输出,再次运行上述代码来验证一下
-
输出结果
-
发现结果并没有通过构造函数。
-
反序列化并不会调用构造方法,反序列化的对象是由JVM 自己生成的对象,不会通过构造方法
1.2 成员是引用的序列化
如果一个实现了Serializable
接口的类成员不仅包含了基本数据类型和String类型,也包含了引用类型,那么这个引用类型对应的类也必须是可序列化的,否则会导致该类无法序列化
实验:新增Teacher
类,去除Person
类的Serializable
接口
person
public class Person {
private String name;
private int age;
public Person(String name, int age){
System.out.println("反序列化过程,构造方法被调用了吗?");
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" + "name=" + name + '\'' + ",age = " + age + "}";
}
}
teacher
public class Teacher implements Serializable {
String name;
Person person;
public Teacher(String name, Person person){
this.name = name;
this.person = person;
}
}
WriteObject
public class WriteObject {
public static void main(String[] args) {
try {
//0. 创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
//1. 将对象序列化到文件
Teacher teacher = new Teacher(1, new Person("zhangsan",11));
oos.writeObject(teacher);
}catch (Exception e){
e.printStackTrace();
}
}
}
-
输出
-
Person
不可序列化导致Teacher
不可序列化
1.3 同一对象多次序列化的机制
-
同一对象序列化多次,会将这个对象序列化多次吗?
- 不会
-
将上述代码中的
Person
实现序列化接口Serializable
-
然后分别先进行序列化
public class WriteObject {
public static void main(String[] args) {
try {
//0. 创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"));
//1. 将对象序列化到文件
Person person = new Person("路飞", 20);
Teacher t1 = new Teacher("雷利", person);
Teacher t2 = new Teacher("红发香克斯", person);
//2.依此将上述对象写入输出流
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(person);
oos.writeObject(t2);
}catch (Exception e){
e.printStackTrace();
}
}
}
- 然后进行反序列化
public class ReadObject {
public static void main(String[] args) {
try {
//0. 创建ObjectInputStream
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("teacher.txt"));
//1. 反序列化的顺序与序列化的顺序保持一致
Teacher t1 = (Teacher) ois.readObject();
Teacher t2 = (Teacher) ois.readObject();
Person person = (Person) ois.readObject();
Teacher t3 = (Teacher) ois.readObject();
System.out.println(t1 == t2);
System.out.println(t1.getPerson() == person);
System.out.println(t2.getPerson() == person);
System.out.println(t2 == t3);
System.out.println(t1.getPerson() == t2.getPerson());
}catch (Exception e){
e.printStackTrace();
}
}
}
-
反序列化输出
-
由此可以更深入的理解最开始的问题了,Java序列化同一对象的时候,并不是多次序列化操作就能得到多个对象,而是不管序列化多少次,最终只只会得到一个对象
1.4 Java序列化算法要点
- 所有保存到磁盘的对象都有一个序列化编码号
- 当尝试对一个对象进行序列化操作前,会检查该对象是否以及被序列化过,只有该对象从未被序列化过,才会将对象序列化为字节序列。
- 如果该对象已经序列化了,则直接输出编号即可。
- 上述序列化过程的图解
1.5 Java序列化算法存在的问题
- 由于
Java
序列化算法不会重复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象,当该对象的内容被改变时,再次序列化,并不会将内容改变后的对象转换为字节序列
public class WriteObject {
public static void main(String[] args) {
try {
//0. 创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"));
ObjectInputStream ios = new ObjectInputStream(new FileInputStream("teacher.txt"));
//1. 第一次序列化Person
Person person = new Person("张三", 11);
oos.writeObject(person);
System.out.println(person);
//2. 修改对象
person.setName("李四");
System.out.println(person);
//3. 第二次序列化
oos.writeObject(person);
//4. 依此反序列化
Person p1 = (Person) ios.readObject();
Person p2 = (Person) ios.readObject();
System.out.println(p1 == p2);
System.out.println(p1.getName());
System.out.println(p2.getName());
System.out.println(p1.getName() == p2.getName());
}catch (Exception e){
e.printStackTrace();
}
}
}
- 输出:
1.6 可选的自定义序列化
1.6.1 transient
有些时候,我们有这样的需求,某些属性不需要序列化。使用transient关键字选择不需要序列化的字段。
- 下面的实验将说明这个问题
Person类
public class Person implements Serializable {
private transient String name;
private transient int age;
private int height;
private transient boolean singlehood;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public boolean isSinglehood() {
return singlehood;
}
public void setSinglehood(boolean singlehood) {
this.singlehood = singlehood;
}
public Person(String name, int age){
this.name = name;
this.age = age;
}
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", height=" + height +
", singlehood=" + singlehood +
'}';
}
}
测试类
public class TransientTest {
public static void main(String[] args) {
try {
//0. 新建输入流和输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.txt"));
Person person = new Person("张三", 11);
person.setHeight(111);
person.setSinglehood(true);
System.out.println("序列化前:"+ person);
oos.writeObject(person);
Person person1 = (Person)ois.readObject();
System.out.println("反序列化结果:" + person1);
}catch (Exception e){
e.printStackTrace();
}
}
}
- 输出
1.6.2 可选的自定义序列化(暂时没懂)
使用transient
虽然简单,但是将此属性完全隔离在了序列化之外,非常的不灵活。Java提供了可选的自定义序列化,可以控制序列化的方式,或者为序列化数据进行编码加密等。
- 一般通过重写下面的方法来实现
private void writeObject(java.io.ObjectOutputStream out) throws IOException;
private void readObject(java.io.ObjectIutputStream in) throws IOException,ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
Person
中定义私有的writeObject()
和readObject()
方法
private String name;
private int age;
private int height;
private boolean singlehood;
public void writeObject(ObjectOutputStream out) throws IOException{
//0. 将name属性反转写入二进制流
out.writeObject(new StringBuffer(this.name).reverse());
out.writeInt(age);
System.out.println("自定义的writeObject是否被调用");
}
public void readObject(ObjectInputStream ins) throws IOException, ClassNotFoundException {
//1. 将读出的字符串反转恢复出来
this.name = ((StringBuffer)ins.readObject()).reverse().toString();
this.age = ins.readInt();
System.out.println("自定义的readObject是否被调用");
}
- 然后再对这个类的对象进行序列化和反序列化,但是不知道为什么,并没有调用自定义的相关方法
1.6.3 更彻底的自定义序列化
- 两个重要方法
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
writeReplace()
:在序列化时,会优先调用此方法, 在调用writeObject()
方法。此方法可以将任意对象代替位目标对象
public class Person implements Serializable {
private String name;
private int age;
private Object writeReplace() throws ObjectStreamException{
ArrayList<Object> list = new ArrayList<>();
list.add(this.name);
list.add(this.age);
return list;
}
- 测试
public class Test {
public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.txt"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.txt"));
Person person = new Person("xiaozhang", 11);
System.out.println("序列化之前:" + person);
oos.writeObject(person);
ArrayList list = (ArrayList)ois.readObject();
System.out.println("反序列化之后:" + list);
}catch (Exception e){
e.printStackTrace();
}
}
}
-
输出
-
可见序列化的时候,通过
writeReplace()
方法,修改了序列化后的对象 -
readResolve()
:反序列化时替换反序列化的对象,反序列化出来的对象被丢弃,在readObject()
后被调用
public class Person implements Serializable {
private String name;
private int age;
public Object readResolve() throws Exception{
return new Person("xiaoming",11);
}
- 测试
public class Test {
public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.txt"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.txt"));
Person person = new Person("xiaozhang", 11);
System.out.println("序列化之前:" + person);
oos.writeObject(person);
Person person1 = (Person) ois.readObject();
System.out.println("反序列化之后:" + person1);
}catch (Exception e){
e.printStackTrace();
}
}
}
- 输出
2. Externalizable:强制自定义序列化
- 通过实现
Externalizable
接口,必须实现writeExternal
、readExternal
方法
public class ExPerson implements Externalizable {
private String name;
private int age;
//0. 注意按此种方式实现序列化,必须添加无参的Public的构造器
public ExPerson(){};
public ExPerson(String name, int age){
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
//1. 将name反转后写入二进制流
StringBuffer reverse = new StringBuffer(name).reverse();
System.out.println("写入时反转name:" + reverse.toString());
out.writeObject(reverse);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
//2.读出字符串的时候也需要反转
this.name = ((StringBuffer) in.readObject()).reverse().toString();
System.out.println("读出时再次反转:" + name);
this.age = in.readInt();
}
@Override
public String toString() {
return "ExPerson{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Test {
public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.txt"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.txt"));
ExPerson exPerson = new ExPerson("xiaozhang", 111);
System.out.println("序列化之前:" + exPerson);
oos.writeObject(exPerson);
ExPerson ep = (ExPerson) ois.readObject();
System.out.println("序列化之后:"+ ep);
}catch (Exception e){
e.printStackTrace();
}
}
}
- 输出:
2.1要点
- 必须实现接口
Externalizable
的两个方法。writeExternal
,readExternal
- 必须提供
public
的空参构造方法,这是因为在反序列化的时候需要反射的创建对象
3. 两种序列化对比
三、序列化版本号:serialVersionUID
反序列化必须有class
文件,但是随着项目升级,class
文件也会升级,序列化怎么保证不同版本之间的兼容性呢?
- Java提供了一个
private static final long serialVersionUID
的序列化版本号。只要版本号相同,即使更改了序列属性,对象也可以被正确的序列化回来。
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
-
如果反序列化使用的
class版本号
与序列化时使用的不一致,反序列化会报InvalidClassException
异常 -
序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;
-
不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化
需要修改serialVersionUID的三种情况
- 如果只是修好方法,反序列化不容易影响,则无需修改
- 如果只是修改了静态变量、瞬时变量(
transient
修饰的变量),反序列化不受影响- 静态变量也不会被序列化,但是反序列化的时候可以正常的赋值,因为静态变量属于类,直接从类中获取即可
- 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。
四、总结
- 有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现
Serializable
接口。 - 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、
transient
实例变量都不会被序列化。 - 如果想让某个变量不被序列化,使用
transient
修饰。 - 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
反序列化时必须有序列化对象的class
文件。 - 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
- 单例类序列化,需要重写
readResolve()
方法;否则会破坏单例原则。 - 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
- 建议所有可序列化的类加上
serialVersionUID
版本号,方便项目升级。