文章目录
对象状态保存与序列化
文中好多例子的代码没有发全,理解精神即可
所谓“对象序列化”,其实就将某个时刻对象的状态(即对象所有字段当前值的集合)写入到一个“流(最常见的就是文件流也可以是内存流)”中,在需要时再从流中读取数据重建对象在指定时刻的状态。
将Java对象转换为字节流,以便保存到文件、数据库或者通过网络传输给其他程序或计算机。当需要还原对象时,可以将字节流反序列化为对象。Java提供了Serializable
接口来实现这一功能。下面详细介绍对象序列化的概念、原理和操作步骤。
序列化的基本原理
Java序列化使用ObjectOutputStream
和ObjectInputStream
这两个流来实现,将对象转换成字节流保存到文件中或者传输。序列化的主要步骤包括:
- 将对象转换为字节流:通过序列化,将对象的状态转换为字节流并存储。
— 保存每个对象的类型
— 保存对象每个属性的值 - 将字节流保存或传输:保存到文件、数据库或者通过网络传输。
- 反序列化:从字节流中恢复对象,重建出与原对象内容一致的Java对象。
— 读取对象类型
— 创建一个该类型的空白对象
— 用存储在流中的数据填充它
实现对象序列化的步骤
JDK提供了ObjectOutputStream和ObjectInputStream两个类来实现对象序列化和反序列化的功能。
ObjectOutputStream负责将对象状态保存到文件中,但它自己不负责存取文件,而是将这个工作委托给另一个流组件——FileOputStream来完成。
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(“文件名”));
要让Java对象可序列化,必须实现Serializable
接口。Serializable
接口是一个标记接口,没有方法,只起到标记作用。
解释:如果被写对象的类型是String
,或数组,或Enum
,或者继承了Serializable
接口,那么就可以对该对象进行序列化,否则将抛出NotSerializableException
。
1 创建对象并保存其状态(即对象序列化)
MyClass Obj = new … ;
out.writeObject(obj);
2.从流中读取数值,重建对象并恢复其状态(即对象反序列化)
ObjectInputStream in =new ObjectInputStream(new FileInputStream(“文件名”));
MyClass obj = (MyClass) in.ReadObject();
• 序列化和反序列化的成功,都要求应用程序能访问到类的 .class文件。
• 当反序列化时,注意不会调用用户定义的构造函数。
示例代码
下面演示了如何将一个Person
对象序列化到文件中,并从文件中读取该对象的内容:
import java.io.*;
public class CustomSerialize {
private static final String objDataFileName = "customSerializeObj.dat";
public static void main(String[] args) {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(objDataFileName));
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(objDataFileName))
) {
MyPerson per = new MyPerson("张三", 23);
oos.writeObject(per);
System.out.println("向文件" + objDataFileName + "中写入对象:" + per);
MyPerson p = (MyPerson) ois.readObject();
System.out.println("从文件" + objDataFileName + "中读入对象:" + p);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
class MyPerson implements Serializable {
private String name;
private int age;
public MyPerson(String name, int age) {
this.name = name;
this.age = age;
}
//提供编译器的语法检查(可选)
@Serial
private void writeObject(ObjectOutputStream out)
throws IOException {
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
@Serial
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
this.name = ((StringBuffer) in.readObject()).toString();
this.age = in.readInt();
}
@Override
public String toString() {
return name + "有" + age + "岁";
}
//region getter and setter
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return this.age;
}
//endregion
}
代码说明
- 生成的数据文件:person.dat,是一个二进制文件,而非文本文件。
- 序列化:用
ObjectOutputStream
的writeObject()
方法将对象写入文件customSerializeObj.dat
,即序列化过程。 - 反序列化:用
ObjectInputStream
的readObject()
方法读取文件,恢复成Person
对象。
序列化中的关键点
-
瞬态(transient)关键字:序列化时,使用
transient
修饰的字段不会被序列化。例如,如果不希望将Person
对象的密码字段序列化,可以将其声明为transient
。private transient String password;
-
静态字段不会被序列化:因为序列化是针对对象状态的,而静态字段属于类,因此不会被序列化。
序列化的应用场景
- 缓存:可以将对象序列化并保存为文件,作为程序启动的缓存数据。
-** 网络传输**:Java RMI(远程方法调用)使用序列化在不同JVM之间传递对象。 - 持久化:将Java对象转换为字节流保存到数据库中,以便后续恢复对象状态。
自定义序列化方法
如果需要更复杂的序列化控制,可以定义writeObject()
和readObject()
方法:
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // 调用默认序列化方法
// 进行额外处理(例如加密某些字段)
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // 调用默认反序列化方法
// 进行额外处理(例如解密某些字段)
}
多次序列化
序列化多个对象:
- 我们也可以调用多次WriteObject方法序列化多个对象,而且这些对象的类型还可以不一样,注意只需按照同样的顺序读取就行了。
import model.MyClass;
import model.MyOtherClass;
import java.io.*;
public class SerializeMultiObject {
private static final String objDataFileName="ObjectData.dat";
public static void main(String args[]) throws IOException, ClassNotFoundException {
writeMultiObjToFile();
readMultiObjFromFile();
}
private static void readMultiObjFromFile()
throws IOException, ClassNotFoundException {
System.out.println("现在从文件中重建对象并输出对象的字段值");
try(ObjectInputStream in = new ObjectInputStream(
new FileInputStream(objDataFileName))){
var myClassObj1 = (MyClass) in.readObject();
var myClassObj2 = (MyClass) in.readObject();
var myOtherClassObj = (MyOtherClass) in.readObject();
System.out.println(myClassObj1);
System.out.println(myClassObj2);
System.out.println(myOtherClassObj);
}
}
private static void writeMultiObjToFile() throws IOException {
try( var out = new ObjectOutputStream(
new FileOutputStream(objDataFileName))){
var myClassObj1 = new MyClass(100);
var myClassObj2 = new MyClass(200);
var myOtherClassObj = new MyOtherClass("Hello");
out.writeObject(myClassObj1);
out.writeObject(myClassObj2);
out.writeObject(myOtherClassObj);
}
System.out.println("三个对象已经写入到文件中。");
}
}
MyClass和MyOtherClass是我创建的两个类。本示例将两个MyClass对象和一个MyOtherClass对象写到文件中,然后再读出来。
== 读与写的顺序需要“严格匹配”。==
多次序列化同一个对象
import model.Person;
import java.io.*;
public class SerializeMultiTimes {
private static final String objDataFileName = "multiTimes.data";
private static final int SerializeTimes = 3;
public static void main(String[] args) {
writePersonMultiTimes();
readPerson();
}
private static void writePersonMultiTimes() {
//将被多次序列化的Person对象
Person person = new Person("张三", 30);
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(objDataFileName))) {
for (int i = 1; i <= SerializeTimes; i++) {
//如果是序列化多个Person对象,会得到不同的结果
// Person person = new Person("张三", 30);
//将person对象写入输出流
oos.writeObject(person);
System.out.println(i + " 对象:“" + person + "”已写入到文件"
+ objDataFileName + "中!");
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
private static void readPerson() {
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(objDataFileName))) {
//用一个数组来保存反序列化结果
Person[] people = new Person[SerializeTimes];
for (int i = 0; i < people.length; i++) {
//从输入流中读取一个Java对象,并将其强制类型转换为Person类
people[i] = (Person) ois.readObject();
System.out.println("[" + i + "] " + people[i]);
}
//检查数组中的各个元素,是否引用同一个Person对象
System.out.println(people[0] == people[1]);
System.out.println(people[1] == people[2]);
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
这是运行结果
3 对象:“张三有30岁”已写入到文件multiTimes.data中!
[0] 张三有30岁
[1] 张三有30岁
[2] 张三有30岁
true
true
从输出结果来看,同一个对象被序列化多次,反序列化时,并不会出现多个“副本”,Java会只反序列化一次,然后,重用这个已经反序列化好的对象。
这是因为,在一次序列化过程中,ObjectOutputStream会跟踪对象的引用。如果多次写入同一个对象的引用,**那么后续的序列化操作不会再次序列化整个对象,而是保存一个指向该对象的引用。**这种行为保持了对象引用的统一性,也叫“引用一致性”。
序列化相同内容的对象
将上面的例子中的代码稍作修改,使每次序列化的对象是相同内容的不同对象。
private static void writePersonMultiTimes() {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(objDataFileName))) {
for (int i = 1; i <= SerializeTimes; i++) {
//如果是序列化多个Person对象,会得到不同的结果
Person person = new Person("张三", 30);
//将person对象写入输出流
oos.writeObject(person);
System.out.println(i + " 对象:“" + person + "”已写入到文件"
+ objDataFileName + "中!");
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
运行结果就变成这样了:
3 对象:“张三有30岁”已写入到文件multiTimes.data中!
[0] 张三有30岁
[1] 张三有30岁
[2] 张三有30岁
false
false
多个相同内容的对象被序列化,反序列化时,会得到不同的对象实例。因为每个对象都进行了序列化并存入了文档中。
同一对象序列化后的追踪修改
经过上面两个例子,会发现同一对象只会被序列化一次。再次序列化时只会保存指针,那么同一个对象被序列化后,如果它的状态有改变,再次序列化时,由于Java不会重复序列化,它只是向流中输出了一个标识,这样一来,对状态的修改,将不会保存到流中。
一,reset()方法
可以通过在第二次序列化之前使用:**oos.reset();**语句,
清除流中已记录的对象引用信息。调用reset()后,Java会将后续的序列化视为新的对象写入,从而包含对象的最新状态。
private static void writePersonMultiTimes() {
//将被两次序列化的Person对象
Person person = new Person("张三", 30);
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(objDataFileName))) {
//将person对象写入输出流
oos.writeObject(person);
System.out.println(" 对象:“" + person + "”已写入到文件"
+ objDataFileName + "中!");
//同一对象,字段值修改!
person.setName("李四");
oos.reset(); //重置输出流
oos.writeObject(person);
System.out.println(" 对象:“" + person + "”已写入到文件"
+ objDataFileName + "中!");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
二,储存在不同的文件内
我们可以将对象序列化和反序列化的代码,抽取为独立的方法,文件名作为方法参数
import model.Person;
import java.io.*;
public class SerializeMultiTimes3 {
private static final String objDataFileName1 = "multiTimes1.dat";
private static final String objDataFileName2 = "multiTimes2.dat";
public static void main(String[] args) {
writePersonMultiTimes();
readPerson();
}
private static void writePersonMultiTimes() {
//将被两次序列化的Person对象
Person person = new Person("张三", 30);
writePersonToFile(person, objDataFileName1);
//同一对象,字段值修改!
person.setName("李四");
writePersonToFile(person, objDataFileName2);
}
//将Person对象的当前值,序列化到指定的文件中
static void writePersonToFile(Person person, String fileName) {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(fileName))) {
//将person对象写入输出流
oos.writeObject(person);
System.out.println("对象:“" + person + "”已写入到文件"
+ fileName + "中!");
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
private static void readPerson() {
Person person1 = readFromFile(objDataFileName1);
System.out.println("第一次反序列化:" + person1);
Person person2 = readFromFile(objDataFileName2);
System.out.println("第二次反序列化:" + person2);
//检查两个变量是否引用同一个Person对象
System.out.println(person1 == person2);
}
//从指定文件中反序列化Person对象
private static Person readFromFile(String fileName) {
Person person = null;
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(fileName))) {
//从输入流中读取一个Java对象,并将其强制类型转换为Person类
person = (Person) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
System.out.println(e.getMessage());
}
return person;
}
}
就像这个例子中,分别把对象存入multiTimes1.dat和multiTimes2.dat中,相应的,反序列出来的对象也不是同一个。因为从不同文件反序列化生成的是两个不同的对象实例。
借助这种方法,可以实现简单的“备份”和“回滚”功能。
备份快照
具体来说,每次序列化都可以被看作是创建一个“备份快照”,每个快照都保留了对象在特定时间点的状态。然后,通过反序列化某个快照,可以恢复对象到那个时间点的状态,达到“回滚”的效果。
- 备份:每次对象状态变化后,调用序列化方法,将对象的当前状态保存到一个新文件中(可以按时间戳或版本号命名)。
- 回滚:当需要恢复到某个状态时,读取对应时间点的文件,反序列化得到对象的旧状态,从而实现“回滚”。
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import model.Person;
public class BackupRollbackExample {
// 备份对象到一个新文件中
public static void backup(Object obj) {
String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
String fileName = "backup_" + timestamp + ".dat";
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName))) {
oos.writeObject(obj);
System.out.println("对象已备份到文件: " + fileName);
} catch (IOException e) {
e.printStackTrace();
}
}
// 从指定备份文件中恢复对象
public static <T> T rollback(String fileName, Class<T> clazz) {
T obj = null;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName))) {
obj = clazz.cast(ois.readObject());
System.out.println("对象已从文件 " + fileName + " 中恢复");
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return obj;
}
public static void main(String[] args) {
// 初始对象
Person person = new Person("张三", 30);
// 第一次备份
backup(person);
// 修改对象状态并备份
person.setName("李四");
backup(person);
// 假设需要回滚到第一个备份状态
Person rollbackPerson = rollback("backup_20241102181109.dat", Person.class);
System.out.println("回滚后的对象: " + rollbackPerson);
}
}
就像这样,简单实现了备份与回滚的功能,假设要实现学生对象的“备份”和“回滚”功能,我们可以为每次备份生成一个唯一文件名(例如通过时间戳命名),并在需要回滚时读取指定文件。
注意事项:
- 备份管理:如果备份频繁,文件数量可能增多,需考虑如何有效管理备份文件(例如删除过旧的备份)。
- 状态一致性:在复杂系统中,需要确保每次备份都能保持系统内的对象一致性,以避免回滚后的状态不完整或不正确。
- 性能:频繁的序列化和反序列化会带来存储和性能的开销,需根据实际需求权衡备份频率。
组合对象的序列化
组合对象的序列化指的是一个类内部包含了其他类的实例对象。当一个包含其他对象的类被序列化时,Java会递归地序列化该类中的所有组合对象,以确保整个对象结构都被保存下来。因此,对于组合对象的序列化,所有成员对象(即“组合的对象”)都需要实现Serializable接口。
实现组合对象序列化的关键点:
-
组合对象实现Serializable接口:被序列化的类及其所有成员对象都必须实现Serializable接口,否则会抛出NotSerializableException异常。
-
递归序列化:在序列化主对象时,Java会自动递归地对其组合对象进行序列化。因此,整个对象图(对象与其成员对象的引用关系)会被完整保存到文件中。
-
瞬态字段(transient):如果某个组合对象不需要被序列化,可以用transient关键字标记该字段,这样在序列化时会跳过此字段。
以下是一个包含组合对象的序列化示例。假设我们有一个Department类和一个Person类。Department包含一个Person对象作为其成员属性。我们希望能够序列化Department对象,同时也将Person对象的状态保存。
import java.io.*;
// 成员类需要实现 Serializable 接口
class Person implements Serializable {
private static final long serialVersionUID = 1L;
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 + '}';
}
}
// 包含组合对象的类
class Department implements Serializable {
private static final long serialVersionUID = 1L;
private String deptName;
private Person manager; // 组合对象
public Department(String deptName, Person manager) {
this.deptName = deptName;
this.manager = manager;
}
@Override
public String toString() {
return "Department{deptName='" + deptName + "', manager=" + manager + '}';
}
}
public class CompositeSerializationTest {
public static void main(String[] args) {
Department dept = new Department("研发部", new Person("张三", 35));
// 序列化 Department 对象
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("department.dat"))) {
oos.writeObject(dept);
System.out.println("序列化完成: " + dept);
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化 Department 对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("department.dat"))) {
Department restoredDept = (Department) ois.readObject();
System.out.println("反序列化完成: " + restoredDept);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
组合对象的序列化指的是一个类内部包含了其他类的实例对象。当一个包含其他对象的类被序列化时,Java会递归地序列化该类中的所有组合对象,以确保整个对象结构都被保存下来。因此,对于组合对象的序列化,所有成员对象(即“组合的对象”)都需要实现Serializable
接口。
实现组合对象序列化的关键点
-
组合对象实现
Serializable
接口:被序列化的类及其所有成员对象都必须实现Serializable
接口,否则会抛出NotSerializableException
异常。 -
递归序列化:在序列化主对象时,Java会自动递归地对其组合对象进行序列化。因此,整个对象图(对象与其成员对象的引用关系)会被完整保存到文件中。
-
瞬态字段(
transient
):如果某个组合对象不需要被序列化,可以用transient
关键字标记该字段,这样在序列化时会跳过此字段。
示例代码
以下是一个包含组合对象的序列化示例。假设我们有一个Department
类和一个Person
类。Department
包含一个Person
对象作为其成员属性。我们希望能够序列化Department
对象,同时也将Person
对象的状态保存。
import java.io.*;
// 成员类需要实现 Serializable 接口
class Person implements Serializable {
private static final long serialVersionUID = 1L;
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 + '}';
}
}
// 包含组合对象的类
class Department implements Serializable {
private static final long serialVersionUID = 1L;
private String deptName;
private Person manager; // 组合对象
public Department(String deptName, Person manager) {
this.deptName = deptName;
this.manager = manager;
}
@Override
public String toString() {
return "Department{deptName='" + deptName + "', manager=" + manager + '}';
}
}
public class CompositeSerializationTest {
public static void main(String[] args) {
Department dept = new Department("研发部", new Person("张三", 35));
// 序列化 Department 对象
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("department.dat"))) {
oos.writeObject(dept);
System.out.println("序列化完成: " + dept);
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化 Department 对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("department.dat"))) {
Department restoredDept = (Department) ois.readObject();
System.out.println("反序列化完成: " + restoredDept);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
序列化完成: Department{deptName='研发部', manager=Person{name='张三', age=35}}
反序列化完成: Department{deptName='研发部', manager=Person{name='张三', age=35}}
代码说明:
- 组合类实现
Serializable
接口:Person
和Department
类都实现了Serializable
接口,以支持组合对象的序列化。 - 序列化过程:当
Department
对象序列化时,其中的Person
对象也会被递归序列化,因此Person
对象的状态也会被保存。 - 反序列化:在反序列化时,
Department
和Person
对象都会被还原,恢复到序列化前的状态。
注意事项
-
transient
字段的使用:如果Department
类中的Person
字段不需要序列化,可以用transient
标记,序列化时会忽略此字段。 -
深拷贝与组合对象:组合对象的序列化也可以用作深拷贝的方法之一,因为在反序列化时会得到一个独立的对象实例,包括其所有的成员对象。
如果有多个外部对象组合了“同一个”对象,那么,序列化这些外部对象时,会不会导致这个被组合的“内部”对象被序列化多次?
可以举个例子来验证:
一个学生,可以有多个老师,也就是说,多个老师对象,它的student字段,可以引用同一个学生对象。
import model.Person;
import java.io.*;
public class ReadWriteTeacher {
private static final String dataFileName = "teacher.dat";
public static void main(String[] args)
throws IOException, ClassNotFoundException {
writeToFile();
readFromFile();
}
private static void readFromFile()
throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(dataFileName))) {
//依次读取ObjectInputStream输入流中的四个对象
Teacher teacher1 = (Teacher) ois.readObject();
System.out.println("从" + dataFileName + "中读出对象teacher1:" + teacher1);
Teacher teacher2 = (Teacher) ois.readObject();
System.out.println("从" + dataFileName + "中读出对象teacher2:" + teacher2);
Person student = (Person) ois.readObject();
System.out.println("从" + dataFileName + "中读出对象student: " + student);
Teacher teacher3 = (Teacher) ois.readObject();
System.out.println("从" + dataFileName + "中读出对象teacher3:" + teacher3);
System.out.println("唐僧的student字段,是否引用孙悟空对象?"
+ (teacher1.getStudent() == student));//输出true
System.out.println("菩提祖师对象一的student字段,是否引用孙悟空对象?"
+ (teacher2.getStudent() == student)); //输出true
System.out.println("菩提祖师对象二的student字段,是否引用孙悟空对象?"
+ (teacher3.getStudent() == student)); //输出true
System.out.println("菩提祖师对象一和菩提祖师对象二,是否是同一个对象?"
+ (teacher2 == teacher3)); //输出false
}
}
private static void writeToFile() throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(dataFileName))) {
//创建一个学生,两个老师对象,注意,两个老师对象共享同一个学生对象
Person student = new Person("孙悟空", 500);
Teacher teacher1 = new Teacher("唐僧", student);
Teacher teacher2 = new Teacher("菩提祖师", student);
//依次将四个对象写入输出流
oos.writeObject(teacher1);
oos.writeObject(teacher2);
oos.writeObject(student);
oos.writeObject(teacher2);//"菩提祖师"对象被序列化了两次
System.out.println("以下对象已被写入到" + dataFileName + "中");
System.out.println("(1)" + teacher1);
System.out.println("(2)" + teacher2);
System.out.println("(3)" + student);
System.out.println("(4)" + teacher2);
}
}
}
运行结果:
以下对象已被写入到teacher.dat中
(1)唐僧的学生是孙悟空
(2)菩提祖师的学生是孙悟空
(3)孙悟空有500岁
(4)菩提祖师的学生是孙悟空
从teacher.dat中读出对象teacher1:唐僧的学生是孙悟空
从teacher.dat中读出对象teacher2:菩提祖师的学生是孙悟空
从teacher.dat中读出对象student: 孙悟空有500岁
从teacher.dat中读出对象teacher3:菩提祖师的学生是孙悟空
唐僧的student字段,是否引用孙悟空对象?true
菩提祖师对象一的student字段,是否引用孙悟空对象?true
菩提祖师对象二的student字段,是否引用孙悟空对象?true
菩提祖师对象一和菩提祖师对象二,是否是同一个对象?true
对于被多个对象所共享的共享对象(本例中为student对象),序列化时,它只是在第一次序列化时将数据写入流中,后继序列化,Java只是向流中写入一个标识,不会反复将相同的数据复制多次,之后反序列化时,此共享对象只被反序列化一次,之后,让组合它的其他对象相应字段“引用”这个唯一的共享对象。
自定义序列化过程
当然我们也可以自定义序列化过程。
不序列化某些字段
通过在字段前面加上transient关键字,可以让此字段不参与序列化。
class PersonIgnoreage implements java.io.Serializable {
private String name;
private transient int age;
...
}
import model.PersonIgnoreage;
import java.io.*;
public class TransientTest {
private static final String dataFileName = "transient.dat";
public static void main(String[] args)
throws IOException, ClassNotFoundException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(dataFileName));
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(dataFileName))) {
PersonIgnoreage per = new PersonIgnoreage("张三", 46);
//系统会将对象转换为字节序列并输出
oos.writeObject(per);
System.out.println("向文件" + dataFileName + "中写入对象:" + per);
PersonIgnoreage p = (PersonIgnoreage) ois.readObject();
System.out.println("向文件" + dataFileName + "中读取对象:" + p);
}
}
}
向文件transient.dat中写入对象:张三有46岁
向文件transient.dat中读取对象:张三有0岁
根据输出结果,能发现age字段没有参与序列化,因此,反序列化时,得到了一个默认值0。
控制序列化过程
如果需要完全自定义序列化过程,可以让对象实现Serializable接口,并添加以下序列化和反序列化方法。
明确定义如何从流中读取和写入数据。
import java.io.*;
public class CustomSerialize {
private static final String objDataFileName = "customSerializeObj.dat";
public static void main(String[] args) {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(objDataFileName));
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(objDataFileName))
) {
MyPerson per = new MyPerson("张三", 23);
oos.writeObject(per);
System.out.println("向文件" + objDataFileName + "中写入对象:" + per);
MyPerson p = (MyPerson) ois.readObject();
System.out.println("从文件" + objDataFileName + "中读入对象:" + p);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
class MyPerson implements Serializable {
private String name;
private int age;
public MyPerson(String name, int age) {
this.name = name;
this.age = age;
}
//提供编译器的语法检查(可选)
@Serial
private void writeObject(ObjectOutputStream out)
throws IOException {
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
@Serial
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
this.name = ((StringBuffer) in.readObject()).toString();
this.age = in.readInt();
}
@Override
public String toString() {
return name + "有" + age + "岁";
}
//region getter and setter
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return this.age;
}
//endregion
}
从程序输出结果中看,确实按照“逆序”方式将MyPerson对象的name字段值写入到了文件中。
向文件customSerializeObj.dat中写入对象:张三有23岁
从文件customSerializeObj.dat中读入对象:三张有23岁
序列化时转换类型
这个例子中,将一个Person
对象序列化为ArrayList
。
import java.io.*;
import java.util.*;
public class ReplaceTest {
private static class ReplacedPerson implements Serializable {
private String name;
private int age;
public ReplacedPerson(String name, int age) {
this.name = name;
this.age = age;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return this.age;
}
@Serial
private Object writeReplace() throws ObjectStreamException {
ArrayList<Object> list = new ArrayList<Object>();
list.add(name);
list.add(age);
return list;
}
@Override
public String toString() {
return name + "有" + age + "岁";
}
}
private static String replaceObjFileName = "replace.dat";
public static void main(String[] args) {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(replaceObjFileName));
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(replaceObjFileName))) {
ReplacedPerson per = new ReplacedPerson("张三", 34);
oos.writeObject(per);
System.out.println("对象:“" + per + "”已写入到" + replaceObjFileName + "文件中");
ArrayList list = (ArrayList) ois.readObject();
System.out.println("从" + replaceObjFileName + "文件中读出:" + list);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
对象:“张三有34岁”已写入到replace.dat文件中
从replace.dat文件中读出:[张三, 34]
要点:
- ReplacedPerson类要实现Serializable接口。
- 要添加一个writeReplace方法。
- Java在序列化某个对象时,总是先调用writeReplace方法再调用writeObject方法。
利用序列化技术实现对象克隆
让对象实现Cloneable和Serializable接口,利用ByteArrayOutputStream实现克隆。
import java.io.*;
public class SerialCloneableBase implements Cloneable, Serializable {
public Object clone() throws CloneNotSupportedException {
Object clone = super.clone();
try (var bout = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bout);
) {
out.writeObject(this); //将自身状态序列化到流中
try (var bin = new ByteArrayInputStream(bout.toByteArray());
ObjectInputStream in = new ObjectInputStream(bin)) {
//基于流反序列化,得到一个克隆对象
clone= in.readObject();
}
} catch (Exception e) {
System.out.println(e);
}
//向外界返回克隆结果
return clone;
}
}
这段代码展示了一种通过序列化和反序列化来实现深拷贝的克隆方法。SerialCloneableBase
类实现了Cloneable
和Serializable
接口,并重写了clone()
方法,使用序列化机制创建对象的深度克隆。
-
类实现
Cloneable
和Serializable
接口Cloneable
接口:表示类支持克隆操作,通常需要重写clone()
方法。Serializable
接口:使对象支持序列化操作,即可以将对象转换为字节流,以便存储或传输。这里的Serializable
接口用于实现深拷贝,因为序列化会递归保存整个对象状态。
-
clone()
方法的实现- 传统的
super.clone()
克隆通常是浅拷贝,对于包含复杂对象的类,浅拷贝可能无法满足需求。这段代码通过序列化实现了深拷贝,使得对象内部所有成员(包括引用类型字段)都得到完整的复制。
- 传统的
-
具体的克隆步骤
Object clone = super.clone();
- 首先调用
super.clone()
方法,执行基础的浅拷贝,并返回一个新对象的引用。
try (var bout = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bout)) { out.writeObject(this); // 将当前对象写入字节流中,完成序列化 }
- 创建一个字节输出流
ByteArrayOutputStream
和对象输出流ObjectOutputStream
,将当前对象序列化到字节流bout
中。序列化操作会将this
对象及其所有组合对象的状态存入字节数组中。
try (var bin = new ByteArrayInputStream(bout.toByteArray()); ObjectInputStream in = new ObjectInputStream(bin)) { clone = in.readObject(); // 从字节流中反序列化,生成深克隆对象 }
- 使用字节输入流
ByteArrayInputStream
和对象输入流ObjectInputStream
,基于前一步生成的字节数据进行反序列化,得到一个新的对象。该对象在内存中有独立的存储空间,实现了深拷贝。
- 首先调用
-
异常处理
- 由于序列化和反序列化过程可能抛出各种异常(如
IOException
或ClassNotFoundException
),此处使用try-catch
块捕获所有异常并打印错误信息。
- 由于序列化和反序列化过程可能抛出各种异常(如
-
返回深拷贝对象
- 最终返回
clone
对象,该对象是通过序列化和反序列化得到的一个独立副本,确保了对象的深拷贝。
- 最终返回
注意事项:
- 性能问题:序列化和反序列化是相对较慢的操作,因此在频繁克隆的场景下,这种方法可能会影响性能。
- 要求
Serializable
支持:使用这种深拷贝方法,所有字段类型都必须实现Serializable
接口,否则在序列化时会抛出NotSerializableException
异常。