在上一篇文章中,我们介绍了在Java中创建对象的5种不同方法 ,我解释了如何对序列化对象进行反序列化以创建新对象,并且在此博客中,我将详细讨论序列化和反序列化。
我们将以下面的Employee
类对象为例进行说明
// If we use Serializable interface, static and transient variables do not get serialize Employee class implements Serializable { // This serialVersionUID field is necessary for Serializable as well as Externalizable to provide version control, // Compiler will provide this field if we do not provide it which might change if we modify the class structure of our class, and we will get InvalidClassException, // If we provide value to this field and do not change it, serialization-deserialization will not fail if we change our class structure. private static final long serialVersionUID = 2L; private final String firstName; // Serialization process do not invoke the constructor but it can assign values to final fields private transient String middleName; // transient variables will not be serialized, serialised object holds null private String lastName; private int age; private static String department; // static variables will not be serialized, serialised object holds null public Employee(String firstName, String middleName, String lastName, int age, String department) { this .firstName = firstName; this .middleName = middleName; this .lastName = lastName; this .age = age; Employee.department = department; validateAge(); } private void validateAge() { System.out.println( "Validating age." ); if (age < 18 || age > 70 ) { throw new IllegalArgumentException( "Not a valid age to create an employee" ); } } @Override public String toString() { return String.format( "Employee {firstName='%s', middleName='%s', lastName='%s', age='%s', department='%s'}" , firstName, middleName, lastName, age, department); } // Custom serialization logic, // This will allow us to have additional serialization logic on top of the default one eg encrypting object before serialization private void writeObject(ObjectOutputStream oos) throws IOException { System.out.println( "Custom serialization logic invoked." ); oos.defaultWriteObject(); // Calling the default serialization logic } // Custom deserialization logic // This will allow us to have additional deserialization logic on top of the default one eg decrypting object after deserialization private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { System.out.println( "Custom deserialization logic invoked." ); ois.defaultReadObject(); // Calling the default deserialization logic // Age validation is just an example but there might some scenario where we might need to write some custom deserialization logic validateAge(); } }
什么是序列化和反序列化
在Java中,我们创建了几个对象,这些对象会相应地存活和死亡,并且当JVM死亡时,每个对象肯定会死亡,但是有时我们可能想在多个JVM之间重用一个对象,或者可能希望通过网络将对象传输到另一台机器。
好吧, 序列化允许我们将对象的状态转换为字节流,然后可以将其保存到本地磁盘上的文件中,或者通过网络发送到任何其他计算机。 反序列化使我们可以逆转该过程,这意味着将序列化的字节流再次转换为对象。
简而言之,对象序列化是将对象的状态保存到字节序列中的过程, 反序列化是从这些字节中重建对象的过程。 通常,完整的过程称为序列化,但我认为最好将两者都分类为更清晰。
序列化过程与平台无关,可以在一个平台上反序列化在一个平台上序列化的对象。
要将对象序列化和反序列化为文件,我们需要调用ObjectOutputStream.writeObject()
和ObjectInputStream.readObject()
如以下代码所示:
public class SerializationExample { public static void main(String[] args) throws IOException, ClassNotFoundException { Employee empObj = new Employee( "Shanti" , "Prasad" , "Sharma" , 25 , "IT" ); System.out.println( "Object before serialization => " + empObj.toString()); // Serialization serialize(empObj); // Deserialization Employee deserialisedEmpObj = deserialize(); System.out.println( "Object after deserialization => " + deserialisedEmpObj.toString()); } // Serialization code static void serialize(Employee empObj) throws IOException { try (FileOutputStream fos = new FileOutputStream( "data.obj" ); ObjectOutputStream oos = new ObjectOutputStream(fos)) { oos.writeObject(empObj); } } // Deserialization code static Employee deserialize() throws IOException, ClassNotFoundException { try (FileInputStream fis = new FileInputStream( "data.obj" ); ObjectInputStream ois = new ObjectInputStream(fis)) { return (Employee) ois.readObject(); } } }
只有实现Serializable的类才能被序列化
类似于序列化中Java克隆的Cloneable接口,我们有一个标记接口Serializable,其作用类似于JVM的标志。 直接或通过其父级实现Serializable
接口的任何类都可以序列化,而没有实现Serializable
则不能进行序列化。
Java的默认序列化过程是完全递归的,因此,每当我们尝试序列化一个对象时,序列化过程都会尝试用我们的类( static
和transient
字段除外)序列化所有字段(原始和引用)。
当一个类实现Serializable
接口时,其所有子类也都可以序列化。 但是,当一个对象引用另一个对象时,这些对象必须分别实现Serializable
接口。 如果我们的类甚至具有对非Serializable
类的单个引用,则JVM将抛出NotSerializableException
。
为什么Object不能实现Serializable?
现在出现一个问题,如果序列化是非常基本的功能,并且任何不能实现Serializable
类都不能Serializable
化,那么为什么Serializable不是由Object
本身实现的呢?通过这种方式,我们所有的对象都可以默认序列化。
Object
类未实现Serializable
接口,因为我们可能不想序列化所有对象,例如,对线程进行序列化没有任何意义,因为在JVM中运行的线程将使用系统的内存,并将其持久化并尝试在JVM中运行毫无意义。
瞬态和静态字段不会序列化
如果我们要序列化一个对象但不想序列化某些特定字段,则可以将这些字段标记为
短暂的 。
所有静态字段都属于类而不是对象,并且序列化过程会序列化对象,因此无法序列化静态字段。
- 序列化并不关心字段的访问修饰符,例如
private
。 所有非瞬态和非静态字段都被视为对象持久状态的一部分,并且可以序列化。 - 我们只能在构造函数中将值分配给final字段,而序列化过程不会调用任何构造函数,但仍可以将值分配给final字段。
什么是serialVersionUID,为什么要声明它?
假设我们有一个类,并且已将其对象序列化为磁盘上的文件,并且由于一些新要求,我们在类中添加/删除了一个字段。 现在,如果我们尝试反序列化已经序列化的对象,我们将得到InvalidClassException
,为什么?
我们之所以得到它,是因为默认情况下,JVM将版本号与每个可序列化的类相关联,以控制类的版本控制。 它用于验证序列化和反序列化的对象具有相同的属性,从而与反序列化兼容。 版本号保存在一个名为serialVersionUID
的字段中。 如果可序列化的类未声明
serialVersionUID
JVM将在运行时自动生成一个。
如果我们更改类结构,例如删除/添加字段,则版本号也会更改,并且根据JVM,我们的类与序列化对象的类版本不兼容。 这就是为什么我们会得到例外,但是如果您真的考虑过它,为什么应该仅仅因为我添加了一个字段就抛出该例外? 不能仅将字段设置为其默认值,然后下次将其写出吗?
是的,可以通过手动提供serialVersionUID
字段并确保始终相同来完成此操作。 强烈建议每个可序列化的类声明其serialVersionUID
因为生成的类是编译器相关的,因此可能导致意外的InvalidClassExceptions。
您可以使用JDK发行版随附的实用程序,称为
serialver
以查看默认情况下该代码是什么(默认情况下只是对象的哈希码)。
使用writeObject和readObject方法自定义序列化和反序列化
JVM可以完全控制默认序列化过程中的对象序列化,但是使用默认序列化过程有很多缺点,其中包括:
- 它无法处理无法序列化的字段的序列化。
- 反序列化过程在创建对象时不会调用构造函数,因此它无法调用构造函数提供的初始化逻辑。
但是我们可以在Java类中覆盖此默认的序列化行为,并提供一些其他逻辑来增强正常过程。 这可以通过在我们要序列化的类中提供两个方法writeObject
和readObject
来完成:
// Custom serialization logic will allow us to have additional serialization logic on top of the default one eg encrypting object before serialization private void writeObject(ObjectOutputStream oos) throws IOException { // Any Custom logic oos.defaultWriteObject(); // Calling the default serialization logic // Any Custom logic } // Custom deserialization logic will allow us to have additional deserialization logic on top of the default one eg decrypting object after deserialization private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { // Any Custom logic ois.defaultReadObject(); // Calling the default deserialization logic // Any Custom logic }
将两个方法都声明为私有是必要的(公共方法将不起作用),因此除了JVM外,其他任何东西都看不到它们。 这也证明方法既不被继承也不被覆盖或重载。 JVM自动检查这些方法并在序列化/反序列化过程中调用它们。 JVM可以调用这些私有方法,但其他对象则不能,因此,类的完整性得以维护,序列化协议可以继续正常工作。
即使提供了那些专用的私有方法,通过调用ObjectOutputStream.writeObject()
或ObjectInputStream.readObject()
,对象序列化的工作方式也相同。
对ObjectOutputStream.writeObject()
或ObjectInputStream.readObject()
的调用将启动序列化协议。 首先,检查对象以确保其实现了Serializable
,然后检查该对象是否提供了这些私有方法中的任何一个。 如果提供了它们,则将流类作为参数传递给这些方法,从而使代码可以控制其用法。
我们可以调用ObjectOutputStream.defaultWriteObject()
和
这些方法中的ObjectInputStream.defaultReadObject()
获得默认的序列化逻辑。 这些调用听起来很像-他们执行序列化对象的默认写入和读取操作,这很重要,因为我们没有替换正常的过程,而只是添加了它。
这些私有方法可用于您要在序列化过程中进行的任何自定义,例如,可以将加密添加到输出中,并将解密添加到输入中(请注意,字节以明文形式写入和读取,完全没有混淆)。 它们可能被用来向流中添加额外的数据,也许是公司的版本代码,其可能性实际上是无限的。
停止序列化和反序列化
假设我们有一个从其父级获得序列化功能的类,这意味着我们的类是从另一个实现Serializable
类扩展的。
这意味着任何人都可以序列化和反序列化我们类的对象。 但是,如果我们不希望对类进行序列化或反序列化(例如,我们的类是单例)并且希望防止任何新对象的创建,记住反序列化过程会创建一个新对象 。
要停止类的序列化,我们可以再次使用上述私有方法抛出NotSerializableException
。 现在,任何对我们的对象进行序列化或反序列化的尝试都将始终导致引发异常。 并且由于这些方法被声明为private
方法,因此没有人可以覆盖您的方法并进行更改。
private void writeObject(ObjectOutputStream oos) throws IOException { throw new NotSerializableException( "Serialization is not supported on this object!" ); } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { throw new NotSerializableException( "Serialization is not supported on this object!" ); }
但是,这违反了《里斯科夫换人原则》。 和
writeReplace和readResolve方法可用于实现类似行为的单例。 这些方法用于允许对象在ObjectStream中为其自身提供替代表示。 简单来说,readResolve可用于更改通过readObject方法反序列化的数据,而writeReplace可用于更改通过writeObject序列化的数据。
Java 序列化还可以用于深度克隆对象 。 Java克隆是Java社区中最有争议的话题,它的确有其缺点,但是在对象完全满足Java克隆的强制条件之前,它仍然是创建对象副本的最流行和最简单的方法。 我在3篇文章的Java克隆系列中详细介绍了克隆 ,其中包括Java克隆和克隆类型(浅和深)等文章, 并带有示例 , Java克隆–复制构造器与克隆 , Java克隆–甚至复制构造器都不是如果您想了解更多有关克隆的知识,请充分阅读它们。
结论
- 序列化是将对象的状态保存为字节序列的过程,然后可以将其存储在文件中或通过网络发送, 反序列化是从这些字节中重建对象的过程。
- 只有
Serializable
接口的子类可以序列化。 - 如果我们的类未实现
Serializable
接口,或者该类具有对非Serializable
类的引用,则JVM将抛出NotSerializableException
。 - 所有
transient
和static
字段都不会序列化。 -
serialVersionUID
用于验证序列化和反序列化的对象具有相同的属性,从而与反序列化兼容。 - 我们应该在我们的类中创建一个
serialVersionUID
字段,这样,如果我们更改类结构(添加/删除字段),JVM将不会通过InvalidClassException
。 如果我们不提供它,那么JVM提供的类可能会随着类结构的改变而改变。 - 通过提供
writeObject
和readObject
方法的实现,我们可以覆盖Java类内部的默认序列化行为。 - 我们可以从
writeObject
和readObject
方法调用ObjectOutputStream.defaultWriteObject()
和ObjectInputStream.defaultReadObject
以获得默认的序列化和反序列化逻辑。 - 如果我们不希望类被序列化或反序列化,则可以从
writeObject
和readObject
抛出NotSerializableException
异常。
可以使用“可Externalizable
接口进一步定制和增强Java序列化过程,我已经在“ 如何通过使用可外部化接口在Java中自定义序列化”中进行了解释。
我还写了一系列文章,解释了有效Java的项目编号74到78,它进一步讨论了如何增强Java序列化过程,请继续阅读,如果愿意的话。
您可以在此Github存储库中找到本文的完整源代码,请随时提供宝贵的反馈。
翻译自: https://www.javacodegeeks.com/2019/08/serialization-everything-java-serialization-explained.html