Java 序列化

欢马劈雪     最近更新时间:2020-08-21 11:12:08

540

Java 序列化是 Java 平台的另一个基础库。 序列化主要用于对象持久化和对象远程传输,在这两种用例中,都需要能够获取对象状态的快照,这样在以后能重新构成它们。本节大体介绍 Java Serialization API,展示如何在程序中使用它。

什么是对象序列化?

序列化 过程中,会以一种特殊的二进制格式存储一个对象及其元数据(比如对象的类名称及其属性名称)的状态。将对象存储为此格式 — 序列化 它 — 可保留在需要时重新构成(或 反序列化 )该对象所必需的全部信息。

对象序列化有两种主要用例:

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

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 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 对象)存储在一个文件中,但同样的技术可用于任何类型的序列化。

要成功运行清单 12 中的代码,您可以使用 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);
  }
}

反序列化对象

序列化对象的唯一目的就是为了能够重新构成或反序列化它。清单 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<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;
  }

}

同样地,要成功运行清单 13 中的代码,可以使用一个类似这样的 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 是执行序列化工作时唯一需要担心的问题。需要明确地序列化和反序列化对象时,可使用清单 12 和清单 13 中所示的技术。但是,随着应用程序对象不断演变,以及在它们之中添加和删除属性,序列化会变得更加复杂。

serialVersionUID

回想中间件和远程对象通信的发展初期,开发人员主要负责控制其对象的”连接格式”,随着技术开始演变,这引发了大量头疼的问题。

展开阅读全文