序列化是将对象的状态转换为字节流;反序列化则相反。换句话说,序列化是将 Java 对象转换为静态字节流(序列),然后我们可以将其保存到数据库或通过网络传输。
序列化和反序列化
序列化过程与实例无关;例如,我们可以在一个平台上序列化对象并在另一个平台上反序列化它们。符合序列化条件的类需要实现一个特殊的标记接口, Serializable。
ObjectInputStream和ObjectOutputStream都是分别扩展java.io.InputStream和java.io.OutputStream的高级类。ObjectOutputStream可以将对象的原始类型和图形作为字节流写入OutputStream 。然后我们可以使用ObjectInputStream读取这些流。
ObjectOutputStream中最重要的方法是:
public final void writeObject(Object o) throws IOException;
此方法采用可序列化对象并将其转换为字节序列(流)。
同样, ObjectInputStream中最重要的方法是:
public final Object readObject() throws IOException, ClassNotFoundException;
此方法可以将字节流转换为 Java 对象。
对于一个序列化的对象 Person,我们需要在类上标注实现 Serializable 接口,该接口没有任何方法需要实现,只需要在对象中添加 serialVersionUID 字段,并赋予一个唯一值。
需要注意的是:static 字段不会被序列化,此外,被 transient 标识的字段也不会被序列化。也就是说,如果一个字段不希望在被序列化,一个可行的办法是添加 transient 关键字。
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
static String country = "Beijing";
private int age;
private String name;
transient int height;
// getters and setters
}
下面的例子,我们将 Person 对象序列化后保存在本地磁盘,然后再从磁盘上反序列化出该对象。
@Test
public void serializingAndDeserializing() ()
throws IOException, ClassNotFoundException {
Person person = new Person();
person.setAge(32);
person.setName("Tom");
// 写入本次磁盘(序列化)
FileOutputStream fileOutputStream
= new FileOutputStream("person.txt");
ObjectOutputStream objectOutputStream
= new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(person);
objectOutputStream.flush();
objectOutputStream.close();
// 从本次读取(反序列化)
FileInputStream fileInputStream
= new FileInputStream("person.txt");
ObjectInputStream objectInputStream
= new ObjectInputStream(fileInputStream);
Person p2 = (Person) objectInputStream.readObject();
objectInputStream.close();
// 比较序列化的对象内容和反序列化的对象内容
assertTrue(p2.getAge() == person.getAge());
assertTrue(p2.getName().equals(person.getName()));
}
我们使用 ObjectOutputStream
将该对象的状态保存到使用 FileOutputStream
的文件中。在项目目录中创建文件 person.txt
。然后使用 FileInputStream
加载该文件。ObjectInputStream
读取该流并将其转换为一个名为p2的新对象。
最后,我们将验证加载对象的状态,并确保它与原始对象的状态相匹配。
请注意,我们必须将加载的对象显式转换为Person类型。
Java 序列化中的继承与组合
当一个类实现java.io.Serializable接口时,它的所有子类也都是可序列化的。反之,当一个对象引用了另一个对象时,这些对象必须分别实现Serializable接口。
public class Person implements Serializable {
private int age;
private String name;
private Address country; // 需要实现序列化
}
如果可序列化对象中的一个字段由对象数组组成,则所有这些对象也必须是可序列化的,否则将抛出 NotSerializableException
。
序列号 UID
JVM 将版本号与每个可序列化的类相关联。我们使用它来验证保存和加载的对象是否具有相同的属性。
大多数 IDE 可以自动生成这个数字,它基于类名、属性和相关的访问修饰符。任何更改都会导致不同的数字,并可能导致 InvalidClassException
。
如果一个可序列化的类没有声明一个 serialVersionUID
,JVM 将在运行时自动生成一个。但是,强烈建议每个类都声明其serialVersionUID,因为生成的类依赖于编译器,因此可能会导致意外的 InvalidClassExceptions
。
Java自定义序列化
Java 指定了序列化对象的默认方式,但 Java 类可以覆盖此默认行为。当尝试序列化具有一些不可序列化属性的对象时,自定义序列化可能特别有用。我们可以通过在要序列化的类中提供两个方法来做到这一点:
private void writeObject(ObjectOutputStream out) throws IOException;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
使用这些方法,我们可以将不可序列化的属性序列化为我们可以序列化的其他形式:
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private transient Address address;
private Person person;
// setters and getters
private void writeObject(ObjectOutputStream oos)
throws IOException {
oos.defaultWriteObject();
oos.writeObject(address.getHouseNumber());
}
private void readObject(ObjectInputStream ois)
throws ClassNotFoundException, IOException {
ois.defaultReadObject();
Integer houseNumber = (Integer) ois.readObject();
Address a = new Address();
a.setHouseNumber(houseNumber);
this.setAddress(a);
}
}
地址对象被显示标注了 transient 关键字,但是可以用自定义方法 writeObject 和 readObject 来改变序列化和反序列化行为。
public class Address {
private int houseNumber;
// setters and getters
}
我们可以运行以下单元测试来测试这个自定义序列化:
@Test
public void whenCustomSerializingAndDeserializing_ThenObjectIsTheSame()
throws IOException, ClassNotFoundException {
Person p = new Person();
p.setAge(20);
p.setName("Joe");
Address a = new Address();
a.setHouseNumber(1);
Employee e = new Employee();
e.setPerson(p);
e.setAddress(a);
FileOutputStream fileOutputStream
= new FileOutputStream("yourfile2.txt");
ObjectOutputStream objectOutputStream
= new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(e);
objectOutputStream.flush();
objectOutputStream.close();
FileInputStream fileInputStream
= new FileInputStream("yourfile2.txt");
ObjectInputStream objectInputStream
= new ObjectInputStream(fileInputStream);
Employee e2 = (Employee) objectInputStream.readObject();
objectInputStream.close();
assertTrue(
e2.getPerson().getAge() == e.getPerson().getAge());
assertTrue(
e2.getAddress().getHouseNumber() == e.getAddress().getHouseNumber());
}
在这段代码中,我们可以看到如何通过使用自定义序列化序列化地址来保存一些不可序列化的属性。请注意,我们必须将不可序列化的属性标记为 transient
以避免 NotSerializableException
。