对象序列化的含义及意义
对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而把这个二进制流保存到磁盘上或者在网络中进行传输。以后需要使用时可以重新还原成原来的对象。
在JavaEE中像HttpSession这种需要保存到磁盘上的对象就需要用到对象序列化技术,RMI技术中的参数和返回值是要在网络中传输的,也需要用到对象序列化技术。
对象序列化
实现Serializable接口
要使用对象序列化的对象,其必须实现Serializable接口。Serializable接口是一个标记接口,实现该接口无需实现任何方法,它仅仅表明这个类的实例是可以序列化的。
public class Student implements Serializable {
private String name;
private int age;
private Teacher teacher;
//构造函数
//getter和setter方法
}
使用ObjectOutputStream处理流
@org.junit.Test
public void test1() throws IOException {
//实例化两个对象
Student studentA = new Student("学生A", 21, null);
Student studentB = new Student("学生B", 22, null);
//实例化ObjectOutoutStream,并用它来包装一个底层节点流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./object"));
//将两个对象序列化到文件object中
objectOutputStream.writeObject(studentA);
objectOutputStream.writeObject(studentB);
}
反序列化
反序列化就是将二进制字节流恢复成Java对象。反序列化需要用到ObjectInputStream。
@org.junit.Test
public void test2() throws IOException, ClassNotFoundException {
//实例化ObjectInputStream
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("./object"));
//readObject()方法返回一个Object对象
Student studentA=(Student)objectInputStream.readObject();
Student studentB=(Student)objectInputStream.readObject();
System.out.println(studentA);
System.out.println(studentB);
}
有几点需要说明:
- readObject()从流中读取一个Object类型的Java对象,我们需要知道Object对象的类型并进行强制转换。反序列化读取Java对象的数据而不是Java类,如果不存在Java对象所属的Java类那么会抛出ClassNotFoundException。
- readObject()和writeObject()的顺序是一致的,也就是说第一次调用writeObject()写入的是studentA那么第一次readObject()读出的也是studentA。
包含对象引用的序列化
上面的示例中,序列化对象中的属性都是基本数据类型,如果是表示其他对象的引用类型呢?该对象可以序列化吗?
答案是可以的,但有一个前提,该对象也必须是可以序列化的,如果该对象还包含其他引用类型,这个引用类型也是必须是可序列化的。这中机制被称为递归序列化。
现在创建一个新的类,这个类也必须实现Serializable接口:
public class Teacher implements Serializable {
private String name;
private int age;
//构造函数
//getter和setter方法
}
建立引用关系,并实现序列化:
@org.junit.Test
public void test1() throws IOException {
Teacher teacherA = new Teacher("teacherA", 33);
Student studentA = new Student("学生A", 21, teacherA);
Student studentB = new Student("学生B", 22, teacherA);
//实例化ObjectOutoutStream,并用它来包装一个底层节点流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./object"));
//将两个对象序列化到文件object中
objectOutputStream.writeObject(studentA);
objectOutputStream.writeObject(studentB);
}
现在序列化是成功了,但是有一个新的问题,序列化之前这三个对象是有如图的引用关系:
那么反序列化之后的对象可以正确地反映这种引用关系吗?
@org.junit.Test
public void test2() throws IOException, ClassNotFoundException {
//实例化ObjectInputStream
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("./object"));
//readObject()方法返回一个Object对象
Student studentA=(Student)objectInputStream.readObject();
Student studentB=(Student)objectInputStream.readObject();
System.out.println(studentA.getTeacher()==studentB.getTeacher());//true
}
居然是可以的!!!要知道对象在内存中可以使用内存单元的地址来正确的阐述对象之间的引用关系,那么在序列化之后的磁盘文件中是如何做到的呢?这就是下面要说的——序列化编号。
序列化编号
Java采用一种特殊的序列化算法来实现:
- 所有的序列化对象都有一个序列化编号。
- 当程序试图序列化一个对象时,首先检查这个对象是否已经序列化过,如果某个对象已经被序列化过程序只是输出这个对象的序列化编号而不是重新序列化这个对象。
- 只有当对象未被序列化过时,Java才会将该对象转换成二进制字节流输出。
现在再看上面的序列化代码:
objectOutputStream.writeObject(studentA);
objectOutputStream.writeObject(studentB);
当序列化studentA时,同时序列化studentA和teacherA,当序列化studentB时,因为teacherA已经序列化过了,所以这里只输出一个序列化编码表示引用关系即可。
这虽然很方便,但也带来了一个潜在问题:
只有第一次调用writeObject()时对象才会转换成字节序列输出,在后面的程序中即使对象实例变量发生了改变,再次调用writeObject()方法,改变的实例也不会输出。
请看下面代码:
@org.junit.Test
public void test3() throws IOException, ClassNotFoundException {
Student student = new Student("jack", 23, null);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./object"));
objectOutputStream.writeObject(student);
student.setName("lucy");
objectOutputStream.writeObject(student);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("./object"));
Student student1 =(Student) objectInputStream.readObject();
Student student2 =(Student) objectInputStream.readObject();
System.out.println(student1==student2);//true
System.out.println(student2.getName());//jack
}
反序列化的特点
- 使用序列化机制向文件中写入多个Java对象,在使用反序列化机制回复对象时必须按照实际写入顺序读取。
- 反序列化机制无需通过构造函数来初始化Java对象。
在反序列化过程中,实际上是创造一个新的对象。这个新的Java对象它的实例化变量的值与序列化之前的对象相同,但它们两个并不是同一个对象。
@org.junit.Test
public void test4() throws IOException, ClassNotFoundException {
Student student = new Student("jack", 22, null);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./object"));
objectOutputStream.writeObject(student);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("./object"));
Student _student = (Student) objectInputStream.readObject();
System.out.println(student==_student);//false
}
这一般不会有什么问题,但是在使用单例、多例设计模式的时候就有点麻烦了。
这是一个典型的多例设计模式:
public class Sex implements Serializable{
public static Sex MALE=new Sex("MALE");
public static Sex FEMALE=new Sex("FEMALE");
private String sex;
private Sex(String sex){
this.sex=sex;
}
}
在正常情况下,这个类只有两个实例化对象MALE和FEMALE,那么如果使用序列化与反序列化机制就会出现这样一个情况:
@org.junit.Test
public void test5() throws IOException, ClassNotFoundException {
Sex male=Sex.MALE;
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./object"));
objectOutputStream.writeObject(male);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("./object"));
Sex _male =(Sex) objectInputStream.readObject();
System.out.println(_male==male);//false
}
在这种情况下_male已经成为了Sex的第三个实例化对象,这打破了我们精心设计的设计模式。
解决方法有两种:
- 使用自定义序列化机制。
- 使用Java提供的枚举enum去替代多例设计模式。
版本号UID
假设,我们将某个类的实例序列化到一个文件中。但是在日后的使用过程中我们发现这个类需要做一些改动,比如给这个类增加一个字段,那么在反序列化的过程中一定会报错。报错的原因在于Java认为修改前与修改之后的类是两个不同的类。
为了解决这个问题,Java提供了一个private static final 的 serialVersionUID,这个变量用于标志Java类的序列化版本,也就是如果一个类升级后,只要它的serialVersionUID的值不变序列化机制会把它当作同一个类来处理。
因此一个最佳实践就是在设计一个需要序列化的类时,显式地指定一个值作为serialVersionUID,如果是使用IDEA可以自动帮我们生成UID,但是需要做一些额外设置。可以参考下面。