节选自《Java 精简入门教程》
进入下面链接或点击文章末尾的 “阅读原文” 查看全部教程
https://nowjava.com/book/java-simplify
什么是对象序列化?
在 序列化 过程中,会以一种特殊的二进制格式存储一个对象及其元数据(比如对象的类名称及其属性名称)的状态。将对象存储为此格式 — 序列化 它 — 可保留在需要时重新构成(或 反序列化 )该对象所必需的全部信息。
对象序列化有两种主要用例:
对象持久化 — 将对象的状态存储在一种永久的持久性机制中,比如数据库
对象远程传输 — 将对象发送至另一个计算机或系统
java.io.Serializable
实现序列化的第一步是让对象能够使用该机制。希望能够序列化的每个对象都必须实现一个名为 java.io.Serializable
的接口:
import java.io.Serializable;public class Person implements Serializable { // etc...}
在此示例中,Serializable
接口将 Person
类(和 Person
的每个子类的对象)向运行时标记为 serializable
。
如果 Java 运行时尝试序列化您的对象,无法序列化的对象的每个属性会导致它抛出一个 NotSerializableException
。可以使用 transient
关键字管理此行为,告诉运行时不要尝试序列化一些属性。在这种情况下,您应该负责确保恢复这些属性(在必要时),以便您的对象能正常运行。
序列化一个对象
现在我们通过一个示例,尝试将刚学到的 Java I/O 知识与现在学习的序列化知识结合起来。
假设您创建并填充一个包含 Employee
对象的 List
,然后希望将该 List
序列化为一个 OutputStream
,在本例中是序列化为一个文件。该过程如清单 12 所示。
清单 12. 序列化一个对象
public class HumanResourcesApplication { private static final Logger log = Logger.getLogger(HumanResourcesApplication.class.getName()); private static final String SOURCE_CLASS = HumanResourcesApplication.class.getName(); public static ListcreateEmployees() { List ret = new ArrayList(); Employee e = new Employee("Jon Smith", 45, 175, 75, "BLUE", Gender.MALE, "123-45-9999", "0001", BigDecimal.valueOf(100000.0)); ret.add(e); // e = new Employee("Jon Jones", 40, 185, 85, "BROWN", Gender.MALE, "223-45-9999", "0002", BigDecimal.valueOf(110000.0)); ret.add(e); // e = new Employee("Mary Smith", 35, 155, 55, "GREEN", Gender.FEMALE, "323-45-9999", "0003", BigDecimal.valueOf(120000.0)); ret.add(e); // e = new Employee("Chris Johnson", 38, 165, 65, "HAZEL", Gender.UNKNOWN, "423-45-9999", "0004", BigDecimal.valueOf(90000.0)); ret.add(e); // Return list of Employees return ret; } public boolean serializeToDisk(String filename, List employees) { final String METHOD_NAME = "serializeToDisk(String filename, List employees)"; boolean ret = false;// default: failed File file = new File(filename); try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(file))) { log.info("Writing " + employees.size() + " employees to disk (using Serializable)..."); outputStream.writeObject(employees); ret = true; log.info("Done."); } catch (IOException e) { log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + file.getName() + ", message = " + e.getLocalizedMessage(), e); } return ret; }
第一步是创建这些对象,在 createEmployees()
中使用 Employee
的特殊化构造函数和一些属性值来完成该工作。接下来创建一个 OutputStream
(在本例中为 FileOutputStream
),然后在该流上调用 writeObject()
。writeObject()
是一个方法,它使用 Java 序列化将一个对象序列化为流。
在此示例中,您将 List
对象(以及它包含的 Employee
对象)存储在一个文件中,但同样的技术可用于任何类型的序列化。
要成功运行清单 12 中的代码,您可以使用 JUnit 测试,如下所示:
public class HumanResourcesApplicationTest { private HumanResourcesApplication classUnderTest; private List testData; @Before public void setUp() { classUnderTest = new HumanResourcesApplication(); testData = HumanResourcesApplication.createEmployees(); } @Test public void testSerializeToDisk() { String filename = "employees-Junit-" + System.currentTimeMillis() + ".ser"; boolean status = classUnderTest.serializeToDisk(filename, testData); assertTrue(status); }}
反序列化对象
序列化对象的唯一目的就是为了能够重新构成或反序列化它。清单 13 读取刚序列化的文件并对其内容反序列化,然后恢复 Employee
对象的 List
的状态。
清单 13. 反序列化对象
public class HumanResourcesApplication { private static final Logger log = Logger.getLogger(HumanResourcesApplication.class.getName()); private static final String SOURCE_CLASS = HumanResourcesApplication.class.getName(); @SuppressWarnings("unchecked") public List deserializeFromDisk(String filename) { final String METHOD_NAME = "deserializeFromDisk(String filename)"; List ret = new ArrayList<>(); File file = new File(filename); int numberOfEmployees = 0; try (ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file))) { List employees = (List)inputStream.readObject(); log.info("Deserialized List says it contains " + employees.size() + " objects..."); for (Employee employee : employees) { log.info("Read Employee:" + employee.toString()); numberOfEmployees++; } ret = employees; log.info("Read " + numberOfEmployees + " employees from disk."); } catch (FileNotFoundException e) { log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + file.getName() + ", message = " + e.getLocalizedMessage(), e); } catch (IOException e) { log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "IOException occurred, message = " + e.getLocalizedMessage(), e); } catch (ClassNotFoundException e) { log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "ClassNotFoundException, message = " + e.getLocalizedMessage(), e); } return ret; }}
同样地,要成功运行清单 13 中的代码,可以使用一个类似这样的 JUnit 测试:
public class HumanResourcesApplicationTest { private HumanResourcesApplication classUnderTest; private List testData; @Before public void setUp() { classUnderTest = new HumanResourcesApplication(); } @Test public void testDeserializeFromDisk() { String filename = "employees-Junit-" + System.currentTimeMillis() + ".ser"; int expectedNumberOfObjects = testData.size(); classUnderTest.serializeToDisk(filename, testData); List employees = classUnderTest.deserializeFromDisk(filename); assertEquals(expectedNumberOfObjects, employees.size()); }}
对于大多数应用程序的用途,将对象标记为 serializable
是执行序列化工作时唯一需要担心的问题。需要明确地序列化和反序列化对象时,可使用清单 12 和清单 13 中所示的技术。但是,随着应用程序对象不断演变,以及在它们之中添加和删除属性,序列化会变得更加复杂。
serialVersionUID
回想中间件和远程对象通信的发展初期,开发人员主要负责控制其对象的”连接格式”,随着技术开始演变,这引发了大量头疼的问题。
假设您向一个对象添加了一个属性,重新编译了它,然后将该代码重新分发到一个应用程序集群中的每个计算机上。该对象存储在一个具有某种代码版本的计算机中,但其他可能具有不同代码版本的计算机访问该对象。这些计算机尝试反序列化该对象时,常常会发生糟糕的事情。
Java 序列化元数据 — 所包含的二进制序列化格式的信息 — 很复杂,而且解决了困扰早期中间件开发人员的许多问题。但它并非能解决所有问题。
Java 序列化使用一个名为 serialVersionUID
的特性来帮助您处理序列化场景中的不同对象版本问题。不需要在对象上声明此特性;默认情况下,Java 平台会使用一种算法并根据类的属性、它的类名称以及在庞大的本地集群中的位置来计算值。在大多数情况下,该算法都能正常运行。但是,如果添加或删除一个属性,这个动态生成的值就会发生变化,而且 Java 运行时会抛出 InvalidClassException
。
要想避免这种情况,可养成明确声明 serialVersionUID
的习惯:
import java.io.Serializable; public class Person implements Serializable { private static final long serialVersionUID = 20100515; // etc... }
我建议您为 serialVersionUID
版本号使用某种模式(我在前面的示例中使用了当前日期)。而且应将 serialVersionUID
声明为 private static final
和 long
类型。
您可能想知道何时应更改此特性。简单的答案是,只要对类执行了不兼容的更改(这通常意味着删除了一个属性),就应该更改它。如果在一台计算机上拥有该对象的一个已删除了某个属性的版本,而且将该对象远程传输至一台计算机,其中包含的对象版本需要该属性,此时就会发生怪异的事情。这时就可以使用 Java 平台的内置 serialVersionUID
进行检查。
作为一条经验规则,任何时候添加或删除一个类特性(也就是属性和方法),都需要更改它的 serialVersionUID
。在连接的另一端获得一个 InvalidClassException
,比由不兼容的类更改导致应用程序错误要更好一些。
--
知识分享,时代前行!
~~ 时代Java
还有更多好文章……
请查看历史文章和官网,
↓有分享,有收获~