序列化是一种对象的传输手段,Java有自己的序列化管理机制。对象序列化,就是把一个对象变为二进制的数据流的一种方法,通过对象的序列化可以方便地实现对象的传输或存储,如图 1-1所示。
一个类的实例化对象想被序列化,则对象所在的类必须实现java.io.Serializable接口。然而此接口并没有提供任何的抽象方法,所以该接口只是一个标识接口(只是表示一种对象可以被序列化的能力)。
范例1:定义序列化对象类Student
//实现Serializable接口
public class Student implements Serializable {
private String name;
private Integer age;
private Integer score;
public Student() {
}
public Student(String name, Integer age, Integer score) {
this.name = name;
this.age = age;
this.score = score;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Integer getScore() {
return score;
}
public void setScore(Integer score) {
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
}
本程序定义的Student类实现了Serializable接口,所以此类的实例化对象都允许序列化转成二进制进行传输。
tips:对象序列化和对象反序列化操作时的版本兼容问题。
序列化和反序列化需要进行二进制存储格式上的统一,为此Java提供了对象序列化操作类。Serializable接口只是定义了某一个类的对象是否被允许序列化,然而对于序列化和反序列化操作的具体实现则需依靠ObjectOutputStream和ObjectInputStream两类完成。这两类的继承结构如图 2-1所示。
ObjectOutputStream类可以将对象转为特定格式的二进制数据输出,常用方法如表 2-1所示;ObjectInputStream类可以读取ObjectOutputStream类输出的二进制对象数据,并将其转为具体类型的对象返回,常用方法如表 2-2所示。
方法 | 类型 | 描述 |
---|---|---|
public ObjectOutputStream(ObjectOutputStream out) throws IOException | 构造 | 传输输出的对象 |
public final void writeObject(Object obj) throws IOException | 普通 | 输出对象 |
方法 | 类型 | 描述 |
---|---|---|
public ObjectInputStream(InputStream in) throws IOException | 构造 | 构造输入对象 |
public final Object readObject() throws IOException, ClassNotFoundException | 普通 | 从指定位置读取对象 |
范例2:实现对象序列化和反序列化操作
/*
在Student类中添加两个方法
*/
//1、序列化操作
public static void serialize(Object object, String fileName) throws IOException {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(
new FileOutputStream(new File(fileName)));
objectOutputStream.writeObject(object);
objectOutputStream.close();
System.out.println("序列化成功!");
}
//2、反序列化操作
public static Object deserialize(String fileName) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(
new FileInputStream(new File(fileName)));
Object object = objectInputStream.readObject();
objectInputStream.close();
System.out.println("反射序列化成功!");
return object;
}
//测试类
public class SerializeTestDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Student student = new Student("Chen", 22, 22);
System.out.println(student);
//序列化
Student.serialize(student, "chen.dat");
System.out.println("==========");
//反序列化
Student chen = (Student) Student.deserialize("chen.dat");
System.out.println(chen);
}
}
/*
输出结果:
Student{name='Chen', age=22, score=22}
序列化成功!
==========
反射序列化成功!
Student{name='Chen', age=22, score=22}
*/
本程序以二进制文件(chen.dat)作为序列化操作的存储终端 ,在程序中首先通过ObjectOutputStream类将一个Student类的实例化对象输出到文件中,随后再利用ObjectInputStream类将二进制的序列化数据读取并转为Student类实例化对象返回。
默认情况下,当执行对象序列化操作的时候,会将类中的全部属性的内容进行序列化操作,为了保证对象序列化的搞笑传输,就需要防止一些不必要的成员属性的序列化处理。当成员属性不需要进行序列化处理时就可以在属性定义上使用transient关键字来完成。
范例3:transient 关键字的序列化和反序列化操作
//将Student类的score字段定义为transient
private transient Integer score;
//测试类的代码不做任何改变
/*
输出结果:
Student{name='Chen', age=22, score=22}
序列化成功!
==========
反射序列化成功!
Student{name='Chen', age=22, score=null}
*/
本程序对Student类对象进行序列化处理,score属性的内容是不会被保存下来的,这样进行反序列化操作时,score使用的将是其数据类型的默认值。
tips:凡是被static修饰的字段是不会被序列化的。
因为序列化保存的是对象的状态而非类的状态,所以会忽略static静态域。
序列化和反序列化的过程其实是有漏洞的,因为从序列化到反序列化是有中间过程的,如果被人拿到中间字节流,然后加以伪造或者篡改,那么反序列化返回的实例化对象就会有一定的风险了。毕竟反序列化也是相当于一种**“隐式的”对象构造**,因此,我么希望在反序列时,进行受控的对象反序列化操作。
具体的操作就是自行编写readObject()方法,用于对象的反序列化构造,从而提高约束性。既然是自行编写readObject()方法,那就可以做更多的事情:比如说各种判断工作。
还是以上述的Student类说,一般而言,成绩的取值是在0~100之间,我们为了防止学生的成绩在反序列时被别人篡改,我们可以自行编写readObject()方法用于反序列的控制
/*
在Student类中再添加一个个方法
*/
//自行编写readObject()方法用于反序列的控制
private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
//调用默认的反序列方法
objectInputStream.defaultReadObject();
//手工检测反序列化后学生成绩的有效性,若发现问题。立即终止操作!
if (0 > score || 100 < score) {
throw new IllegalArgumentException("分数只能在0~100之间");
}
}
//测试类
public class SerializeTestDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Student student01 = new Student("cbc", 222, 22);
System.out.println(student01);
//序列化
Student.serialize(student01, "cbc.dat");
//反序列化
Student cbc = (Student) Student.deserialize("cbc.dat");
System.out.println(cbc);
System.out.println("==============================");
Student student02 = new Student("zhang", 2, 101);
System.out.println(student02);
//序列化
Student.serialize(student02, "zhang.dat");
//反序列化
Student zhang = (Student) Student.deserialize("zhang.dat");
System.out.println(zhang);
}
}
/*
输出结果:
序列化成功!
反射序列化成功!
Student{name='cbc', age=222, score=22}
==============================
Student{name='zhang', age=2, score=101}
序列化成功!
Exception in thread "main" java.lang.IllegalArgumentException: 分数只能在0~100之间
*/
对于上面的代码,我一开始也是很懵,怎么自定义为private的readObject()方法也可以被自行调用。后来看到大牛的博客后,看到ObjectStreamClass类最底层的实现,才恍然大悟:又是Java强大的反射机制。如图 4-1。