【Java原理系列】 可序列化接口Serializable原理示例源码分析
文章目录
原理
一个类实现了 Serializable
接口,就表示该类可以进行序列化。没有实现该接口的类将不会被序列化或反序列化。所有实现了 Serializable
接口的子类也是可序列化的。这个序列化接口没有方法或字段,仅用于标识可序列化的语义。
为了使非可序列化的类的子类能够进行序列化,子类可以承担起保存和恢复父类的公共、受保护以及(如果可访问)包级字段状态的责任。只有当扩展的类具有可访问的无参构造函数来初始化类的状态时,子类才能承担这种责任。如果不满足这个条件,则声明类为可序列化是错误的,错误会在运行时被检测到。
在反序列化过程中,非可序列化类的字段将使用类的公共或受保护的无参构造函数进行初始化。无参构造函数必须对可序列化的子类可访问。可序列化子类的字段将从流中恢复。
在遍历图形结构时,可能会遇到不支持 Serializable
接口的对象。在这种情况下,将抛出 NotSerializableException
异常,并标识非可序列化对象的类。
需要特殊处理序列化和反序列化过程的类必须实现以下具有确切签名的特殊方法:
private void writeObject(java.io.ObjectOutputStream out) throws IOException
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
-
writeObject
方法负责为特定类写入对象的状态,以便对应的readObject
方法可以恢复状态。可以通过调用out.defaultWriteObject
来调用默认的字段保存机制。该方法不需要关注其超类或子类的状态。通过使用writeObject
方法将各个字段写入ObjectOutputStream
,或者使用DataOutput
支持的原始数据类型的方法来保存状态。 -
readObject
方法负责从流中读取并恢复类的字段。它可以调用in.defaultReadObject
来调用默认的恢复对象非静态和非瞬态字段的机制。defaultReadObject
方法根据流中的信息,将流中保存的对象的字段与当前对象中同名的字段进行赋值。这处理了类演变添加新字段的情况。该方法不需要关注其超类或子类的状态。通过使用readObject
方法将各个字段从ObjectInputStream
中读取,或者使用DataOutput
支持的原始数据类型的方法来恢复状态。 -
readObjectNoData
方法负责在序列化流未列出给定类作为被反序列化对象的超类时,初始化特定类的状态。这可能发生在接收方使用与发送方不同版本的反序列化实例类,并且接收方的版本扩展了发送方版本未扩展的类。这也可能发生在序列化流被篡改的情况下,因此readObjectNoData
在源流存在敌意或不完整时很有用,以便正确地初始化反序列化对象。
需要指定在写入对象到流时使用替代对象的可序列化类,应该使用具有以下确切签名的特殊方法:
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
如果存在 writeReplace
方法,并且该方法对于序列化存在并且可以从对象类定义的方法中访问,则在序列化时将调用它。因此,该方法可以具有 private、protected 和 package-private 访问。子类访问该方法遵循 Java 的可访问性规则。
当从流中读取一个实例时,需要为其指定替代对象时,应该使用具有以下确切签名的特殊方法:
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
readResolve
方法遵循与 writeReplace
相同的调用规则和可访问性规则。
序列化运行时会为每个可序列化类关联一个版本号,称为 serialVersionUID
,在反序列化过程中用于验证序列化对象的发送方和接收方是否加载了与序列化兼容的类。如果接收方加载的类与发送方的类具有不同的 serialVersionUID
,则反序列化将导致 InvalidClassException
。可序列化类可以通过声明一个名为 “serialVersionUID” 的静态、最终的、类型为 long 的字段来显式地指定自己的 serialVersionUID
:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;
如果一个可序列化的类没有显式声明serialVersionUID,则序列化运行时会根据类的各个方面计算出一个默认的serialVersionUID值,具体细节请参考Java™对象序列化规范。
然而,强烈建议所有可序列化的类都显式地声明serialVersionUID值,因为默认的serialVersionUID计算对于可能因编译器实现而变化的类细节非常敏感,可能导致在反序列化过程中出现意外的InvalidClassExceptions。因此,为了保证在不同的java编译器实现中获得一致的serialVersionUID值,可序列化的类必须声明一个显式的serialVersionUID值。
此外,强烈建议显式的serialVersionUID声明在可能的情况下使用private修饰符,因为这样的声明仅适用于立即声明的类–serialVersionUID字段作为继承成员是没有用处的。数组类不能声明显式的serialVersionUID,所以它们总是具有默认的计算值,但是对于数组类,匹配的serialVersionUID值的要求被豁免了
注意事项
在使用Java的Serializable接口时,有一些注意事项需要注意:
- 被序列化的类必须实现Serializable接口。这个接口没有任何方法定义,只是一个标记接口,表示该类可以被序列化。
- 如果一个类的父类实现了Serializable接口,那么子类也会自动实现Serializable接口,无需再次声明。
- 在可序列化的类中,所有非瞬态(transient)的字段都将被默认序列化。如果某个字段不希望被序列化,可以使用transient关键字进行修饰。
- 序列化和反序列化的过程是通过ObjectOutputStream和ObjectInputStream来完成的。可以使用这两个类的writeObject和readObject方法来手动控制序列化和反序列化的过程。
- 当对一个对象进行序列化时,它引用的其他对象也会被递归地进行序列化。但是要注意,被引用的对象也必须实现Serializable接口,否则会抛出NotSerializableException异常。
- 序列化和反序列化的过程可能涉及到类版本的兼容性。当一个类进行了修改后,比如添加了新的字段或者修改了字段的类型,之前已经序列化的对象可能无法被正确地反序列化。为了解决这个问题,我们可以显式地声明serialVersionUID,并保持其值不变。
- 在进行网络传输或持久化存储时,要特别注意数据的安全性和完整性。可以使用加密算法对序列化的数据进行加密,或者使用数字签名来验证数据的完整性。
writeObject适用场景
writeObject
方法适用于以下场景:
-
自定义序列化行为:如果需要对对象的状态进行特殊处理,或者以不同于默认机制的方式序列化对象的字段,可以覆写
writeObject
方法。通过自定义序列化行为,可以控制序列化过程中哪些字段被写入输出流,以及如何处理这些字段。 -
保存额外的字段:除了对象的普通字段外,可能还有一些额外的信息需要在序列化时保存下来,以便在反序列化时恢复。可以在
writeObject
方法中将这些额外的字段写入输出流,并在readObject
方法中读取并恢复它们。 -
处理无法序列化的字段:某些字段由于各种原因(如不可序列化的类型、敏感信息等)无法被默认的序列化机制处理。通过覆写
writeObject
方法,可以自定义处理这些字段,例如先进行加密或忽略它们。
需要注意的是,在覆写writeObject
方法时,必须调用out.defaultWriteObject()
来使用默认的序列化机制将对象的非瞬态字段写入输出流。只有在确实需要自定义序列化行为或保存额外的字段时,才需要覆写writeObject
方法。
序列化不使用Serializable可以吗?
可以,还可以使用Externalizable接口
Externalizable 和Serializable区别
Externalizable
和Serializable
是Java中用于支持对象序列化的两个接口。它们之间的主要区别如下:
-
实现方式:为了使一个类支持序列化,可以实现
Serializable
接口,并且不需要覆写任何方法。而如果想要使用Externalizable
接口,则需要实现该接口,并且需要覆写writeExternal
和readExternal
方法。 -
序列化控制:通过实现
Serializable
接口,可以使用Java的默认序列化机制对对象进行序列化和反序列化。这种方式对类的内部结构没有特殊要求,所有非瞬态字段都将被自动序列化。而实现Externalizable
接口可以完全控制对象的序列化和反序列化过程,包括选择序列化哪些字段以及如何处理它们。 -
默认字段处理:
Serializable
接口使用默认机制对所有非瞬态字段进行序列化和反序列化,无需额外的代码。而Externalizable
接口则需要手动编写writeExternal
和readExternal
方法来控制每个字段的序列化和反序列化过程。 -
序列化大小:使用
Externalizable
接口序列化的对象通常比使用Serializable
接口序列化的对象更紧凑,因为Externalizable
允许你选择只序列化对象中的一部分字段。 -
版本控制:
Serializable
接口可以通过添加一个名为serialVersionUID
的特殊字段来提供版本控制。这个字段用于确保序列化和反序列化的对象版本一致。而Externalizable
接口没有直接提供版本控制的机制,需要开发人员自行实现。
总结来说,Serializable
接口提供了简单的、基于默认机制的对象序列化支持,而Externalizable
接口则允许更细粒度的控制,但也需要更多的开发工作。选择使用哪种接口取决于你对序列化过程的需求和控制程度。
多种示例
基本示例
序列化和反序列化的过程是通过ObjectOutputStream和ObjectInputStream来完成的。可以使用这两个类的writeObject和readObject方法来手动控制序列化和反序列化的过程
import java.io.*;
class Person implements Serializable {
private String name;
private int age;
private transient String address; // The field marked as transient will not be serialized
public Person(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
private void writeObject(ObjectOutputStream out) throws IOException {
// Manually control the serialization process
out.defaultWriteObject(); // Default serialization of name and age fields
// Custom serialization for the address field
out.writeUTF(address);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// Manually control the deserialization process
in.defaultReadObject(); // Default deserialization of name and age fields
// Custom deserialization for the address field
address = in.readUTF();
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", address=" + address + "]";
}
}
public class SerializationExample {
public static void main(String[] args) {
Person person = new Person("Alice", 25, "123 Main St.");
try {
FileOutputStream fileOut = new FileOutputStream("person.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(person); // Manually call writeObject to serialize
out.close();
fileOut.close();
FileInputStream fileIn = new FileInputStream("person.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
Person restoredPerson = (Person) in.readObject(); // Manually call readObject to deserialize
in.close();
fileIn.close();
System.out.println("Original Person: " + person);
System.out.println("Restored Person: " + restoredPerson);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
// Original Person: Person [name=Alice, age=25, address=123 Main St.]
// Restored Person: Person [name=Alice, age=25, address=123 Main St.]
示例1(子类)
如果一个类的父类实现了Serializable接口,那么子类也会自动实现Serializable接口,无需再次声明
package com.jess.test;
import java.io.*;
class Person1 implements Serializable {
private String name;
private int age;
public Person1(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person1 [name=" + name + ", age=" + age + "]";
}
}
class Employee extends Person1 {
private String employeeId;
public Employee(String name, int age, String employeeId) {
super(name, age);
this.employeeId = employeeId;
}
@Override
public String toString() {
return "Employee [employeeId=" + employeeId + ", " + super.toString() + "]";
}
}
public class SerializationExample1 {
public static void main(String[] args) {
Employee employee = new Employee("Alice", 30, "12345");
try {
FileOutputStream fileOut = new FileOutputStream("employee.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(employee); // Serialize the Employee object
out.close();
fileOut.close();
FileInputStream fileIn = new FileInputStream("employee.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
Employee restoredEmployee = (Employee) in.readObject(); // Deserialize into an Employee object
in.close();
fileIn.close();
System.out.println("Original Employee: " + employee);
System.out.println("Restored Employee: " + restoredEmployee);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
// Original Employee: Employee [employeeId=12345, Person1 [name=Alice, age=30]]
// Restored Employee: Employee [employeeId=12345, Person1 [name=Alice, age=30]]
示例2(transient)
在可序列化的类中,所有非瞬态(transient)的字段都将被默认序列化。如果某个字段不希望被序列化,可以使用transient关键字进行修饰。
在这个示例中,我们定义了一个Person
类,并实现了Serializable
接口。Person
类有两个字段:name
和age
。其中,age
字段使用了transient
关键字修饰,表示该字段不会被序列化。
在main
方法中,我们创建了一个Person
对象person
并将其序列化到文件中。然后,我们从文件中读取序列化的数据,并使用强制类型转换将其转换为Person
对象restoredPerson
。最后,我们输出原始的person
对象和恢复后的restoredPerson
对象来验证序列化和反序列化的结果。
由于age
字段被标记为transient
,所以它不会被序列化。因此,在输出结果中,age
字段的值在序列化和反序列化后保持为默认值0
package com.jess.test;
/**
* @program: gradleTest
* @description:
* @author: Mr.Lee
* @create: 2024-01-07 10:17
**/
import java.io.*;
class Person2 implements Serializable {
private