文章目录
1. 什么是序列化和反序列化
- 序列化 是将数据结构或对象转换为字节流或其他可传输的格式的过程。
序列化后的数据可以被存储在文件中,用于持久化存储,也可以通过网络传输给其他程序或系统。
- 反序列化 是将之前序列化的数据重新恢复为原始的数据结构或对象的过程。
通过反序列化,我们可以从文件中读取序列化的数据,或者接收通过网络传输的序列化数据,并将其转换回原始数据类型或对象。
2. 为什么需要序列化和反序列化
序列化和反序列化在许多场景中发挥着重要作用。以下是一些常见的情况下为什么我们需要序列化和反序列化:
-
持久化存储: 将对象序列化后,可以将其保存到文件系统或数据库中。这样可以实现数据的持久化存储,以便在程序下一次运行时恢复对象。
-
网络传输: 当我们需要通过网络发送数据时,需要将数据序列化为字节流,以便传输。接收方可以通过反序列化将接收到的数据转换回原始数据结构或对象。
-
分布式系统: 在分布式系统中,各个节点之间需要进行数据交换和通信。序列化和反序列化可以帮助节点之间传输对象,并在接收方恢复对象以进行进一步的处理。
-
远程调用: 在通过远程调用调用远程服务时,需要将参数对象序列化并传输到服务端进行处理。服务端再通过反序列化将数据恢复为对象,并执行相应的操作。
3. Java序列化如何工作
3.1 Java序列化和反序列化
- Java序列化:将 Java 对象转换成字节流的过程
序列化过程:是指把一个 Java 对象变成二进制内容,实质上就是一个 byte[]。
- Java反序列化:将字节流转换成 Java 对象的过程
反序列化过程:把一个二进制内容(也就是 byte[])变回 Java 对象。
3.2 Java序列化和反序列化实现
Java的序列化和反序列化是通过实现java.io.Serializable
接口来完成的。当一个对象实现了Serializable
接口后,就可以被序列化为一个字节流,然后存储到磁盘或者通过网络传输。反之,如果要将一个字节流反序列化为一个对象,则需要该对象的类实现Serializable
接口
下面是Student
没有实现Serializable
接口抛出的异常
java.io.NotSerializableException: com.xmc.serializable.Student
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at com.xmc.serializable.SerializationDemo.main(SerializationDemo.java:23)
我们根据堆栈信息查看源码,果然有是否实现Serializable
接口的判断
3.3 Java序列化过程
- 创建一个
ObjectOutputStream
对象,并将其连接到输出流(如文件流或网络流)。 - 调用
ObjectOutputStream
的writeObject()
方法,将需要序列化的对象作为参数传入。 writeObject()
方法将对象转换为字节流,并将其写入输出流中。- 一旦对象被写入输出流,它就可以在文件中持久化存储,或通过网络传输给其他系统。
示例如下:
// 实现Serializable接口
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable {
private static final long serialVersionUID = -8509352946652498240L;
private String name;
private int age;
}
//序列化测试
public class SerializationDemo {
public static void main(String[] args) {
Student student = new Student("Alice", 20);
try {
// 创建ObjectOutputStream对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.ser"));
// 将对象序列化到文件中
oos.writeObject(student);
// 关闭流
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.4 Java反序列化过程
- 创建一个
ObjectInputStream
对象,并将其连接到输入流(如文件流或网络流)。 - 调用
ObjectInputStream
的readObject()
方法,从输入流中读取字节流数据。 readObject()
方法负责将字节流数据转换回原始对象,并返回一个对象引用。- 可以使用返回的对象引用进行进一步的操作和处理。
示例如下:
public class DeserializationDemo {
public static void main(String[] args) {
try {
// 创建ObjectInputStream对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.ser"));
// 从文件中读取字节流并转换为对象
Student student = (Student) ois.readObject();
// 输出对象的属性
System.out.println("Name: " + student.getName());
System.out.println("Age: " + student.getAge());
// 关闭流
ois.close();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
3.5 serialVersionUID
的作用
上述示例Student
没有显示添加serialVersionUID
字段,我序列化一个Student
实例之后,然后增加了set/get方法,反序列的过程中抛出如下异常
java.io.InvalidClassException: com.xmc.serializable.Student; local class incompatible: stream classdesc serialVersionUID = 4397770319721316078, local class serialVersionUID = -8509352946652498240
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1843)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1713)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2000)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1535)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
at com.xmc.serializable.DeserializationDemo.main(DeserializationDemo.java:21)
serialVersionUID
是Java
中用于序列化和反序列化的一个特殊的静态变量。它是一个长整型常量,作为序列化版本标识符,用于确定类的版本是否与序列化的实例匹配。
当一个类实现了Serializable
接口后,如果没有提供显式的serialVersionUID
,编译器会根据类的结构自动生成一个默认的 serialVersionUID
。序列化和反序列化过程中,JVM会使用serialVersionUID
来验证类的版本是否一致,如果版本不一致,将会抛出InvalidClassException
异常。
在序列化时,如果一个类的serialVersionUID
被明确指定为不同的值,那么即使类的结构没有发生变化,也将被视为不同的类,从而会导致反序列化失败。
为了避免这种问题,通常建议在实现Serializable
接口的类中显式地定义serialVersionUID
,并且保持它的值不变。例如:
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
// ...
}
通过显式地定义serialVersionUID
,可以确保在类的结构没有发生变化的情况下仍然兼容之前序列化的实例。如果需要对类进行更新,可以通过增加、删除、修改字段或方法来实现,而不会导致反序列化失败。
3.6 瞬态变量transient
transient
是Java中用于修饰变量的关键字之一。当一个变量被transient
修饰时,它将不会被自动序列化。
通常情况下,对象的所有非静态和非常量成员变量都会被序列化,以便在将对象写入持久存储介质(如文件、数据库等)或进行远程传输时,可以将对象的状态保存下来并在需要时恢复。
然而,有些变量可能不希望被序列化,这可能是因为它们包含敏感信息或临时状态,或者它们不需要在对象重建时保留其值。在这种情况下,可以使用transient
关键字来标记这些变量,告知Java
虚拟机在序列化过程中忽略它们。
需要注意的是,被transient
修饰的变量在序列化过程中会被赋予默认值,而不是对象在未序列化时的值
示例如下:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable {
private static final long serialVersionUID = -8509352946652498240L;
private String name;
private transient int age;
}
然后按照上述代码示例测试结果如下:
Name: Alice
Age: 0
3.7 Externalizable
序列化实现方式
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
Externalizable
接口不同于Serializable
接口,需要实现writeExternal
和readExternal
两个方法,以便精确的控制序列化和反序列化
示例如下:
public class Student implements Externalizable {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}
}
上述中,序列化次序name、age,那么反序列化时也要保持次序不变
4. 总结
- 序列化一个对象时,它所引用的所有其他对象也会被序列化,依此类推,直到序列化完整的对象树为止。
- 如果基类实现了
Serializable
接口,则其子类也能被序列化。 - 静态变量不能被序列化,因为静态变量属于类级别的变量,被所有类的实例共享,而不是特定对象的状态。