Java中的序列化与反序列化

时间:2023-03-09 16:58:52
Java中的序列化与反序列化

序列化定义

  将对象转换为字节流保存起来,并在以后还原这个对象,这种机制叫做对象序列化

  将一个对象保存到永久存储设备上称为持久化

  一个对象要想能够实现序列化,必须实现java.io.Serializable接口。该接口中没有定义任何方法,是一个标识性接口(Marker Interface),当一个类实现了该接口,就表示这个类的对象是可以序列化的。

  序列化(serialization)是把一个对象的状态写入一个字节流的过程。当你想要把你的程序状态存到一个固定的存储区域,例如文件时,它是很管用的。

  从程序到外面叫序列化,从外面读回来叫反序列化。

序列化特点

  假设一个被序列化的对象引用了其他对象,同样,其他对象又引用了更多的对象。这一系列的对象和它们的关系形成了一个顺序图表。

  对象序列化和反序列化工具被设计出来并在这一假定条件下运行良好。

  如果你试图序列化一个对象图表中顶层的对象,所有其他的引用对象都被循环地定位和序列化;同样,在反序列化过程中,所有的这些对象以及它们的引用都被正确地恢复。

序列化实现细节

  只有实现Serializable接口的对象可以被序列化工具存储和恢复。

  Serializable接口没有定义任何成员,它只用来表示一个类可以被序列化。

  如果一个类可以被序列化,它的所有子类都可以序列化。

  当一个对象被序列化时,只保存对象的非静态成员变量,不能保存任何的成员方法和静态的成员变量。

  如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存。

  如果一个可序列化的对象包含对某个不可序列化对象的引用,那么整个序列化操作将会失败,并且会抛出一个NotSerializableException

  我们可以将这个不可序列化的引用标记为transient,那么原对象仍然可以序列化。使用transient修饰的变量将不会被序列化。

  反序列化时不会调用对象的任何构造方法,仅仅根据所保存的对象状态信息,在内存中重新构建对象。

ObjectOutput接口

  ObjectOutput接口继承DataOutput接口并且支持对象序列化。

  特别注意writeObject(Object obj)方法,它被称为序列化一个对象。

  所有这些方法在出错情况下引发IOException异常。

ObjectOutputStream类

  对象的输出流。ObjectOutputStream类继承OutputStream类和实现ObjectOutput接口。它负责向流写入对象。

  该类的构造函数ObjectOutputStream(OutputStream out),参数是序列化对象将要写入的输出流。

  根据它的构造函数可以判断它是一个包装类,是一个过滤流。

ObjectInput接口

  ObjectInput接口继承DataInput接口。它支持对象序列化。

  特别注意readObject()方法,它叫反序列化一个对象。

  所有这些方法在出错情况下引发IOException异常。

ObjectInputStream类

  ObjectInputStream类继承InputStream类,实现ObjectInput接口。它负责从流中读取对象。

  该类的构造函数ObjectInputStream(InputStream in),参数是序列化对象将被读取的输入流。

  ObjectInputStream类是一个过滤流,它包装了节点流。

  序列化的程序例子SerializableTest1:这个例子定义了一个类叫Person,程序实现了Person类对象的序列化(写出)和反序列化(读入)。 

SerializableTest1
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable; public class SerializableTest1
{
public static void main(String[] args) throws Exception
{
Person p1 = new Person("ZhangSan", 29, 1.78);
Person p2 = new Person("LiSi", 50, 1.7);
Person p3 = new Person("WangWu", 46, 1.67); FileOutputStream fos = new FileOutputStream("Person.txt");// 当前项目路径下的一个文本文档
ObjectOutputStream oos = new ObjectOutputStream(fos); // 用序列化的方式将对象存储在文件中
oos.writeObject(p1);
oos.writeObject(p2);
oos.writeObject(p3); oos.close(); // 反序列化,将对象信息读出
FileInputStream fis = new FileInputStream("Person.txt");
ObjectInputStream ois = new ObjectInputStream(fis); Person person = null; for (int i = 0; i < 3; ++i)
{
person = (Person) ois.readObject(); System.out.println(person.name + ", " + person.age + ", "
+ person.height);
} ois.close(); } } class Person implements Serializable
{
// 这个类需要实现Serializable接口
// 否则在序列化时会抛出java.io.NotSerializableException
transient String name;
transient int age;
double height; // 加上transient关键字后将不被序列化 public Person(String name, int age, double height)
{
this.name = name;
this.age = age;
this.height = height;
}
}

  在程序中尝试可知,如果加上transient关键字,变量将不被序列化,反序列化时读取不到,输出默认值,如整形和double型变量输出为默认值0,字符串将输出为null。

一个序列化中的特殊情况

  在序列化和反序列化中需要特殊处理的Serializable类应该实现以下方法:

  private void writeObject (java.io.ObjectOutputStream stream) throws IOException

  private void readObject (java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException;

  这两个方法不属于任何一个类或任何一个接口,是非常特殊的方法。

  只要我们提供了这两个方法,在序列化反序列化的时候它们会得到自动调用。

  如根据第一个程序改编的程序2,注意只是将Person类改名为Person2,然后加入上面两个特殊的函数。注意这时候去掉了所有的transient关键字。 

SerializableTest2
Java中的序列化与反序列化
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable; public class SerializableTest2
{
public static void main(String[] args) throws Exception
{
Person2 p1 = new Person2("ZhangSan", 29, 1.78);
Person2 p2 = new Person2("LiSi", 50, 1.7);
Person2 p3 = new Person2("WangWu", 46, 1.67); FileOutputStream fos = new FileOutputStream("Person2.txt");// 当前项目路径下的一个文本文档
ObjectOutputStream oos = new ObjectOutputStream(fos); // 用序列化的方式将对象存储在文件中
oos.writeObject(p1);
oos.writeObject(p2);
oos.writeObject(p3); oos.close(); // 反序列化,将对象信息读出
FileInputStream fis = new FileInputStream("Person2.txt");
ObjectInputStream ois = new ObjectInputStream(fis); Person2 person = null; for (int i = 0; i < 3; ++i)
{
person = (Person2) ois.readObject(); System.out.println(person.name + ", " + person.age + ", "
+ person.height);
} ois.close(); } } class Person2 implements Serializable
{
// 这个类需要实现Serializable接口
// 否则在序列化时会抛出java.io.NotSerializableException
String name;
int age;
double height; // 加上transient关键字后将不被序列化 public Person2(String name, int age, double height)
{
this.name = name;
this.age = age;
this.height = height;
} private void writeObject(java.io.ObjectOutputStream out) throws IOException
{
System.out.println("writeObject Method");
} private void readObject(java.io.ObjectInputStream in) throws IOException,
ClassNotFoundException
{
System.out.println("readObject Method");
} }
Java中的序列化与反序列化

  程序输出如下:

writeObject Method
writeObject Method
writeObject Method
readObject Method
null, 0, 0.0
readObject Method
null, 0, 0.0
readObject Method
null, 0, 0.0

  可以看到这两个方法是自动被调用的,但要注意的是反序列化后得到的值全是默认值。

  虽然所有的变量都不是transient关键字修饰,但是程序在序列化时只处理了变量名字,并没有将变量值序列化。

  当提供了这两个方法后,序列化和反序列化就完全由我们自己控制

  如果我们在方法中不写入相关代码,则反序列化后并不能读入什么值,所有变量都是默认值。加入相关变量的序列化和反序列化代码后才能真的执行操作。

  我们在一个待序列化/反序列化的类中实现了以上两个private方法(方法声明要与上面的完全保持一致,具体在Serializable接口的文档中也有解释),那么就允许我们以更加底层、更加细粒度的方式控制序列化/反序列化的过程。

  如下代码,在前面代码的基础上加入了其中两个变量的序列化和反序列化代码。

SerializableTest2 改进版
Java中的序列化与反序列化
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable; public class SerializableTest2
{
public static void main(String[] args) throws Exception
{
Person2 p1 = new Person2("ZhangSan", 29, 1.78);
Person2 p2 = new Person2("LiSi", 50, 1.7);
Person2 p3 = new Person2("WangWu", 46, 1.67); FileOutputStream fos = new FileOutputStream("Person2.txt");// 当前项目路径下的一个文本文档
ObjectOutputStream oos = new ObjectOutputStream(fos); // 用序列化的方式将对象存储在文件中
oos.writeObject(p1);
oos.writeObject(p2);
oos.writeObject(p3); oos.close(); // 反序列化,将对象信息读出
FileInputStream fis = new FileInputStream("Person2.txt");
ObjectInputStream ois = new ObjectInputStream(fis); Person2 person = null; for (int i = 0; i < 3; ++i)
{
person = (Person2) ois.readObject(); System.out.println(person.name + ", " + person.age + ", "
+ person.height);
} ois.close(); } } class Person2 implements Serializable
{
// 这个类需要实现Serializable接口
// 否则在序列化时会抛出java.io.NotSerializableException
String name;
int age;
double height; // 加上transient关键字后将不被序列化 public Person2(String name, int age, double height)
{
this.name = name;
this.age = age;
this.height = height;
} private void writeObject(java.io.ObjectOutputStream out) throws IOException
{
out.writeUTF(name);
out.writeInt(age);
System.out.println("writeObject Method");
} private void readObject(java.io.ObjectInputStream in) throws IOException,
ClassNotFoundException
{
name = in.readUTF();
age = in.readInt();
System.out.println("readObject Method");
} }
Java中的序列化与反序列化

参考资料

  圣思园张龙老师Java SE系列视频。