1.导引
我们最常使用的创建对象的方式,就是使用new运算符来创建对象,并使用之。比如,如果你有一个Student类,在其构造函数中接受一个人的name, gender和height作为参数,你可以创建一个Student对象,如下所示:
Student zhangsan= new Person(“张三”, “男”, 6.7);
如果你想将对象zhangsan保存到文件中,然后在不使用new运算符的情况下将其恢复到内存中,那该怎么做呢? 本节就来讨论这个主题内容 。
首先要明白,将内存中的对象转换为字节序列并将字节序列存储在诸如文件的存储介质中的过程称为 对象 序列化 。 你可以将字节序列存储到永久存储器中,例如文件或数据库;还可以通过网络传输字节序列。 从存储器中读取序列化过程产生的字节序列并将对象恢复到内存中的过程称为 对象反序列化 。 对象的序列化也称为对对象进行 压缩或编组 。 对象的反序列化也称为对对象进行 复原或解组 。 可以将序列化视为将对象从内存写入存储介质,将反序列化视为从存储介质将对象读入内存。

完成这两类工作的是ObjectOutputStream类和ObjectInputStream类
ObjectOutputStream 类的对象用于序列化对象。 ObjectInputStream 类的对象用于反序列化对象。你还可以使用这些类的对象来序列化原始数据类型的值,例如int,double,boolean等。
ObjectOutputStream和ObjectInputStream类分别是输出和输入流的具体装饰器类(IO族系类中装饰器用的非常之多)。 但是,它们不是从它们的抽象装饰器类继承而来的。 它们是从各自的抽象组件类继承而来的。 ObjectOutputStream继承自OutputStream,ObjectInputStream继承自InputStream。貌似这与装饰模式不一致的。其实,这仍然适合装饰器模式(这里就不展开了)。
关于对象序列化,你的需要序列的对象的类,其必须实现 Serializable 或 Externalizable 接口,只有这样了,才能进行序列化或反序列化。 Serializable接口 是一个标记接口(没有任何成员)。 如果希望序列化Student类的对象,则需要按如下方式声明Student类:
public class Student implements Serializable { // Student相关代码 }
序列化时,由Java负责从(向)流读取(写入)Serializable对象的细节,而你只需要将对象传递给(从)流,调用相应方法把对象写入(读取)到流中即可。
若你的类实现了Externalizable接口,可以更好地控制从流中读取对象和写入对象。该接口继承了Serializable接口。 声明如下:
public interface Externalizable extends Serializable {
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
void writeExternal(ObjectOutput out) throws IOExcept io n;
}
从流中读取对象时,将调用readExternal()方法。 将对象写入流时,将调用writeExternal()方法。 但你必须分别编写逻辑来读取和写入readExternal()和writeExternal()方法中的对象字段。 实现Externalizable接口的类示例如下:
public class Person implements Externalizable { public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { // 编写从流中读取Person对象字段逻辑 } public void writeExternal(ObjectOutput out) throws IOException { //编写把Person对象写入流的逻辑 } }

下面进入序列化和反序列化的具体内容。
2.对象序列化
要序列化对象,需要执行以下步骤:
1. 获得要序列化的对象的引用。
2. 为要序列化的对象创建存储介质输出流。
3. 将对象写入到输出流。
4. 关闭对象输出流。
通过将ObjectOutputStream类的对象用作另一个输出流的装饰器来创建ObjectOutputStream类的对象,该输出流表示用于保存对象的存储介质(也就是说创建ObjectOutputStream类的对象,用作另一个输出流的装饰器,那个输出流代表保存对象的存储介质)。 例如,要将对象保存到student.ser文件,请按如下方式创建对象输出流:
// 创建输出流对象去把对象写入文件 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.ser"))//你可以指定一个完整路径,如c:tmpstudent.ser
要将对象保存到ByteArrayOutputStream,请按如下方式构造对象输出流:
// 创建一个字节数组输出流去写入数据 ByteArrayOutputStream baos = new ByteArrayOutputStream(); //创建一个对象输出流去把对象写到字节数组输出流中 ObjectOutputStream oos = new ObjectOutputStream(baos);
使用ObjectOutputStream类的writeObject()方法通过将对象引用作为参数传递来的对象序列化,就像这样:
// 序列化zhangsan对象 oos.writeObject(zhangsan);
最后,在完成向对象输出流写入所有对象时,使用close()方法关闭对象输出流:
// 关闭队形输出流 oos.close();
清单-1定义了一个实现Serializable接口的Student类。Student类包含三个字段:name,gender和height。 它覆盖toString()方法并使用这三个字段返回Student描述。 为简洁起见,我没有在Student类的字段中添加getter和setter方法。
代码清单-1: 实现可序列化接口的Student类
package com.newday.ios;
import java.io.Serializable;
public class Student implements Serializable {
private String name = "Unknown";
private String gender = "Unknown";
private double height = Double. NaN ;
public Student(String name, String gender, double height) {
this.name = name;
this.gender = gender;
this.height = height;
}
@Override
public String toString() {
return "Name: " + this.name + ", Gender: " + this.gender
+ ", Height: " + this.height;
}
}
清单-2演示了如何将Student对象写入student.ser文件。 输出显示写入文件的对象和文件的绝对路径,这些路径在您的计算机上可能不同。
清单-2. 序列化对象
package com.newday.ios; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; public class SerializationTest { //指定序列化文件路径 static String locFileName = "c:\xTmp\Student.ser"; public static void main(String[] args) { // 创建Student对象 Student zhangsan = new Student("张三", "Male", 6.7); Student lisi = new Student("李四", "Male", 5.7); Student wangwu = new Student("wangwu", "Female", 5.4); // 输出文件 File fileObject = new File(locFileName); try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileObject))) { // 把对象写入/序列化到输出文件 oos.writeObject(zhangsan); oos.writeObject(lisi); oos.writeObject(wangwu); // 输出序列化对象的信息 System.out.println(zhangsan); System.out.println(lisi); System.out.println(wangwu); //显示输出路径 System.out.println("对象被序列化到: " + fileObject.getAbsolutePath()); } catch (IOException e) { e.printStackTrace(); } } }
输出结果如下:
Name: 张三, Gender: Male, Height: 6.7
Name: 李四, Gender: Male, Height: 5.7
Name: wangwu, Gender: Female, Height: 5.4
对象被序列化到: c:xTmpStudent.ser
就这么简单,序列化搞定!只要实现了Serializable接口,接口序列化!
留一习题:能不能把含有元素的ArrayList序列化?咋搞法子?^_^

3.对象反序列化
接下来可以从Student.ser文件中读取对象了。 读取序列化对象与序列化对象相反。 要反序列化对象,需要执行以下步骤:
1. 为存储介质创建对象输入流,以便从中读取对象。
2. 调用接口方法读取对象。
3. 关闭对象输入流。
通过将ObjectInputStream类用作另一个输入流的装饰器来创建ObjectInputStream类的对象,该输入流表示存储序列化对象的存储介质。例如,要从Student.ser文件中读取对象,请按如下方式创建对象输入流:
// 创建从文件中读取对象的输入流对象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Student.ser"));
要从ByteArrayInputStream中读取对象,请按如下方式创建对象输出流:
// 创建从字节数组输入流中读取对象的对象输入流 ObjectInputStream ois = new ObjectInputStream(Byte-Array-Input-Stream-Reference);//参数为字节数组输入流对象
使用ObjectInputStream类的readObject()方法反序列化对象,就像这样:
// 从流中读取对象 Object obj = oos.readObject();
注意: 确保以调用writeObject()方法写入对象相同的顺序调用readObject()方法读取对象(什么顺序写入就什么顺序读出)。 例如,如果你以object-1,float和object-2顺序写入了三条信息,则必须按相同的顺序读取它们:object-1,float和object-2。
最后,关闭对象输入流,如下所示:
// 关闭输入流对象 ois.close();
清单-3演示了如何从Student.ser文件中读取对象。确保当前目录中存在Student.ser文件。 否则,程序将打印一条错误消息,其中包含此文件的预期位置。
清单-3.从文件中读取对象
package com.newday.ios; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.ObjectInputStream; public class DeserializationTest { static String locFileName = "c:\xTmp\Student.ser"; //1. 创建对象输入流(将从中读取对象的存储介质-文件)。 //2. 调用ObjectInputStream类型对象,读取对象。 //3. 完成工作后,关闭对象输入流。 public static void main(String[] args) { // 输入文件 File fileObject = new File(locFileName); try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileObject))) { // 读取和反序列化写入的三个对象 Student zhangsan = (Student) ois.readObject(); Student lisi = (Student) ois.readObject(); Student wangwu = (Student) ois.readObject(); // 展示读取的对象信息 System.out.println(zhangsan); System.out.println(lisi); System.out.println(wangwu); // 打印输入路径(序列化对象) System.out.println("读取的对象来源:" + fileObject.getAbsolutePath()); } catch (FileNotFoundException e) { System.out.println(fileObject.getPath()); } catch (ClassNotFoundException | IOException e) { e.printStackTrace(); } } }
输出信息如下(和写入的对象一致):
Name: 张三, Gender: Male, Height: 6.7
Name: 李四, Gender: Male, Height: 5.7
Name: wangwu, Gender: Female, Height: 5.4
读取的对象来源:c:xTmpStudent.ser
4.Externalizable序列化
在前面的部分中,我们介绍了如何序列化和反序列化可序列化对象。 在本节中,我将向你展示如何用Externalizable序列化和反序列化(Externalizable)对象。 我修改了Student类来实现Externalizable接口。 我将新类命名为StudentExt,如清单-4所示。
package com.chapter4.ios; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; public class StudentExt implements Externalizable { private String name = "Unknown"; private String gender = "Unknown"; private double height = Double.NaN; // 必须为此类定义无参构造器. // 在此类的readExternal()方法被调用前, // 其构造器用于在反序列号过程中构造对象. public StudentExt() { } public StudentExt(String name, String gender, double height) { this.name = name; this.gender = gender; this.height = height; } // 重载toString()方法,返回student描述 @Override public String toString() { return "Name: " + this.name + ", Gender: " + this.gender + ", Height: " + this.height; } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { // 以写入相同的顺序读取name 和gender this.name = in.readUTF(); this.gender = in.readUTF(); } @Override public void writeExternal(ObjectOutput out) throws IOException { // 只向流中写入name and gender out.writeUTF(this.name); out.writeUTF(this.gender); } }
Java将分别将对象输出流和对象输入流的引用传递给StudentExt类的writeExternal()和readExternal()方法。
在writeExternal()方法中,将name和gender字段写入对象输出流。 请注意,height字段不会写入对象输出流。 这意味着当你从readExternal()方法中读取流中的对象时,将无法获得height字段的值。writeUTF()方法用于将字符串(name和gender)写入对象输出流。
在readExternal()方法中,可从流中读取name和gender字段,并在name和gender实例变量中设置它们。
清单-5和清单-6包含StudentExt对象的序列化和反序列化逻辑。
清单-5.序列化实现了Externalizable接口的StudentExt对象
package com.newday.ios; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; public class StudentExtSerializationTest { public static void main(String[] args) { String serFileName = "c:\xTmp\StudentExt.ser"; // 创建对象 StudentExt zhs = new StudentExt("章三", "Male", 6.7); StudentExt lis = new StudentExt("李斯", "Male", 5.7); StudentExt wangw = new StudentExt("王舞", "Female", 5.6); // 输出文件 File fileObject = new File(serFileName); try (ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream(fileObject))) { //把对象写入输出流 oos.writeObject(zhs); oos.writeObject(lis); oos.writeObject(wangw); // 控制台显示被序列号的对象 System.out.println(zhs); System.out.println(lis); System.out.println(wangw); //打印输出路径 Print the output path System.out.println("Objects were written to " + fileObject.getAbsolutePath()); } catch (IOException e1) { e1.printStackTrace(); } } }
运行程序输出信息如下:
Name: 章三, Gender: Male, Height: 6.7
Name: 李斯, Gender: Male, Height: 5.7
Name: 王舞, Gender: Female, Height: 5.6
Objects were written to c:xTmpStudentExt.ser
清单-6.反序列化实现了Externalizable接口的StudentExt对象
package com.newday.ios; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.ObjectInputStream; public class StudentExtDeserializationTest { public static void main(String[] args) { String serFileName = "c:\xTmp\StudentExt.ser"; // 输入文件 File fileObject = new File(serFileName); try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileObject))) { // 读取 (或反序列号) 三个对象 StudentExt zhs = (StudentExt) ois.readObject(); StudentExt lis = (StudentExt) ois.readObject(); StudentExt wangw = (StudentExt) ois.readObject(); // 显示被读取的对象 System.out.println(zhs); System.out.println(lis); System.out.println(wangw); // 打印输入路径 System.out.println("Objects were read from " + fileObject.getAbsolutePath()); } catch (FileNotFoundException e) { System.out.println(fileObject.getPath()); } catch (ClassNotFoundException | IOException e) { e.printStackTrace(); } } }
输出结果如下:
Name: 章三, Gender: Male, Height: NaN
Name: 李斯, Gender: Male, Height: NaN
Name: 王舞, Gender: Female, Height: NaN
Objects were read from c:xTmpStudentExt.ser
清单-6的输出演示了在反序列化StudentExt对象后,height字段的值是默认值(Double.NaN)。
以下是使用Externalizable接口序列化和反序列化对象的步骤:
1.当调用writeObject()方法来写入Externalizable对象时, Java(执行引擎) 会将 对象的标识 写入到输出流中,然后调用其类的writeExternal()方法。 你可以在(序列化对象类的)writeExternal()方法中将与对象相关的数据写入输出流。 如果需要,可以完全控制在此方法中写入流的对象的相关数据。 如果要存储某些敏感数据,可能需要先将其加密,然后再将其写入流中,并在从流中读取数据时对其进行解密。
2.当调用readObject()方法读取Externalizable对象时,Java会从流中读取 对象的标识 。请注意,对于Externalizable对象,Java仅将对象的标识写入到输出流,而不是有关其类定义的任何详细信息。Java使用对象类的no-args构造函数来创建对象。这就是你必须为一个Externalizable对象提供一个无参(no-args)构造函数的原因。 它调用对象的readExternal()方法,以便在此可以完成填充或装配对象的相关字段值。
对于Serializable对象,JVM仅序列化未声明为瞬态的实例变量。 我将在下一节讨论序列化瞬态(transient)变量。而对于Externalizable对象,你则可完全控制被序列化的数据片段。
本文就到此为止,下一次将谈一谈对象序列裂化的瞬时字段和一些高级主题。