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