一、序列化和反序列化
1、基本概念
序列化:将对象转换为字节流。
反序列化:将字节流转换回对象。
Java 提供了Serializable接口来标识一个类可以被序列化,使用ObjectOutputStream和ObjectInputStream来进行序列化和反序列化。
示例:
import java.io.*;
class Person implements Serializable {
private final String name;
private final int age;
// 构造函数
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// getter 和 setter 方法
public String getName() {
return name;
}
public int getAge() {
return age;
}
// 输出对象信息
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + '}';
}
}
public class SerializationExample {
public static void main(String[] args) {
try {
// 创建一个 Person 对象
Person person1 = new Person("小明", 20);
// 序列化:将对象转换成字节流
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(person1);
objectOutputStream.flush();
// 获取字节流
byte[] byteData = byteArrayOutputStream.toByteArray();
// 输出序列化后的字节数据
System.out.println("序列化后的字节数据: ");
for (byte b : byteData) {
System.out.print(b + " ");
}
System.out.println();
// 反序列化:将字节流转换回对象
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteData);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Person person2 = (Person) objectInputStream.readObject();
// 输出反序列化后的对象
System.out.println("原始对象: " + person1 + " hashCode: " + System.identityHashCode(person1));
System.out.println("反序列化后的对象: " + person2 + " hashCode: " + System.identityHashCode(person2));
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
执行结果:
序列化后的字节数据:
-84 -19 0 5 115 114 0 14 99 111 109 46 115 101 114 46 80 101 114 115 111 110 -32 -13 39 118 58 -4 37 105 2 0 2 73 0 3 97 103 101 76 0 4 110 97 109 101 116 0 18 76 106 97 118 97 47 108 97 110 103 47 83 116 114 105 110 103 59 120 112 0 0 0 20 116 0 6 -27 -80 -113 -26 -104 -114
原始对象: Person{name='小明', age=20} hashCode: 764308918
反序列化后的对象: Person{name='小明', age=20} hashCode: 2114664380
在输出时也把原始对象和序列化后对象的hashcode(内存地址)输出出来了,主要是想在这里告诉大家,虽然原始对象和序列化后对象的内容是相同的,但本质上不同。
2、原始对象和反序列化后的对象区别
在序列化和反序列化过程中,原始对象和反序列化后的对象的区别主要体现在它们的“内存地址”和“对象身份”的不同,而对象的内容通常是相同的。
原始对象:
内存地址:原始对象是程序运行时创建的对象实例,存储在堆内存中。当你打印原始对象时,实际是打印它的字段值。
对象身份:原始对象是通过 new 关键字创建的,每个对象在内存中都有唯一的标识(即内存地址),因此原始对象的标识和反序列化后的对象不同。
生命周期:原始对象通常是在内存中创建和操作的,直到它被垃圾回收。
反序列化后的对象:
内存地址:反序列化后的对象是从字节流恢复出来的,它在内存中的位置和原始对象的内存地址不同。即使两个对象的字段值完全相同,它们在内存中的位置不同,所以它们的内存地址也是不同的。
对象身份:通过反序列化恢复的对象是一个全新的实例,尽管其字段值与原始对象相同,但它是一个不同的对象,拥有不同的内存地址。
生命周期:反序列化后的对象的生命周期由 JVM 管理,它会在不再使用时被垃圾回收。
3、serialVersionUID
serialVersionUID 是一个序列化版本标识符,它帮助 Java 确保序列化和反序列化过程中对象的类版本兼容。如果在序列化对象时保存了 serialVersionUID,当进行反序列化时,Java 会检查反序列化的类和原来序列化时的类是否具有相同的 serialVersionUID,以此来验证类的兼容性。如果不相同,反序列化时会抛出 InvalidClassException 异常。
为什么需要serialVersionUID?
1、当一个类被序列化时,它的 serialVersionUID 会被保存在序列化数据中。
2、当尝试反序列化时,JVM 会读取这个 serialVersionUID,然后检查目标类是否有相同的 serialVersionUID。
3、如果没有显式声明 serialVersionUID,Java 会根据类的内部结构自动计算该 ID,这样不同的类变更可能会导致生成不同的 serialVersionUID,从而无法兼容之前的版本。
声明方法
private static final long serialVersionUID = 1L;
4、transient static final
transient
transient 关键字标记的字段在序列化过程中会被忽略,不会被序列化或反序列化。当一个字段被声明为 transient,它的值不会被序列化。反序列化时,这些字段将被赋予其类型的默认值。
示例:
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private transient int age; // 不会被序列化public Person(String name, int age) {
this.name = name;
this.age = age;
}// Getter and Setter
}
static
static 修饰的字段是类级别的,而不是对象级别的。因此,它们不参与序列化过程。静态字段是与类相关的,而不是与实例相关的,它们不会随着对象的序列化一起存储。
示例:
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private static int count = 0; // 不会被序列化public Person(String name) {
this.name = name;
count++;
}// Getter and Setter
}
final
常量字段(final 字段)在序列化时是被序列化的,但它的值在反序列化时不一定需要被修改。通常,对于一个 final 字段,Java 会假设其值在整个对象的生命周期内是固定的,因此,反序列化时,这些字段的值不会被重新赋值。但这并不意味着它不会被序列化。final 字段在序列化时是存储的,但在反序列化时,JVM 会尝试使用原来的值进行初始化。
5、Externalizable接口
在Java中除了Serializable接口可以进行序列化之外,Externalizable接口同样也可以。Externalizable 是 Serializable 接口的一个子接口,它提供了更多的控制权,允许开发者自定义序列化和反序列化的过程。与 Serializable 接口不同,Externalizable 需要实现两个方法:writeExternal 和 readExternal,这两个方法可以控制对象如何序列化和如何反序列化。
将上面的例子用Externalizable接口表示:
import java.io.*;
class Person implements Externalizable {
private String name;
private int age;
// 默认构造函数是必需的,因为 Externalizable 需要它
public Person() {
// 必须存在无参构造器
}
// 带参数的构造函数
public Person(String name, int age) {
this.name = name;
this