「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」
1.什么是java序列化和反序列化
序列化:将java对象转换为字节序列
反序列化:将字节序列转换为java对象
为什么要做序列化:序列化对象,可以使得对象可以保存在磁盘中或者可以通过网络进行传输,对象不必依赖程序而存在,反序列化对象可以将字节序列转换成原来的对象。
2.序列化方式实现
-
Serializable接口
如果你想要将对象保存到文件中或者通过网络传输,那么该对象的类就要实现Serializable接口
实践:
创建一个java类Student:实现了Serializable接口,
public class Student implements Serializable {
private String name;
private int age;
//省略getter setter 构造函数
//......
}
创建一个main方法,在main方法中测试。
首先创建一个Student的实例,创建ObjectOutputStream输出流,然后调用ObjectOutputStream的writeObject()方法将studen对象序列化到test.txt文件中。
public static void main(String[] args) {
try {
Student student = new Student("张三",18);
//创建一个输对象出流
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.txt"));
outputStream.writeObject(student);
} catch (IOException e) {
e.printStackTrace();
}
}
执行,在src文件夹下生成了test.txt文件
我们再反序列化一下看看能不能反序列化回来。
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("test.txt"));
Student student2 = (Student) inputStream.readObject();
System.out.println("反序列化:"+student2);
从结果可以看出是可以序列化是成功了。
有一点注意的是,反序列化时不会调用构造方法。
属性为引用数据类型的对象,如果要序列化,那么该属性也要实现Serializable接口才能实现该对象的完全序列化,否则就会报错NotSerializableException
新增一个Class(班级)类:
public class Class {
private String className;
private int nums;
}
Student新增Calss属性
public class Student implements Serializable {
private String name;
private int age;
private Class aClass;
Class实现Serializable接口,调用序列化和反序列化
- java序列化算法
- 所有保存到磁盘的对象都有一个序列化编号
- 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
- 如果此对象已经序列化过,则直接输出编号即可。
- 序列化存在的问题
由于序列化时不会重复序列化同一个对象,所以对象第一次被序列化之后,后面对对象进行了修改再次序列化时得到的
还是原来的序列化对象。
public static void main(String[] args) {
try {
String name=new String("sss");
Class aClass = new Class("1班",40);
Student student = new Student("张三",18,aClass);
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.txt"));
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("test.txt"));
outputStream.writeObject(student);
Student student1 = (Student) inputStream.readObject();
System.out.println("反序列化:"+student1);
aClass.setClassName("2班");
student.setaClass(aClass);
outputStream.writeObject(student);
Student student2 = (Student) inputStream.readObject();
System.out.println("反序列化:"+student2);
} catch (Exception e) {
e.printStackTrace();
}
}
输出结果:
可以看出我修改后再序列化,反序列化回来的对象还是和第一次一样。
transient关键字
如果我们希望有些类的成员不被序列化,可以使用transient关键字修饰。
public class Student implements Serializable {
private transient String name;
private int age;
private Class aClass;
}
输出结果:
反序列化:Student{name='null', age=18, aClass=Class{className='1班', nums=40}}
使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。
此外,你可以通过重新writeObject()和readObject()方法实现自己的序列化方法。
- 序列化版本号serialVersionUID
随着项目的升级,java序列化的class文件会改变,那么序列化怎么保证前后兼容性呢?java序列化提供了一个private static final long serialVersionUID 的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。
我们先给Student一个序列化版本号,然后把Student序列化保存到stu.txt文件中。
public class Student implements Serializable {
private transient String name;
private int age;
private Class aClass;
}
然后修改,把age的类型改成long;
public class Student implements Serializable {
private static final long serialVersionUID = 1001L;
private transient String name;
private long age;
private Class aClass;
}
输出结果:
java.io.InvalidClassException: 更文挑战.day01.serializable.Student; local class incompatible: stream classdesc serialVersionUID = 8678683859078700039, local class serialVersionUID = -3533377131204334585
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at 更文挑战.day01.serializable.Main.main(Main.java:23)
Student加上序列号版本号
输出结果:
反序列化:Student{name='null', age=18, aClass=Class{className='1班', nums=40}}
虽然我改变了类,但是因为我定义的序列号版本号相同,所以还是反序列化成功,由于我比较好奇如果类型不兼容会怎样,于是我定义了序列化版本号,先序列化保存到文件,修改Student类的age类型为String。
输出反序列化结果:
java.io.InvalidClassException: 更文挑战.day01.serializable.Student; incompatible types for field age
at java.io.ObjectStreamClass.matchFields(ObjectStreamClass.java:2453)
at java.io.ObjectStreamClass.getReflector(ObjectStreamClass.java:2347)
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:753)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at 更文挑战.day01.serializable.Main.main(Main.java:23)
类型不兼容,反序列化失败。
总结
- 需要网络传输的对象的类需要实现Serializable接口。
- 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
- 使用transient修饰变量不会被序列化。
- 序列化对象的引用类型成员变量,也必须是可序列化的,不然会报错。
- 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
- 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。