内容


第 22 单元:Java 序列化

将对象状态存储为二进制格式

Comments

开始之前

本单元是 “Java 编程入门” 学习路径的一部分。尽管各单元中讨论的概念具有独立性,但实践组件是在您学习各单元的过程中逐步建立起来的,推荐您在继续学习之前复习 前提条件、设置和单元细节

单元目标

  • 了解对象序列化是什么以及为什么需要使用它
  • 了解让对象可序列化,序列化对象和去序列化对象的语法
  • 能够在序列化场景中处理同一个对象的不同版本

什么是对象序列化?

序列化 的过程中,对象和它的元数据(比如对象的类名和它的属性名称)存储为一种特殊的二进制格式。将对象存储为这种格式(序列化 它)会保留所有必要的信息,使您在需要时能够重建(或去序列化)对象。

对象序列化的两个主要使用场景包括:

  • 对象持久化:将对象的状态存储在一种永久的持久性机制中,比如数据库
  • 对象远程存储:将对象发送到另一台计算机或另一个系统

java.io.Serializable

实现序列化的第一步是使对象能够使用该机制。您希望能够序列化的每个对象必须实现一个名为 java.io.Serializable 的接口:

import java.io.Serializable;
public class Person implements Serializable {
  // etc...
}

在此示例中,Serializable 接口将 Person 类(和 Person 的每个子类的对象)向运行时标记为 serializable

如果 Java 运行时尝试序列化您的对象,无法序列化的对象的每个属性会导致它抛出一个 NotSerializableException。可以使用 transient 关键字管理此行为,告诉运行时不要尝试序列化一些属性。在这种情况下,您应该负责确保恢复这些属性(在必要时),以便您的对象能正常运行。

序列化对象

现在,我们将通过一个示例,尝试将您在 第 21 单元 中学到的 Java I/O 知识与您现在学习的序列化知识结合起来。

假设您创建并填充一个包含 Employee 对象的 List,然后希望将该 List 序列化为一个 OutputStream,在本例中则是序列化为一个文件。该过程如清单 1 所示。

清单 1. 序列化一个对象
public class HumanResourcesApplication {
  private static final Logger log = Logger.getLogger(HumanResourcesApplication.class.getName());
  private static final String SOURCE_CLASS = HumanResourcesApplication.class.getName();
  
  public static List<Employee> createEmployees() {
    List<Employee> ret = new ArrayList<Employee>();
    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<Employee> employees) {
    final String METHOD_NAME = "serializeToDisk(String filename, List<Employee> 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 对象)存储在一个文件中,但同样的技术可用于任何类型的序列化。

要成功运行 清单 1 中的代码,您可以使用 JUnit 测试,如下所示:

public class HumanResourcesApplicationTest {

  private HumanResourcesApplication classUnderTest;
  private List<Employee> 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);
  }

}

去序列化对象

序列化对象的唯一目的就是为了能够重建或去序列化它。清单 2 读取您刚序列化的文件并去序列化它的内容,然后恢复包含 Employee 对象的 List 的状态。

清单 2. 去序列化对象
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<Employee> deserializeFromDisk(String filename) {
    final String METHOD_NAME = "deserializeFromDisk(String filename)";
    
    List<Employee> ret = new ArrayList<>();
    File file = new File(filename);
    int numberOfEmployees = 0;
    try (ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file))) {
      List<Employee> employees = (List<Employee>)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;
  }
  
}

同样地,要成功运行 清单 2 中的代码,可以使用一个类似这样的 JUnit 测试:

public class HumanResourcesApplicationTest {
  
  private HumanResourcesApplication classUnderTest;
  
  private List<Employee> 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<Employee> employees = classUnderTest.deserializeFromDisk(filename);
    assertEquals(expectedNumberOfObjects, employees.size());
  }

}

对于大部分应用用途,将对象标记为 serializable 是在执行序列化时唯一需要担忧的问题。需要显式序列化和去序列化对象时,可以使用 清单 1清单 2 中所示的技术。但随着应用程序对象不断演变,以及在它们之中添加和删除属性,序列化会变得更加复杂。

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 finallong 类型。

您可能想知道应在何时更改此特性。简单的答案是,只要对代码执行了不兼容的更改(这通常意味着您添加或删除了某个属性),就应该更改它。如果您在一台计算机上的对象版本中添加或删除了该属性,而且该对象远程传输到了一台包含缺少或需要该属性的对象版本的计算机,就会发生一些怪异的事情。这时就可以使用 Java 平台的内置 serialVersionUID 进行检查。

作为一条经验规则,无论任何时候添加或删除一个类特征(即属性或其他任何实例级状态变量),都需要更改它的 serialVersionUID。在连接的另一端获得一个 java.io.InvalidClassException,比由不兼容的类更改导致应用程序错误要好。

测试您的理解情况

  1. 如何让类 “可序列化”?
    1. 使用 Java 序列化没有特殊需求;它已内置到 Java 语言中。
    2. 每个需要序列化的类,都至少必须实现 java.io.Serializable 接口。
    3. 要使用 Java 序列化,必须实现 hashCode()equals()
    4. 属性值必须按字母顺序声明,否则它们无法正确地序列化和去序列化。
    5. 上述选项都不是
  2. 如果您尝试序列化某个对象,却没有在它的类上为它声明 serialVersionUID,会发生什么?
    1. Java 序列化运行时将基于类的声明为此字段计算一个默认值,并正常地继续序列化。
    2. 在尝试序列化该对象时,您会获得一个 NotSerializableException
    3. 在将该类载入内存中时,JVM 会抛出一个 NoSerialVersionUIDFieldDeclared 异常。
    4. 该对象将序列化,但它无法去序列化 — 这是一项安全措施,用于防止执行不兼容的类更改。
    5. 上述选项都不是
  3. 选择 serialVersionUID 的正确声明:
    1. public static final String serialVersionUID = "1";
    2. private long serialVersionUID = 1L;
    3. private static final long serialVersionUID = 12345L;
    4. private static final String serialVersionUID = "Sdflkjsdfgd0980980(DF)(*)90";
    5. 上述选项都不是
  4. 作为最佳实践,在哪些条件下,您希望为一个类重新生成 serialVersionUID
    1. serialVersionUID 会自动生成,所以从不需要重新生成它。
    2. 如果您的类实现了多个类和 3 个属性,这可能是种不错的做法。
    3. 如果对类执行的更改导致该类以前的实例不兼容,则应重新生成 serialVersionUID,以避免不经意地向代码中引入错误。
    4. 如果对类执行更改,即使确定该更改与该类的任何以前的实例兼容,为安全起见,也应该重新生成 serialVersionUID
    5. 上述选项都不是
  5. 假设您需要将一个属性添加到不需要序列化的类。如何让 Java 序列化运行时知道?
    1. 不需要这样做。序列化运行时会自动检测和处理不重要的属性。
    2. 使用 ignoreSerial 关键字声明该属性。
    3. 您必须编写自定义代码,以便在去序列化这些属性时处理它们。
    4. 您使用 transient 关键字声明该属性。
    5. 上述选项都不是
  6. 创建两个类:
    • Container
    • Contained

    让两个类都可序列化。每个类必须包含一个只读属性:name(一个 String)。为该属性生成一个 getter。Container 有一个额外的只读属性:contained(这是一个 Contained 实例)。为此属性生成一个 getter。初始化每个类的构造方法中的 name 属性。初始化 Container 的构造方法中的 Contained 实例。

    编写一个名为 testContainer_problem6() 的 JUnit 测试平台,该平台创建了一个 Container 实例(将名称设置为您喜欢的任何名称)并序列化到一个名为 Container.ser 的磁盘文件。

    刷新您的项目,确保显示了该文件。
  7. 编写一个名为 testContainer_problem7() 的 JUnit 测试案例,以从问题 6 读入该序列化对象,并确认内容符合您的预期。
  8. 将一个新 String 属性添加到 Container 并将它设置为您喜欢的任何名称。然后重新运行 testContainer_problem7()(重要的是重新生成 Container.ser)。您预计会发生什么?解释您的答案。

核对您的答案。

进一步探索

关于 Java 对象序列化您不知道的 5 件事

Java 教程:可序列化对象

什么是 serialVersionUID 和为什么应使用它?

了解 serialVersionUID

上一单元:I/O下一单元:云中的 Java


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=1039325
ArticleTitle=第 22 单元:Java 序列化
publish-date=11012016