写点什么

理解 Java 序列化

用户头像
RookieMZL
关注
发布于: 2020 年 05 月 08 日
一、什么是序列化


序列化是一种对象持久化的手段。类通过实现 java.io.Serializable 接口以启用其序列化功能。

序列化:把对象转换为字节序列的过程。

反序列化:把字节序列恢复为对象的过程。


《阿里巴巴 Java 开发手册》中对于序列化有以下规定 :

【强制】序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如 果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。

说明:注意 serialVersionUID 不一致会抛出序列化运行时异常。


序列化的场景

  • 当需要把内存中的对象状态保存到一个文件中或者数据库中时候;

  • 当需要使用套接字在网络上传送对象的时候;

  • 当需要通过 RMI(远程方法调用,可以理解为 Java 对象之间的调用)传输对象的时候;

  • 当要跨进程跨网络传输对象的时候,这时候基本要序列化了。


二、如何序列化


2.1 Serializable 接口

Java 类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法进行序列化或反序列化。

Serializable 只是一个空接口,接口没有任何的方法和字段,仅仅用于标识。 如果一个类没有实现这个接口,想要被序列化的话,就会抛出 java.io.NotSerializableException 异常。


2.2 Externalizable 接口

Externalizable 继承自 Serializable , 它更加灵活一点,它里面定义了 writeExternal()readExternal() 两个抽象方法分别用于序列化和反序列化使用。

通过这两个方法,程序猿决定需要序列化那些数据。如果对象中涉及到很少的属性需要序列化,大多数属性无需序列化,这种情况使用Externalizable 接口是比较灵活的。


2.3 transient 关键字

transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。


Tip: 自己查看 String 、Integer 等的源码,可以看到本身也是实现了 Serializable 的。

三、什么是 serialVersionUID


3.1 serialVersionUID 的 作用

serialVersionUID 是用来验证版本一致性的 。


序列化是将对象的状态信息转换为可存储或传输的形式的过程。 Java 对象是保存在 JVM 的堆内存中的,如果 JVM 堆不存在了,对象也就跟着随即消失。


而序列化提供了一种方案,可以在即使 JVM 停机的情况下也能把对象保存下来的方案。 把 Java 对象序列化成可存储或传输的形式(如二进制流),比如保存在文件中。 当再次需要这个对象的时候,从文件中读取出二进制流,再从二进制流中反序列化出对象。


虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致, 一个非常重要的一点是两个类的序列化 ID 是否一致,这个所谓的序列化 ID(serialVersionUID)。


在反序列化时,Jvm 会把传来的字节流中的 serialVersionUID 和本地相应实体类的 serialVersionUID 进行比较, 如果相同就认为一致,可以进行反序列化,否则出现 InvalidCastException 异常。


3.2 代码案例演示

public class Student implements Serializable {        private static final long serialVersionUID = -303793456610254190L;
private String name;
private String address;
private long id;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public long getId() { return id; }
public void setId(long id) { this.id = id; }
@Override public String toString() { return "Student{" + "name='" + name + '\'' + ", address='" + address + '\'' + ", id=" + id + '}'; }
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 序列化 Student student = new Student(); student.setAddress("北京"); student.setId(001); student.setName("小花");
ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream(new File("E:\\data\\Student.txt"))); output.writeObject(student); output.close(); System.out.println("序列化成功:" + student);
// 反序列化 ObjectInputStream input = new ObjectInputStream(new FileInputStream(new File("E:\\data\\Student.txt"))); Student student2 = (Student) input.readObject(); input.close(); System.out.println("反序列化成功:" + student2); }}
复制代码

执行上面的 Main 方法,控制台会打印以下信息:

序列化成功:Student{name='小花', address='北京', id=1}反序列化成功:Student{name='小花', address='北京', id=1}
复制代码

Tip : 如果将 Student 实现的 Serializable 接口去掉会报错如下

Exception in thread "main" java.io.NotSerializableException: com.example.demo.sample.Student	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)	at com.example.demo.sample.Student.main(Student.java:61)
复制代码

3.4 如果修改了 serialVersionUID 会发生什么?

注释掉序列化的代码,修改 serialVersionUID ,再次执行了上面的反序列化代码后。(前提是已经执行成功过一次,已经有 Student.txt 文件了)会报错如下:

Exception in thread "main" java.io.InvalidClassException: com.example.demo.sample.Student; local class incompatible: stream classdesc serialVersionUID = -303793456610254190, local class serialVersionUID = -403793456610254190	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687)	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1883)	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1749)	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2040)	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1571)	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)	at com.example.demo.sample.Student.main(Student.java:66)
复制代码

3.5 transient:定义临时变量


transient 关键字的作用:就是让某些被修饰的成员属性变量不被序列化。


在 Student 类中添加属性 ,在执行序列化的时候设置 age 为 18;

private transient int  age;
复制代码


运行 Main 方法后,可以看到 age 属性并没有被序列化,因为反序列化显示的是 0;

序列化成功:Student{name='小花', address='北京', id=1, age=18}反序列化成功:Student{name='小花', address='北京', id=1, age=0}
复制代码


3.6 static & static-final


static 静态变量不是对象状态的一部分,因此它不参与序列化。那么反序列化之后。我们可以这么理解,静态变量的话,不属于对象的特有的,谁都可以进行改变,所以序列化了之后,在反序列返回来可能就不是那个值了。


如果一个变量修饰为 static final 的话,这时候由于 final 定义的不可被改变,那么这时候这个属性就会被持久化了。


四、总结

(1)序列化和反序列化的定义:就是对象转换为字节和字节转换为对象的过程。

(2)为什么需要序列化:主要是为了跨进程跨网络的数据传输。

(3)序列化的方式:常用的就是实现接口 Serializable。

(4)如何让属性不序列化:只要将属性定义为 transient 即可。

发布于: 2020 年 05 月 08 日阅读数: 99
用户头像

RookieMZL

关注

原谅我这一生不羁放纵爱自由。 2018.11.10 加入

原谅我这一生不羁放纵爱自由。

评论

发布
暂无评论
理解 Java 序列化