好的,各位编程界的才子佳人,大家好!我是你们的老朋友,人称“代码界的段子手”的程序猿老王。今天,咱们要一起扒一扒 Java 序列化与反序列化的底裤,哦不,是原理!
引子:对象也想有个“家”?
话说,咱们辛辛苦苦 new 出来的对象,在 JVM 里活蹦乱跳,好不热闹。可是,一旦程序关了,JVM 嗝屁了,这些对象也就跟着灰飞烟灭了。这就像辛辛苦苦攒了一辈子的钱,结果人没了,钱也跟着进了银行保险柜,想想都心疼!
难道对象就不能有个“家”,可以永久保存,随时取用吗?🤔
当然可以!这就是序列化和反序列化的意义所在。它们就像对象的“搬家公司”,负责把对象从内存里搬到硬盘上(序列化),或者从硬盘上搬回内存里(反序列化)。
第一章:序列化:对象“变形记”
序列化,简单来说,就是把一个 Java 对象转换成一串字节流,以便存储到文件、数据库,或者通过网络传输。这个过程就像把一个活生生的人,变成了一堆乐高积木,虽然模样变了,但本质还在。
1.1 Serializable 接口:对象的“身份证”
要让一个对象能够被序列化,最简单的方法就是让它实现 java.io.Serializable
接口。这个接口就像对象的“身份证”,有了它,JVM 才知道这个对象可以被序列化。
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 省略 getter 和 setter 方法
@Override
public String toString() {
return "Person{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
}
注意,Serializable
接口是一个标记接口,它里面没有任何方法。JVM 看到这个接口,就知道该怎么处理了。
1.2 ObjectOutputStream:序列化的“搬运工”
有了“身份证”,还得有“搬运工”才能把对象搬走。这个“搬运工”就是 java.io.ObjectOutputStream
类。
import java.io.*;
public class SerializationDemo {
public static void main(String[] args) {
Person person = new Person("老王", 30);
try (FileOutputStream fileOutputStream = new FileOutputStream("person.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {
objectOutputStream.writeObject(person);
System.out.println("对象序列化成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
这段代码做了什么呢?
- 创建了一个
Person
对象。 - 创建了一个
FileOutputStream
,用于将数据写入到person.ser
文件中。 - 创建了一个
ObjectOutputStream
,它负责把Person
对象转换成字节流,并写入到FileOutputStream
中。 - 调用
writeObject()
方法,把Person
对象序列化到文件中。
运行这段代码,你会发现生成了一个 person.ser
文件。这个文件里存储的就是 Person
对象的字节流。
1.3 序列化注意事项:有些秘密不能说
并非所有字段都可以被序列化。如果一个字段不想被序列化,可以使用 transient
关键字修饰。transient
修饰的字段,在序列化时会被忽略,反序列化后会恢复为默认值。
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
private transient String password; // 不希望被序列化
public Person(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}
// 省略 getter 和 setter 方法
@Override
public String toString() {
return "Person{" +
"name='" + name + ''' +
", age=" + age +
", password='" + password + ''' +
'}';
}
}
为什么要有 transient
关键字呢?
- 安全考虑: 有些敏感信息,比如密码、银行卡号,不应该被序列化到磁盘上,以免泄露。
- 性能考虑: 有些字段的值可以通过其他字段计算出来,不需要被序列化,以节省存储空间和序列化时间。
第二章:反序列化:对象“复活记”
反序列化,就是把一串字节流转换成 Java 对象。这个过程就像把一堆乐高积木,重新拼成一个活生生的人。
2.1 ObjectInputStream:反序列化的“还原师”
反序列化的“还原师”是 java.io.ObjectInputStream
类。
import java.io.*;
public class DeserializationDemo {
public static void main(String[] args) {
try (FileInputStream fileInputStream = new FileInputStream("person.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
Person person = (Person) objectInputStream.readObject();
System.out.println("对象反序列化成功!" + person);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
这段代码做了什么呢?
- 创建了一个
FileInputStream
,用于从person.ser
文件中读取数据。 - 创建了一个
ObjectInputStream
,它负责把字节流转换成Person
对象。 - 调用
readObject()
方法,从文件中读取字节流,并反序列化成Person
对象。
运行这段代码,你会看到控制台输出了 Person
对象的信息。
2.2 反序列化注意事项:版本不兼容的“悲剧”
反序列化时,需要确保类的版本号与序列化时的版本号一致。如果类的结构发生了变化,比如增加了字段、删除了字段,或者修改了字段的类型,那么反序列化可能会失败。
为了解决版本兼容性问题,可以显式地指定类的版本号,即 serialVersionUID
。
import java.io.Serializable;
public 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;
}
// 省略 getter 和 setter 方法
@Override
public String toString() {
return "Person{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
}
显式指定 serialVersionUID
的好处是:即使类的结构发生了微小的变化,只要 serialVersionUID
不变,反序列化仍然可以成功。
第三章:Externalizable 接口:对象的“自主权”
除了 Serializable
接口,Java 还提供了 java.io.Externalizable
接口。Externalizable
接口提供了更强大的控制权,允许开发者完全自定义序列化和反序列化的过程。
3.1 Externalizable 接口:对象的“私人订制”
Externalizable
接口继承自 Serializable
接口,但它强制开发者实现 writeExternal()
和 readExternal()
方法。
import java.io.*;
public class Student implements Externalizable {
private String name;
private int age;
private String className;
public Student() {
// 必须提供一个无参构造函数
}
public Student(String name, int age, String className) {
this.name = name;
this.age = age;
this.className = className;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// 自定义序列化逻辑
out.writeObject(name);
out.writeInt(age);
out.writeObject(className);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// 自定义反序列化逻辑
name = (String) in.readObject();
age = in.readInt();
className = (String) in.readObject();
}
// 省略 getter 和 setter 方法
@Override
public String toString() {
return "Student{" +
"name='" + name + ''' +
", age=" + age +
", className='" + className + ''' +
'}';
}
}
注意,实现 Externalizable
接口的类,必须提供一个无参构造函数。这是因为在反序列化时,JVM 会先调用无参构造函数创建一个对象,然后再调用 readExternal()
方法来初始化对象的字段。
3.2 Serializable vs. Externalizable:谁更胜一筹?
特性 | Serializable | Externalizable |
---|---|---|
实现方式 | 标记接口,无需实现任何方法 | 继承自 Serializable 接口,需要实现 writeExternal() 和 readExternal() 方法 |
序列化方式 | 默认序列化,JVM 自动处理 | 自定义序列化,开发者完全控制 |
性能 | 相对较低,因为 JVM 需要通过反射来获取对象的字段值 | 相对较高,因为开发者可以直接写入和读取字段值 |
安全性 | 相对较低,因为所有非 transient 字段都会被序列化 |
相对较高,因为开发者可以控制哪些字段被序列化 |
版本兼容性 | 依赖 serialVersionUID ,如果类的结构发生了变化,可能会导致反序列化失败 |
需要开发者自己处理版本兼容性问题 |
适用场景 | 简单对象,对性能要求不高,对安全性要求不高 | 复杂对象,对性能要求高,对安全性要求高,需要自定义序列化逻辑 |
无参构造函数 | 不需要 | 需要 |
总的来说,Serializable
接口简单易用,适用于大多数场景。Externalizable
接口提供了更强大的控制权,适用于对性能和安全性有较高要求的场景。
第四章:序列化与反序列化的应用场景
序列化和反序列化在实际开发中有着广泛的应用。
4.1 持久化存储:对象的“时光机”
最常见的应用场景就是把对象保存到文件或数据库中,实现对象的持久化存储。这样,即使程序关闭了,对象的数据也不会丢失。
4.2 网络传输:对象的“快递员”
在分布式系统中,需要把对象通过网络传输到不同的节点。序列化和反序列化可以将对象转换成字节流,方便在网络上传输。
4.3 缓存:对象的“备忘录”
在缓存系统中,可以把对象序列化后存储到缓存中,以提高访问速度。
4.4 RMI (Remote Method Invocation):对象的“远程调用”
RMI 允许一个 JVM 调用另一个 JVM 中的对象的方法。序列化和反序列化是 RMI 的基础。
总结:对象的“变形金刚”
序列化和反序列化就像对象的“变形金刚”,可以把对象转换成字节流,方便存储、传输和缓存。掌握了序列化和反序列化的原理,你就可以更好地理解 Java 对象的生命周期,编写更高效、更安全的代码。
好了,今天的分享就到这里。希望大家有所收获!如果觉得老王讲得还不错,记得点个赞哦!👍
彩蛋:序列化与单例模式
如果一个类是单例模式,那么在反序列化时,可能会创建多个实例,破坏单例的特性。为了解决这个问题,可以实现 readResolve()
方法。
import java.io.Serializable;
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static final Singleton instance = new Singleton();
private Singleton() {
// 私有构造函数,防止外部创建实例
}
public static Singleton getInstance() {
return instance;
}
private Object readResolve() {
// 在反序列化时,返回已有的实例
return instance;
}
}
readResolve()
方法会在反序列化完成后被调用。在这个方法里,我们可以返回已有的单例实例,从而保证单例的特性。
希望这个彩蛋对大家有所帮助!祝大家编程愉快!😊