为什么要序列化?
现在开发过程中经常遇到多个进程多个服务间需要交互,或者不同语言的服务之间需要交互,这个时候,我们一般选择使用固定的协议,将数据传输过去,但是在很多语言,比如java等jvm语言中,传输的数据是特有的类对象,而类对象仅仅在当前jvm是有效的,传递给别的jvm或者传递给别的语言的时候,是无法直接识别类对象的,那么,我们需要多个服务之间交互或者不同语言交互,该怎么办?这个时候我们就需要通过固定的协议,传输固定的数据格式,而这个数据传输的协议称之为序列化,而定义了传输数据行为的框架组件也称之为序列化组件(框架)。
序列化框架中会包含两种操作,一种将对象或数据结构转换为特定格式(传输数据协议),我们称之为序列化;另一种是将特定格式转换为对象或数据结构的过程,我们称之为反序列化。
序列化的目的
- 使数据可以在网络中传输,或者可存储在内存或文件中。
- 使数据可以脱离应用程序而独立存在。
序列化使用场景
不同服务间的交互,服务可以同语言也可以不同语言。
Java语言内置序列化
序列化的实现可以分为文本序列化、语言内置序列化和跨语言序列化,本文主要讲述Java语言内置序列化。
每种语言基本都会有实现的序列化框架,此序列化的方式我们称之为语言内置序列化,其优点是语言提供了序列化的框架(序列化协议,序列化和反序列化操作),方便使用,缺点是交互的应用程序必须是同一种语言。
下面将讲述Java需要内置序列化框架的使用,Java序列化协议是字节序列(由jvm实现)。
序列化框架使用的核心
-
序列化接口(Serializable和Externalizable)
需要序列化的类对象需要继承此接口,只有当前接口修饰定义的类对象才可以按照指定的方式传输对象。
-
序列化id-serialVersionUID
用来作为传输/读取双端进程(javaBean)的版本是否一致的,防止我们因为版本不一致导致的序列化失败。
编译器提供两种序列化id的方式,一种是生成默认的versionID,这个值为1L,还有一种方式是根据类名、接口名、成员方法及属性等来生成一个 64 位的哈希字段,只要我们类名、方法名、变量有修改,或者有空格、注释、换行等操作,计算出来的哈希字段都会不同,当然这里需要注意,每次我们有以上的操作的时候尽量都要重新生成一次serialVerionUID(编译器并不会给你自动修改)。
默认固定值1L和64位哈希数值不需要自己计算,继承了序列化接口的类,如果没有序列化id字段,在类名下会显示黄色的波浪线,鼠标放置在类名上,选择需要的方式即可自动添加序列化id字段(如有变动,删除后再根据提示添加即可)。 -
对象序列化和数据写入操作(输出流java.io.ObjectOutputStream)
用于执行序列化操作,并将序列化后的数据写入文件或进行传输。
-
数据读取和对象反序列化操作(输入流java.io.ObjectInputStream)
用于执行反序列化操作,将文件或从网络接收到的数据反序列化为对象。
案例1-基础序列化和反序列化Serializable
下面以序列化对象存储入文件后,读取文件再使用对象为例,讲解序列化框架的使用,此使用同序列化数据后进行传输,因为无论是数据传输还是存取文件,都需要用到输入和输出流,只是获取输入输出流的参数不一致。
Person类实现
public class Person implements Serializable {
//64位哈希序列化ID
private static final long serialVersionUID = 8247451571741032209L;
private String name;
private int age;
private transient boolean isMan; // true=男,false=女
public Person(String name, int age, boolean isMan) {
this.name = name;
this.age = age;
this.isMan = isMan;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public boolean isMan() {
return isMan;
}
public void setMan(boolean isMan) {
this.isMan = isMan;
}
/*此方法只是用于打印,与序列化无关*/
public String toString() {
return "Person{" + "name=" + name + ",age = " + age+",isMan = " + isMan + "}";
}
}
序列化操作
public static void writeObject() {
try {
// 0. 创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("E:/object.txt"));
// 1. 将对象序列化到文件s
Person person = new Person("lilu", 18, true);
oos.writeObject(person);
System.out.println("Person对象已写入object.txt文件");
} catch (Exception e) {
e.printStackTrace();
}
}
反序列化操作
public static void readObject() {
try {
// 0. 创建ObjectInputStream
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
"E:/object.txt"));
Person person1 = (Person) ois.readObject();
System.out.println("读取到的Person对象数据如下:");
System.out.println(person1);
} catch (Exception e) {
e.printStackTrace();
}
}
main函数调用
public static void main(String[] args) {
writeObject();
readObject();
}
运行结果
Java序列化使用的总结
- Java 序列化只是针对对象的属性(对象的类名,变量的类型、变量的数据)的传递,至于方法和序列化过程无关。
- 类中的某个成员变量不需要序列化,使用transient声明该字段。
- 类静态变量(static修饰的变量)不会被序列化。
- 成员是引用的序列化,这个引用类型对应的类也必须是可序列化的。
- 序列化具有传递性-当一个父类实现了序列化,那么子类会自动实现序列化,不需要显示实现序列化接口,反过来,子类实现序列化,而父类没有实现序列化则序列化会失败。 同一对象序列化多次,不会多次序列化同一对象,而是不管进行多少次序列化操作,只会得到同一个对象。
扩展
- 自定义序列化,重写writeObject 和 readObject
- 继承接口Externalizable
- 序列化框架的源码详解
- 跨语言序列化的实现