1 什么是序列化
序列化是一种对象持久化的手段。普遍应用在网络传输的场景中。对于 Java 而言,序列化主要用于对对象进行持久化操作。一般情况下,Java 对象的生命周期短于 JVM 的生命周期,随着 JVM 停止运行,Java 对象也随之消亡。但是,为了能在 JVM 停止运行后对指定对象进行持久化,并在将来的某个时刻重新读取被保存的对象,或跨 JVM 传输对象,持久化能实现上述需求。
Java 在持久化对象时,会将对象的状态保存为一组字节。在反序列化时,将字节组装成对象。
值得注意的是,序列化保存的的是对象的状态,即它的成员变量,而不会保存类的静态变量和被transient修饰的变量。
2 Java序列化的相关特性
- 只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化。
- 通过 ObjectOutputStream 和 ObjectInputStream 实现对象的序列化及反序列化。
- 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)。
- 序列化并不保存静态变量。
- 要想将父类对象也序列化,就需要让父类也实现 Serializable 接口。
- transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
3 Java序列化的方式
实现空接口 Serializable。
- 这种方式隐式实现序列化,是最简单的序列化方式,
- 默认情况下,会自动序列化所有非static和transient修饰的成员变量。
- 实现 Externalizable 接口。
- Externalizable 继承自Serializable,一旦实现了Externalizable 接口,那么基于Serializable的机制将失效。
- 这种方式必须实现 writeExternal() 和 readExternal() 方法,这两个方法是自动调用的。
- writeExternal()方法内可以自己选择哪些部分进行序列化。
- writeExternal()方法内可以自定义选择序列化被transient修饰的成员变量。
- 实现 Serializable 接口并添加 writeObject() 和 readObject() 方法。
- 手动添加writeObject() 和 readObject() 方法,注意不是重写或覆盖。
- 这两个方法必须被 private 修饰。
- 第一行应该默认调用 defaultReadObject() 与 defaultWriteObject() 方法,目的是隐式序列化非static和非transient变量。
- 最后调用 readObject() 和 writeObject() 进行显示序列化被transient修饰的成员变量。
- writeObject()方法内可以自定义选择序列化被transient修饰的成员变量。
- 手动添加writeObject() 和 readObject() 方法,注意不是重写或覆盖。
具体代码如下:
package org.example;
import java.io.*;
// Serializable接口实现序列化
public class SerializeTest {
public static void main(String[] args) {
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\Serialize.txt")));
) {
Student s1 = new Student("zhangsan", 1);
oos.writeObject(s1);
Student s2 = new Student("lisi", 2);
oos.writeObject(s2);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\Serialize.txt")))
) {
Student o = (Student) ois.readObject();
System.out.println(o);
Student o2 = (Student) ois.readObject();
System.out.println(o2);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
static class Student implements Serializable {
private String name;
private transient int id;
// 省略了全参、空参构造方法、get、set方法、toString方法
}
}
package org.example;
import java.io.*;
// Externalizable接口 实现自定义序列化和反序列化
public class SerializeTest2 {
public static void main(String[] args) {
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\Serialize.txt")));
) {
Student s1 = new Student("zhangsan", 1, new School("清华", "北京"), 1);
oos.writeObject(s1);
Student s2 = new Student("lisi", 2,new School("北大", "北京"), 2);
oos.writeObject(s2);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\Serialize.txt")))
) {
Student o = (Student) ois.readObject();
System.out.println(o);
Student o2 = (Student) ois.readObject();
System.out.println(o2);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
// 实现Externalizable接口
static class Student implements Externalizable {
private String name;
private transient Integer age;
private School school;
private int id;
// 省略了全参、空参构造方法、get、set方法、toString方法
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// 根据变量的类型不同,对需要进行序列化的变量进行writeXXX操作
// 这里没有对id变量进行序列化,其反序列化的结果为类型的默认值
out.writeObject(name);
out.writeInt(age);
out.writeObject(school);
// out.writeInt(id);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// 获取反序列化后的结果,注意对类型进行转换
name = (String) in.readObject();
age = in.readInt();
school = (School) in.readObject();
// id = in.readInt();
}
}
private static class School implements Externalizable {
private String name;
private String address;
// 省略了全参、空参构造方法、get、set方法、toString方法
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeObject(address);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
address = (String) in.readObject();
}
}
}
package org.example;
import java.io.*;
// Serializable 接口并添加 writeObject() 和 readObject() 方法
public class SerializeTest3 {
public static void main(String[] args) {
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\Serialize.txt")));
) {
Student s1 = new Student("zhangsan", 1);
oos.writeObject(s1);
Student s2 = new Student("lisi", 2);
oos.writeObject(s2);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\Serialize.txt")))
) {
Student o = (Student) ois.readObject();
System.out.println(o);
Student o2 = (Student) ois.readObject();
System.out.println(o2);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
static class Student implements Serializable {
private String name;
private transient int id;
// 省略了全参、空参构造方法、get、set方法、toString方法
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
// 显示序列化被transient修饰的变量
oos.writeInt(id);
}
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
ois.defaultReadObject();
id = ois.readInt();
}
}
}
4 ArrayList中的序列化
首先,ArrayList 中实现了 Serializable 接口,意味着这个集合类可以进行序列化操作。
在ArrayList 底层,保存具体集合元素的 Object[] 类型的数组变量 elementData 被 transient 修饰,但是集合元素经过反序列化后依旧可以复原。原因如下:ArrayList 提供了两个方法:writeObject 和 readObject。
- 在writeObject方法中:
- 首先调用ObjectOutputStream 的 defaultWriteObject 方法,写出非transient非static的属性;以及ArrayList的size属性。
- 然后再把 Object[] 中下标 [0, size-1] 的数据逐一写入。注意这时候长度是数组的实际长度(也就是前面写出的size),而不是ArrayList的数组容量。
- 在readObject 方法中:
- 首先调用ObjectInputStream 的 defaultReadObject 方法,读取非transient非static的属性;以及ArrayList的size属性。
- 其次先读出数组长度,如果数组长度不够则调用ensureCapacityInternal 方法扩容。
- 最后逐一读出。
为什么要用transient修饰elementData
既然要序列化ArrayList,为什么又要用transient修饰elementData?
ArrayList 由于其动态性,数组容量总是大于等于数组实际数量,当序列化时可能会将大量空值序列化为 null ,造成不必要的浪费,因此用transient修饰elementData,然后在writeObjec进行手动序列化,只序列化实际存储的元素,而不是整个数组。5 能序列化的前提
如果一个类想被序列化,需要实现 Serializable 接口进行自动序列化,或者实现 Externalizable 接口进行手动序列化,否则强行序列化该类的对象,就会抛出 NotSerializableException 异常,这是因为,在序列化操作过程中会对类型进行检查(代码如下),要求被序列化的类必须属于 Enum、Array 和 Serializable 类型其中的任何一种(Externalizable也继承了Serializable)。
JVM 是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)。后面进行具体的介绍。
transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
FileOutputStream 类有一个带有两个参数的重载构造方法——FileOutputStream(String, boolean)。若其第二个参数为 true 且 String 代表的文件存在,那么将把新的内容写到原来文件的末尾而非重写这个文件,故不能用这个版本的构造函数来实现序列化,也就是说必须重写这个文件,否则在读取这个文件反序列化的过程中就会抛出异常,导致只有第一次写到这个文件中的对象可以被反序列化,之后程序就会出错。if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
}
6 Java序列化中的serialVersionUID
serialVersionUID 用于 Java 的序列化机制。简单来说,Java 的序列化机制是通过判断类的 serialVersionUID 来验证版本一致性的。
在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是 InvalidCastException 。
序列化操作的时候系统会把当前类的 serialVersionUID 写入到序列化文件中,当反序列化时系统会去检测文件中的 serialVersionUID ,判断它是否与当前类的 serialVersionUID 一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。serialVersionUID 有两种生成机制:
- 采用默认的private static final long serialVersionUID = 1L。
- 根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:
private static final long serialVersionUID = xxxxL;
当实现 java.io.Serializable 接口的类没有显式地定义一个 serialVersionUID 变量时候,Java 序列化机制会根据编译的 Class 自动生成一个 serialVersionUID 作序列化版本比较用。这种情况下,如果 Class 文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID 也不会变化的。
7 Java序列化中父子类相关问题
- 如果父类已经实现了序列化,其所有子类都自动实现序列化。
- 事实上,很多时候父类并没有实现序列化,这个时候其子类要想实现序列化,除了自身实现 Serializable 以外,还必须满足两个条件:
- 父类有无参的构造方法。
- 在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。
- 当我们在反序列后输出父对象的变量值时,它的值是调用父类无参构造函数后的值。
- 子类要负责父类中成员变量的序列化和反序列化。
- 还记得前面提到的
writeObject
和readObject
方法吗,就是利用这两个方法手动序列化父类的成员变量。 - 需要注意的是,序列化前父类中定义的变量的值,和序列化后的值是不一样的。
- 还记得前面提到的
- 父类有无参的构造方法。
package org.example.exception;
import java.io.*;
// 父类实现了序列化,其所有子类自动实现了序列化
public class ExceptionDemo {
public static void main(String[] args) throws Exception {
File file = new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\exception\\test.txt");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
Student s1 = new Student("zhangsan", 1);
Student s2 = new Student2("lisi", 2);
oos.writeObject(s1);
oos.writeObject(s2);
System.out.println(s1);
System.out.println(s2);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Student s3 = (Student) ois.readObject();
Student2 s4 = (Student2) ois.readObject();
System.out.println(s3);
System.out.println(s4);
}
static class People implements Serializable {
}
// 子类,注意没有实现Serializable接口
static class Student extends People {
public String name;
public int id;
public Student(String name, int id) {
this.name = name;
this.id = id;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}
// 孙类,注意没有实现Serializable接口
static class Student2 extends Student {
public Student2(String name, int id) {
super(name, id);
}
@Override
public String toString() {
return "Student2{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}
}
package org.example.exception;
import java.io.*;
// 父类没有实现序列化,子类实现序列化
public class ExceptionDemo2 {
public static void main(String[] args) throws Exception {
File file = new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\exception\\test2.txt");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
Student s1 = new Student("male","zhangsan", 1);
oos.writeObject(s1);
System.out.println(s1);
System.out.println("--------------------");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Student s3 = (Student) ois.readObject();
System.out.println(s3);
}
static class People {
public String gender;
public People(String gender) {
this.gender = gender;
}
// 父类必须有空参构造方法
// 反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象
public People() {
System.out.println("父类的无参构造方法");
}
@Override
public String toString() {
return "People{" +
"gender='" + gender + '\'' +
'}';
}
}
// 子类
static class Student extends People implements Serializable {
public String name;
public int id;
public Student(String gender, String name, int id) {
super(gender);
this.name = name;
this.id = id;
}
private void writeObject(ObjectOutputStream out) throws IOException {
// 先序列化子类对象
out.defaultWriteObject();
// 再手动序列化父类的成员变量
out.writeObject(gender);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 先反序列化子类对象
in.defaultReadObject();
// 再手动反序列化父类的成员变量
String gender = (String) in.readObject();
}
@Override
public String toString() {
return "Student{" +
"gender='" + gender + '\'' +
", name='" + name + '\'' +
", id=" + id +
'}';
}
}
}
输出结果:
Student{gender='male', name='zhangsan', id=1}
--------------------
父类的无参构造方法
Student{gender='null', name='zhangsan', id=1}
父类没有实现序列化,子类也没有实现序列化,孙类实现了序列化。
package org.example.exception;
import java.io.*;
// 父类没有实现序列化,子类也没有实现序列化,孙类实现了序列化
public class ExceptionDemo3 {
public static void main(String[] args) throws Exception {
File file = new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\exception\\test3.txt");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
Student s2 = new Student2("male","lisi", 2, "清华");
oos.writeObject(s2);
System.out.println(s2);
System.out.println("--------------------");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Student2 s3 = (Student2) ois.readObject();
System.out.println(s3);
}
static class People {
public String gender;
public People(String gender) {
this.gender = gender;
}
// 父类必须有空参构造方法
// 反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象
public People() {
System.out.println("父类的父类的无参构造方法");
}
@Override
public String toString() {
return "People{" +
"gender='" + gender + '\'' +
'}';
}
}
// 子类
static class Student extends People {
public String name;
public int id;
public Student() {
System.out.println("父类的无参构造方法");
}
public Student(String gender, String name, int id) {
super(gender);
this.name = name;
this.id = id;
}
@Override
public String toString() {
return "Student{" +
"gender='" + gender + '\'' +
", name='" + name + '\'' +
", id=" + id +
'}';
}
}
// 孙类
static class Student2 extends Student implements Serializable {
String school;
public Student2() {
}
public Student2(String gender, String name, int id, String school) {
super(gender, name, id);
this.school = school;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeObject(gender);
out.writeObject(name);
out.writeInt(id);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
String gender = (String) in.readObject();
String name = (String) in.readObject();
id = in.readInt();
}
@Override
public String toString() {
return "Student2{" +
"gender='" + gender + '\'' +
", name='" + name + '\'' +
", id=" + id +
", school='" + school + '\'' +
'}';
}
}
}
输出结果:
Student2{gender='male', name='lisi', id=2, school='清华'}
--------------------
父类的父类的无参构造方法
父类的无参构造方法
Student2{gender='null', name='null', id=2, school='清华'}
参考
- https://blog.csdn.net/qq_43561507/article/details/109439693
- https://www.cnblogs.com/vinozly/p/5171227.html
- https://www.cnblogs.com/kubixuesheng/p/10350523.html
- https://snailclimb.gitee.io/javaguide/#/docs/java/basis/Java%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86?id=io-%e6%b5%81
- https://blog.nowcoder.net/n/8f0280724e074093a7e7b5951098c2bc