如果需要让某个对象支持序列化机制,则必须让它的类是可序列化的。为了让某个类是可序列化的,该类必须实现如下两个接口之一。
Serializable
Externalizable
java的很多类已经实现了Serializable,该接口是一个标记接口,实现该接口无须实现任何方法,它只是表明该类的实例是可序列化的。
所有可能在网络上传输的对象的类都应该是可序列化的否则程序将会出现异常,比如RMI过程中的参数和返回值;所有需要保存到磁盘里的对象的类都必须可序列化。
因为序列化是RMI过程的参数和返回值都必须实现的机制,而RMI又是javaee技术的基础,所有的分布式应用常常需要跨平台,跨网络,所以要求所有传递的参数,返回值必须实现序列化。因此序列化机制是javaEE平台的基础。
使用Serializable来实现序列化非常简单,主要让目标类实现Serializable标记接口即可,无须实现任何方法。
一旦某个类实现了Serializable接口,该类的对象就是可序列化的,程序可以通过如下两个步骤来序列化该对象。
1:创建一个ObjectOutputStream,这个输出流是一个处理流,所以必须建立在其他节点流的基础之上。如下代码所示:
ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream("a.tat"));
2:调用ObjectOutputStream对象的writeObject()方法输出可序列化对象,如下代码所示:
//将一个Person对象输出到输出流中 obj.writeObject(p);
下面程序定义了一个Person类,这个Person类就是一个普通的java类,只是实现了Serializable接口,该接口标识该类的对象是可序列化的。
1 import java.io.Serializable; 2 3 public class Person implements Serializable 4 { 5 private String name; 6 private int age; 7 //此处没有提供无参数的构造器 8 public Person(String name , int age) 9 { 10 System.out.println("有参数的构造器"); 11 this.name = name; 12 this.age = age; 13 } 14 public String getName() 15 { 16 return name; 17 } 18 public void setName(String name) 19 { 20 this.name = name; 21 } 22 public int getAge() 23 { 24 return age; 25 } 26 public void setAge(int age) 27 { 28 this.age = age; 29 } 30 31 32 }
下面程序使用ObjectOutputStream将一个Person对象写入磁盘文件。
1 public class WriteIbject 2 { 3 public static void main(String[] args) 4 { 5 try( 6 //创建一个ObjectOutputStream输出流 7 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("b.txt"))) 8 { 9 person p = new person("孙悟空", 500); 10 //将per对象写入输出流 11 oos.writeObject(p); 12 } 13 catch(IOException ex) 14 { 15 ex.printStackTrace(); 16 } 17 } 18 }
上面程序中的第一行粗体字代码创建了一个ObjectOutputStream输出流,这个OvjectOutputStream输出流建立在一个文件输出流的基础之上;程序使用write()方法将一个Person对象写入输出流。
如果希望从二进制流中恢复java对象,则需要使用反序列化,反序列化的步骤如下。
1:创建一个ObjectInputStream输入流,这个输入流是一个处理流,所以必须建立在其他节点流的基础之上。如下代码所示。
//创建一个ObjectInputStream输入流 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(b.txt));
2:调用ObjectInputStream对象的readObject()方法读取流中的对象,该方法返回一个Object类型的java对象,如果程序知道java对象的类型,则可以将该对象强制类型转换成其真实的类型,如下代码所示:
//从输入流中读取一个java对象,并将其强制类型转换为Person类 Person p = (Person)ois.readObject();
下面程序示范了从刚刚生成的b.txt文件中读取Person对象的步骤:
1 public class ReadObject 2 { 3 4 public static void main(String[] args) 5 { 6 try( 7 //创建一个ObjectInputStream输入流 8 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("b.txt"))) 9 { 10 //从输入流中读取一个java对象,并将其强制类型转换为Person类 11 Person p = (Person)ois.readObject(); 12 System.out.println("名字为:"+ p.getName() + "\n年龄为:" + p.getAge()); 13 } 14 catch(Exception ex) 15 { 16 ex.printStackTrace(); 17 } 18 19 } 20 21 }
反序列化读取的仅仅是java对象的数据,而不是java类,因此采用反序列化恢复java对象时,必须提供该java对象所属类的calss文件,否则将会引发异常。
Person类只有一个有参数的构造器,没有无参数的构造器,而且该构造器内有一个普通的打印语句。当反序列化读取java对象时,并没有看到程序调用该构造器,这表明反序列化机制无须通过构造器来初始化java对象。
如果受用序列化机制向文件中写入了多个java对象,使用反序列化机制恢复对象时必须按照写入的顺序读取。
当一个可序列化类有多个父类时,这些父类要么有无参数构造器,要么也是可序列化的,否则反序列化时将抛出异常。如果父类是不可序列化的,之上带有无参数的构造器,则该父类中定义的成员变量值不会序列化到二进制流中。
对象引用的序列化
前面的person类的两个成员变量分别是String类型和int类型,如果某个类的成员变量的类型不是基本类型或者String类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型成员变量的类也是不可序列化的。
如下Teacher类持有一个Person类的引用,只有Person类是可序列化的,Teacher类才是可序列化的。如果Person类不可被序列化,则无论Teacher类是否实现Serilizable Externalizable接口,Teacher类都是不可序列化的。
1 public class Teacher implements Serializable 2 { 3 private String name; 4 private Person student; 5 6 public Teacher(String name, Person student) 7 { 8 this.name = name; 9 this.student = student; 10 } 11 12 public String getName() { 13 return name; 14 } 15 16 public void setName(String name) { 17 this.name = name; 18 } 19 20 public Person getStudent() { 21 return student; 22 } 23 24 public void setStudent(Person student) { 25 this.student = student; 26 } 27 28 29 }
现在假设有如下一种特殊情形:程序中有两个Teacher对象,他们的student实例变量都引用同一个Person对象,而日该Person对象还有一个引用变量引用它。如下代码所示:
Person per = new Person("孙悟空",500); Teacher t1 = new Teacher("唐僧",per); Teacher t2 = new Teacher("菩提老祖",per);
上面代码创建了两个Teacher对象和一个Person对象。
这里产生了一个问题,如果先序列化t1对象,则系统将该t1对象所引用的Person对象一起序列化;如果程序再显式序列化per对象,系统将一样会序列化该t2对象,并且将再次序列化该t2对象所引用的Person对象;如果程序再显式序列化per对象,系统将再次序列化该Person对象。这个过程似乎会想输出流中输出三个Person对象。
如果系统想输出流中写入了三个Person对象,从而引起t1和t2所引用的Person对象不是同一个对象。
所以,java序列化机制采用了一种特殊的序列化算法,其算法内容如下:
1:所有保存到此哦鞍中的对象都有一个序列化编号
2:当程序视图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未被序列化过,系统才会将该对象转换成字节序列并输出。
3:如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是再次重写序列化该对象。
下面程序序列化了两个Teacher对象,两个Teacher对象都持有一个引用到同一个Person对象的引用,而且程序两次调用writeObject()方法输出同一个Teacher对象。
1 public class WriteTeacher 2 { 3 public static void main(String[] args) 4 { 5 try( 6 //创建一个ObjectOutputStream输出流 7 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) 8 { 9 Person p = new Person("孙悟空",500); 10 Teacher t1 = new Teacher("唐僧",p); 11 Teacher t2 = new Teacher("菩提老祖",p); 12 13 //依次将4个对象写入输出流 14 oos.writeObject(t1); 15 oos.writeObject(t2); 16 oos.writeObject(p); 17 oos.writeObject(t2); 18 } 19 catch(IOException ex) 20 { 21 ex.printStackTrace(); 22 } 23 } 24 25 }
上面程序4次调用了writeObject()方法来输出对象,实际上只序列化了三个对象,而且序列的两个Teacher对象的stydent引用实际是同一个Person对象。下面程序读取虚礼欸文件中的对象即可证明这一点。
1 public class ReadTeacher 2 { 3 public static void main(String[] args) 4 { 5 try( 6 //创建一个ObjectInputStream输入流 7 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("teacher.txt"))) 8 { 9 //依次读取ObjectInputStream输入流中的4个对象 10 Teacher t1 = (Teacher)ois.readObject(); 11 Teacher t2 = (Teacher)ois.readObject(); 12 Person p = (Person)ois.readObject(); 13 Teacher t3 = (Teacher)ois.readObject(); 14 15 //输出true 16 System.out.println("t1的student引用和p是否相同:"+ (t1.getStudent() == p)); 17 18 //输出true 19 System.out.println("t2的student引用和p是否相同:"+ (t2.getStudent() == p)); 20 21 //输出true 22 System.out.println("t2和t3是否是同一个对象:"+ (t2 == t3)); 23 } 24 catch(Exception ex) 25 { 26 ex.printStackTrace(); 27 } 28 29 } 30 31 }
上面程序依次读取了序列化文件中的4个java对象,但通过比较判断,不难发现t2和t3是同一个java对象,t1的student引用的,t2的student引用的和p引用变量引用的也是同一个java对象。
由于java序列化机制使然:如果多层虚礼欸同一个java对象时,只有第一次序列化时才会把该java对象转换成字节序列并输出,这样可能引起一个潜在的问题,但程序序列化一个可变对象时,只有第一次使用writeObject()方法输出时才会将该对象转换成字节序列并输出,当程序再次调用writeObject()方法时,程序之上输出前面的序列化编号,即使后面该对象的实例变量值已被改变,改变的实例变量值也不会被输出。如下程序所示:
1 public class SerializaMutable 2 { 3 public static void main(String[] args) 4 { 5 try( 6 //创建一个ObjectOutputStream输出流 7 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("mutable.txt")); 8 //创建一个ObjectInputStream输入流 9 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("mutable.txt"))) 10 { 11 Person per = new Person("孙悟空",500); 12 //系统将per对象转换成字节序列并输出 13 oos.writeObject(per); 14 //改变per对象的name实例变量的值 15 per.setName("猪八戒"); 16 //系统只是输出序列化编号,所以改变后的name不会被序列化 17 oos.writeObject(per); 18 19 Person p1 = (Person) ois.readObject(); 20 Person p2 = (Person) ois.readObject(); 21 22 //下面输出true,即反序列化后p1等于p2 23 System.out.println(p1 == p2); 24 //下面依然看到输出"孙悟空",即改变后的实例变量没有被序列化 25 System.out.println(p2.getName()); 26 } 27 catch(Exception ex) 28 { 29 ex.printStackTrace(); 30 } 31 32 } 33 34 }
程序中先使用writeObject()方法写入了一个Person对象,接着程序改变了Person对象的name实例变量值,然后程序再次输出Person对象,但这次的输出已经不会将Person对象转换成字节序列并输出了,而是仅仅输出了一个序列化编号。
程序中代码两次调用readObject()方法读取了序列化文件的java对象,比较两次读取的java对象将完全相同,程序输出第二次读取Person对象的name实例变量的值依然是“孙悟空”,表明改变后的Person对象并没有被写入----这与java序列化机制相符。
当使用java序列化机制序列化可变对象时一定要注意,只有第一次调用wirteObject()方法来输出对象时才会将对象转换成字节序列,并写入到ObjectOutputStream;在后面程序中即使该对象的实例变量发生了改变,再次调用writeObject()方法输出该对象时,改变后的实例变量也不会被输出。