Java中的序列化。 没那么简单



序列化是将对象转换为字节序列的过程,然后可以从中完全还原该序列。 为什么需要这个? 事实是,在正常程序执行期间,从程序启动到结束,任何对象的最大寿命都是已知的。 序列化允许您以程序启动之间的相同方式扩展此框架并为对象“赋予生命”。

一切的一个额外好处是保留了跨平台。 无论您使用什么操作系统,序列化都会将对象转换为字节流,可以在任何OS上还原该字节流。 如果需要通过网络传输对象,则可以序列化对象,将其保存到文件中,然后通过网络将其传输给接收者。 他将能够恢复收到的对象。 序列化还允许您远程调用可能具有不同操作系统的不同计算机上的方法(Java RMI),并像在调用Java进程的计算机上一样使用它们。

实现序列化机制非常简单。 您的课程需要实现Serializable接口。 该接口是没有方法的标识符,但它告诉jvm该类的对象可以序列化。 由于序列化机制已连接到基本的输入/输出系统,并将对象转换为字节流,要执行该序列化,您需要创建输出流OutputStream ,将其打包在ObjectOutputStream中并调用writeObject()方法 要还原对象,您需要将InputStream打包到ObjectInputStream中,并调用readObject()方法

在序列化过程中,将与可序列化对象一起保存其对象图。 即 所有与此相关的对象,其他类的对象也将与此一起序列化。

考虑一个序列化Person类的对象的示例。

import java.io.*; class Home implements Serializable { private String home; public Home(String home) { this.home = home; } public String getHome() { return home; } } public class Person implements Serializable { private String name; private int countOfNiva; private String fatherName; private Home home; public Person(String name, int countOfNiva, String fatherName, Home home) { this.name = name; this.countOfNiva = countOfNiva; this.fatherName = fatherName; this.home = home; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", countOfNiva=" + countOfNiva + ", fatherName='" + fatherName + '\'' + ", home=" + home + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Home home = new Home("Vishnevaia 1"); Person igor = new Person("Igor", 2, "Raphael", home); Person renat = new Person("Renat", 2, "Raphael", home); //      ObjectOutputStream ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("person.out")); objectOutputStream.writeObject(igor); objectOutputStream.writeObject(renat); objectOutputStream.close(); //       ObjectInputStream ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream("person.out")); Person igorRestored = (Person) objectInputStream.readObject(); Person renatRestored = (Person) objectInputStream.readObject(); objectInputStream.close(); //    ByteArrayOutputStream ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream2 = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream2.writeObject(igor); objectOutputStream2.writeObject(renat); objectOutputStream2.flush(); //    ByteArrayInputStream ObjectInputStream objectInputStream2 = new ObjectInputStream( new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); Person igorRestoredFromByte = (Person) objectInputStream2.readObject(); Person renatRestoredFromByte = (Person) objectInputStream2.readObject(); objectInputStream2.close(); System.out.println("Before Serialize: " + "\n" + igor + "\n" + renat); System.out.println("After Restored From Byte: " + "\n" + igorRestoredFromByte + "\n" + renatRestoredFromByte); System.out.println("After Restored: " + "\n" + igorRestored + "\n" + renatRestored); } } 

结论:

 Before Serialize: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@355da254} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@355da254} After Restored From Byte: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} After Restored: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} 

在此示例中,将创建Home类来演示在序列化Person对象时,该对象的图也随之序列化。 Home类还必须实现Serializable接口,否则将发生java.io.NotSerializableException 。 该示例还描述了使用ByteArrayOutputStream类进行序列化。

从程序执行的结果可以得出一个有趣的结论: 在序列化之前还原具有对同一对象的引用的对象时,该对象将仅被还原一次 。 恢复后,可以在对象的相同链接上看到这一点:

 After Restored From Byte: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} After Restored: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} 

但是,还可以看到,当使用两个输出流(我们有ObjectInputStreamByteArrayOutputStream )进行记录时,尽管先前已经在其中一个流中创建了原始对象,但仍将重新创建该原始对象。 我们在两个流中收到的原始对象的不同地址上看到了这一点。 事实证明,如果使用一个输出流进行序列化,然后还原对象,那么我们可以保证还原完整的对象网络,而不会造成不必要的重复。 当然,在程序执行期间,对象的状态可能会更改,但这是出于程序员的良心。

问题

该示例还显示,还原对象时,可能会发生ClassNotFoundException 。 这是什么原因呢? 事实是,我们可以轻松地将Person类的对象序列化为文件,并将其通过网络传输给我们的朋友,后者可以通过根本不存在Person类的另一个应用程序来还原该对象。

它的序列化。 怎么做?

如果您想自己管理序列化怎么办? 例如,您的对象存储用户的用户名和密码。 您需要对其进行序列化,以通过网络进一步传输。 在这种情况下,通过密码极其不可靠。 如何解决这个问题? 有两种方法。 首先,使用transient关键字。 其次,与其实现Serializable兴趣, 不如使用其扩展名-Externalizable接口。 考虑用于比较它们的第一方法和第二方法的示例。

第一种方法-使用瞬态进行序列化

 import java.io.*; public class Logon implements Serializable { private String login; private transient String password; public Logon(String login, String password) { this.login = login; this.password = password; } @Override public String toString() { return "Logon{" + "login='" + login + '\'' + ", password='" + password + '\'' + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Logon igor = new Logon("IgorIvanovich", "Khoziain"); Logon renat = new Logon("Renat", "2500RUB"); System.out.println("Before: \n" + igor); System.out.println(renat); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Externals.out")); out.writeObject(igor); out.writeObject(renat); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Externals.out")); igor = (Logon) in.readObject(); renat = (Logon) in.readObject(); System.out.println("After: \n" + igor); System.out.println(renat); } } 

结论:

 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} After: Logon{login='IgorIvanovich', password='null'} Logon{login='Renat', password='null'} 

第二种方式-使用Externalizable接口的实现进行序列化

 import java.io.*; public class Logon implements Externalizable { private String login; private String password; public Logon() { } public Logon(String login, String password) { this.login = login; this.password = password; } @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(login); } @Override public String toString() { return "Logon{" + "login='" + login + '\'' + ", password='" + password + '\'' + '}'; } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { login = (String) in.readObject(); } public static void main(String[] args) throws IOException, ClassNotFoundException { Logon igor = new Logon("IgorIvanovich", "Khoziain"); Logon renat = new Logon("Renat", "2500RUB"); System.out.println("Before: \n" + igor); System.out.println(renat); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Externals.out")); out.writeObject(igor); out.writeObject(renat); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Externals.out")); igor = (Logon) in.readObject(); renat = (Logon) in.readObject(); System.out.println("After: \n" + igor); System.out.println(renat); } } 

结论:

 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} After: Logon{login='IgorIvanovich', password='null'} Logon{login='Renat', password='null'} 

引起您注意的两个选项之间的第一个区别是代码的大小。 在实现Externalizable接口时,我们需要重写两个方法: writeExternal()readExternal() 。 在writeExternal()方法中,我们指示哪些字段将被序列化,以及如何在readExternal()中读取它们。 当使用瞬态一词时我们明确指出哪些字段不需要序列化。 我们还注意到,在第二种方法中,我们显式创建了一个默认的构造函数,而且是一个公共的构造函数。 为什么要这样做? 让我们尝试在没有此构造函数的情况下运行代码。 并查看输出:

 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} Exception in thread "main" java.io.InvalidClassException: Logon; no valid constructor at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:169) at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:874) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2043) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431) at Logon.main(Logon.java:45) 

我们有java.io.InvalidClassException异常。 这是什么原因呢? 如果沿着堆栈跟踪,可以发现ObjectStreamClass类的构造函数中有几行

  if (externalizable) { cons = getExternalizableConstructor(cl); } else { cons = getSerializableConstructor(cl); 

对于Externalizable接口,将调用getExternalizableConstructor()构造函数方法在其中,我们将通过Reflection尝试获取要为其还原对象的类的默认构造函数。 如果我们找不到它,或者它不是public ,那么我们会得到一个例外。 您可以按以下方式解决这种情况:不要在类中显式创建任何构造函数,并使用setters填充字段并使用getters获取值。 然后,在编译类时,将创建一个默认构造函数,该构造函数可用于getExternalizableConstructor() 。 对于Serializable, getSerializableConstructor()方法获取Object类的构造函数,并从该类中查找所需的类,如果找不到该类,则将收到ClassNotFoundException异常。 事实证明, SerializableExternalizable之间的主要区别在于前者不需要构造函数来创建对象恢复。 它只会从字节中完全恢复。 第二,在恢复期间,将首先使用构造函数在声明点创建对象,然后将序列化过程中接收到的字节中其字段的值写入其中。 就个人而言,我更喜欢第一种方法,它要简单得多。 而且,即使我们仍然需要设置序列化行为,我们也不能使用Externalizable ,也不能通过向其添加(而不覆盖) writeObject()readObject()方法来实现Serializable 。 但是,为了使他们能够“工作”,必须严格遵守其签名。

 import java.io.*; public class Talda implements Serializable { private String name; private String description; public Talda(String name, String description) { this.name = name; this.description = description; } private void writeObject(ObjectOutputStream stream) throws IOException { stream.defaultWriteObject(); System.out.println("Our writeObject"); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); System.out.println("Our readObject"); } @Override public String toString() { return "Talda{" + "name='" + name + '\'' + ", description='" + description + '\'' + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Talda partizanka = new Talda("Partizanka", "Viiiski"); System.out.println("Before: \n" + partizanka); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Talda.out")); out.writeObject(partizanka); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Talda.out")); partizanka = (Talda) in.readObject(); System.out.println("After: \n" + partizanka); } } 

结论:

 Before: Talda{name='Partizanka', description='Viiiski'} Our writeObject Our readObject After: Talda{name='Partizanka', description='Viiiski'} 

在我们添加的方法中,将调用defaultWriteObject()defaultReadObject() 。 他们负责默认的序列化,就好像没有我们添加的方法也可以工作一样。

实际上,这只是冰山一角,如果您继续研究序列化的机制,那么很有可能您会发现更多细微差别,我们会说:“序列化……不是那么简单。”

Source: https://habr.com/ru/post/zh-CN431524/


All Articles