Java 序列化

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 final 和 long 类型。

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

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

上一篇 下一篇