🍁序列化的原理是什么?
- 🍁典型-----解析
- 🍁拓展知识仓
- 🍁Serializable 和 Externalizable 接门有何不同?
- 🍁如果序列化后的文件或者原始类被篡改,还能被反序列化吗?
- 🍁serialVersionUID 有何用途? 如果没定义会有什么问题?
- 🍁在Java中,有哪些好的序列化框架,有什么好处?
🍁典型-----解析
序列化是将对象转换为可传输格式的过程。是一种数据的持久化手段。一般广泛应用于网络传输,RMI和RPC等场景中。 几乎所有的商用编程语言都有序列化的能力,不管是数据存储到硬盘,还是通过网络的微服务传输,都需要序列化能力。
在Java的序列化机制中,如果是String,枚举或者实现了Serializable接口的类,均可以通过Java的序列化机制将类序列化为符合编码的数据流,然后通过InputStream和OutputStream将内存中的类持久化到硬盘或者网络中;同时,也可以通过反序列化机制将磁盘中的字节码再转换成内存中的类。
如果一个类想被序列化,需要实现Serializable接口。否则将抛出NotSerializableException异常。Serializable接门没有方法或字段,仅用于标识可序列化的语义。
自定义类通过实现Serializable接口做标识,进而在10中实现序列化和反序列化,具体的执行路径如下:
#write0bject -> #writeobjecto(判断类是否是自定义类) -> writeOrdinary0bject(区分Serializable和Externalizable) -> writeSerialData(序列化fields) -> invokewriteobject(反射调用类自己的序列化策略)
其中,在invokeWriteObject的阶段,系统就会处理自定义类的序列化方案。
这是因为,在序列化操作过程中会对类型进行检查,要求被序列化的类必须属于Enum、Array和Serializable类型其中的任何一种。
看一段代码,对象的序列化和反序列化:
import java.io.*;
import java.util.*;
class Employee implements Serializable {
private String name;
private int age;
private Department department;
public Employee(String name, int age, Department department) {
this.name = name;
this.age = age;
this.department = department;
}
public String toString() {
return "Employee [name=" + name + ", age=" + age + ", department=" + department + "]";
}
}
class Department implements Serializable {
private String name;
private List<Employee> employees;
public Department(String name) {
this.name = name;
this.employees = new ArrayList<>();
}
public void addEmployee(Employee employee) {
employees.add(employee);
}
public String toString() {
return "Department [name=" + name + ", employees=" + employees + "]";
}
}
public class ComplexSerializationDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 创建对象关系图
Department department1 = new Department("HR");
Department department2 = new Department("IT");
Employee employee1 = new Employee("Alice", 25, department1);
Employee employee2 = new Employee("Bob", 30, department2);
Employee employee3 = new Employee("Charlie", 35, department1);
department1.addEmployee(employee1);
department1.addEmployee(employee3);
department2.addEmployee(employee2);
// 序列化对象关系图到文件
FileOutputStream fileOut = new FileOutputStream("complexObject.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(department1);
out.writeObject(department2);
out.writeObject(employee1);
out.writeObject(employee2);
out.writeObject(employee3);
out.close();
fileOut.close();
System.out.println("对象关系图已序列化到文件complexObject.ser");
// 从文件中反序列化对象关系图
FileInputStream fileIn = new FileInputStream("complexObject.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
Department department1_ = (Department) in.readObject();
Department department2_ = (Department) in.readObject();
Employee employee1_ = (Employee) in.readObject();
Employee employee2_ = (Employee) in.readObject();
Employee employee3_ = (Employee) in.readObject();
in.close();
fileIn.close();
System.out.println("从文件complexObject.ser反序列化的对象关系图:");
System.out.println("Department 1: " + department1_);
System.out.println("Department 2: " + department2_);
System.out.println("Employee 1: " + employee1_);
System.out.println("Employee 2: " + employee2_);
System.out.println("Employee 3: " + employee3_);
}
}
🍁拓展知识仓
🍁Serializable 和 Externalizable 接门有何不同?
类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。
当试图对一个对象进行序列化的时候,如果遇到不支持 Serializable 接口的对象。在此情况下,将抛出NotSerializableException。
如果要序列化的类有父类,要想同时将在父类中定义过的变量持久化下来,那么父类也应该实现Java.io.Serializable接口。
Externalizable继承了Serializable,该接口中定义了两个抽象方法: writeExternal()与readExternal()。当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法,如果没有在这两个方法中定义序列化实现细节,那么序列化之后,对象内容为空。实现Externalizable接口的类必须要提供一个public的无参的构造器。
所以,实现Externalizable,并实现writeExternal0和readExternal()方法可以指定序列化哪些属性。
🍁如果序列化后的文件或者原始类被篡改,还能被反序列化吗?
🍁serialVersionUID 有何用途? 如果没定义会有什么问题?
序列化是将对象的状态信息转换为可存储或传输的形式的过程。我们都知道,Java对象是保存在JVM的堆内存中的,也就是说,如果JVM堆不存在了,那么对象也就跟着消失了。
而序列化提供了一种方案,可以让你在即使JVM停机的情况下也能把对象保存下来的方案。就像我们平时用的U盘一样。
把Java对象序列化成可存诸或传输的形式(如二进制流),比如保存在文件中。这样,当再次需要这人对象的时候,从文件中读取出二进制流,再从二进制流中反序列化出对象。
但是,虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化ID 是否 致,即serialVersionUID要求 一致。
在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。这样做是为了保证安全,因为文件存储中的内容可能被篡改。
当实现iava.io.Serializable接口的类没有显式地定义一个serialVersionUID变量时候,Java序列化机制会根据编译的Class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,如果Class文件没有发生变化,就算重偏译多次,serialVersionUID也不会变化的。但是,如果发生了变化,那么这个文件对应的serialVersionUID也就会发生变化。
基于以上原理,如果我们一个类实现了Serializable接口,但是没有定义serialVersionUID,然后序列化,在序列化之后,由于某些原因,我们对该类做了变更,重新启动应用后,我们相对之前序列化过的对象进行反序列化的话就会报错。
看一段代码,如何使用自定义的序列化方法,以及如何处理序列化过程中的异常:
import java.io.*;
import java.util.*;
class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private Set<String> skills;
public Employee(String name, int age, Set<String> skills) {
this.name = name;
this.age = age;
this.skills = skills;
}
public void displayInfo() {
System.out.println("Name: " + name);
System.out.println("Age: " + age);
System.out.println("Skills: " + skills);
}
// 自定义的序列化方法
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
out.writeObject(skills);
}
// 自定义的反序列化方法
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
name = in.readUTF();
age = in.readInt();
skills = (Set<String>) in.readObject();
}
}
public class SerializationDemo {
public static void main(String[] args) {
try {
// 创建一个 Employee 对象并序列化
Set<String> skills = new HashSet<>();
skills.add("Java");
skills.add("Python");
Employee employee = new Employee("John", 25, skills);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
employee.writeObject(oos); // 使用自定义的序列化方法
oos.close();
// 反序列化 Employee 对象(确保使用相同的 serialVersionUID)
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Employee deserializedEmployee = new Employee("", 0, new HashSet<>()); // 创建一个新的 Employee 对象用于反序列化
deserializedEmployee.readObject(ois); // 使用自定义的反序列化方法
ois.close();
deserializedEmployee.displayInfo(); // 输出员工的详细信息
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
在上面的示例中,为 Employee 类实现了 writeObject 和 readObject 方法,以自定义序列化和反序列化的过程。我们在 writeObject 方法中使用了 ObjectOutputStream 的 writeUTF、writeInt 和 writeObject 方法来写入员工的姓名、年龄和技能集合。在 readObject 方法中,我们使用 ObjectInputStream 的 readUTF、readInt 和 readObject 方法来读取这些值。我们还创建了一个新的 Employee 对象用于反序列化,并调用了自定义的反序列化方法。这个示例展示了如何处理序列化和反序列化过程中的异常,并展示了如何使用自定义的序列化方法来控制对象的序列化和反序列化过程。
🍁在Java中,有哪些好的序列化框架,有什么好处?
Java中常用的序列化框架:
java、 kryo、hessian、 protostuff、 gson、fastjson等。
Kryo: 速度快,序列化后体积小: 跨语言支持较复杂
Hessian: 默认支持跨语言: 效率不高
Protostuff: 速度快,基于protobuf; 需静态编译
Protostuff-Runtime: 无需静态编译,但序列化前需预先传入schema; 不支持无默认构造函数的类,反序列化时需用户自己初始化序列化后的对象,其只负责将该对象进行赋值
Java: 使用方便,可序列化所有类;速度慢,占空间