之前的文章中我们介绍过有关字节流字符流的使用,当时我们对于将一个对象输出到流中的操作,使用DataOutputStream流将该对象中的每个属性值逐个输出到流中,读出时相反。在我们看来这种行为实在是繁琐,尤其是在这个对象中属性值很多的时候。基于此,Java中对象的序列化机制就可以很好的解决这种操作。本篇就简单的介绍Java对象序列化,主要内容如下:
- 简洁的代码实现
- 序列化实现的基本算法
- 两种特殊的情况
- 自定义序列化机制
- 序列化的版本控制
一、简洁的代码实现
在介绍对象序列化的使用方法之前,先看看我们之前是怎么存储一个对象类型的数据的。
//简单定义一个Student类 public class Student { private String name; private int age; public Student(){} public Student(String name,int age){ this.name = name; this.age=age; } public void setName(String name){ this.name = name; } public void setAge(int age){ this.age = age; } public String getName(){ return this.name; } public int getAge(){ return this.age; } //重写toString @Override public String toString(){ return ("my name is:"+this.name+" age is:"+this.age); } }</div>
//main方法实现了将对象写入文件并读取出来 public static void main(String[] args) throws IOException{ DataOutputStream dot = new DataOutputStream(new FileOutputStream("hello.txt")); Student stuW = new Student("walker",21); //将此对象写入到文件中 dot.writeUTF(stuW.getName()); dot.writeInt(stuW.getAge()); dot.close(); //将对象从文件中读出 DataInputStream din = new DataInputStream(new FileInputStream("hello.txt")); Student stuR = new Student(); stuR.setName(din.readUTF()); stuR.setAge(din.readInt()); din.close(); System.out.println(stuR); }</div>
输出结果:my name is:walker age is:21
显然这种代码书写是繁琐的,接下来我们看看,如何使用序列化来完成保存对象的信息。
public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt")); Student stuW = new Student("walker",21); oos.writeObject(stuW); oos.close(); //从文件中读取该对象返回 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt")); Student stuR = (Student)ois.readObject(); System.out.println(stuR); }</div>
写入文件时,只用了一条语句就是writeObject,读取时也是只用了一条语句readObject。并且Student中的那些set,get方法都用不到了。是不是很简洁呢?接下来介绍实现细节。
二、实现序列化的基本算法
在这种机制中,每个对象都是对应着唯一的一个序列号,而每个对象在被保存的时候也是根据这个序列号来对应着每个不同的对象,对象序列化就是指利用了每个对象的序列号进行保存和读取的。首先以写对象到流中为例,对于每个对象,第一次遇到的时候会将这个对象的基本信息保存到流中,如果当前遇到的对象已经被保存过了,就不会再次保存这些信息,转而记录此对象的序列号(因为数据没必要重复保存)。对于读的情况,从流中遇到的每个对象,如果第一次遇到,直接输出,如果读取到的是某个对象的序列号,就会找到相关联的对象,输出。
说明几点,一个对象要想是可序列化的,就必须实现接口 java.io.Serializable;,这是一个标记接口,不用实现任何的方法。而我们的ObjectOutputStream流,就是一个可以将对象信息转为字节的流,构造函数如下:
public ObjectOutputStream(OutputStream out)</div>
也就是所有字节流都可以作为参数传入,兼容一切字节操作。在这个流中定义了writeObject和readObject方法,实现了序列化对象和反序列化对象。当然,我们也是可以通过在类中实现这两个方法来自定义序列化机制,具体的后文介绍。此处我们只需要了解整个序列化机制,所有的对象数据只会保存一份,至于相同的对象再次出现,只保存对应的序列号。下面,通过两个特殊的情况直观的感受下他的这个基本算法。
三、两个特殊的实例
先看第一个实例:
public class Student implements Serializable { String name; int age; Teacher t; //另外一个对象类型 public Student(){} public Student(String name,int age,Teacher t){ this.name = name; this.age=age; this.t = t; } public void setName(String name){this.name = name;} public void setAge(int age){this.age = age;} public void setT(Teacher t){this.t = t;} public String getName(){return this.name;} public int getAge(){return this.age;} public Teacher getT(){return this.t;} } public class Teacher implements Serializable { String name; public Teacher(String name){ this.name = name; } } public static void main(String[] args) throws IOException, ClassNotFoundException { Teacher t = new Teacher("li"); Student stu1 = new Student("walker",21,t); Student stu2 = new Student("yam",22,t); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt")); oos.writeObject(stu1); oos.writeObject(stu2); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt")); Student stuR1 = (Student)ois.readObject(); Student stuR2 = (Student)ois.readObject(); if (stuR1.getT() == stuR2.getT()) System.out.println("相同对象"); }</div>
结果是很显而易见的,输出了相同对象。我们在main函数中定义了两个student类型对象,他们却都引用的同一个teacher对象在内部。完成序列化之后,反序列化出来两个对象,通过比较他们内部的teacher对象是否是同一个实例,可以看出来,在序列化第一个student对象的时候t是被写入流中的,但是在遇到第二个student对象的teacher对象实例时,发现前面已经写过了,于是不再写入流中,只保存对应的序列号作为引用。当然在反序列化的时候,原理类似。这和我们上面介绍的基本算法是一样的。
下面看第二个特殊实例:
public class Student implements Serializable { String name; Teacher t; } public class Teacher implements Serializable { String name; Student stu; } public static void main(String[] args) throws IOException, ClassNotFoundException { Teacher t = new Teacher(); Student s =new Student(); t.name = "walker"; t.stu = s; s.name = "yam"; s.t = t; ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt")); oos.writeObject(t); oos.writeObject(s); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt")); Teacher tR = (Teacher)ois.readObject(); Student sR = (Student)ois.readObject(); if(tR == sR.t && sR == tR.stu)System.out.println("ok"); }</div>
输出的结果是ok,这个例子可以叫做:循环引用。从结果我们可以看出来,序列化之前两个对象存在的相互的引用关系,经过序列化之后,两者之间的这种引用关系是依然存在的。其实按照我们之前介绍的判断算法来看,首先我们先序列化了teacher对象,因为他内部引用了student的对象,两者都是第一次遇到,所以将两者序列化到流中,然后我们去序列化student对象,发现这个对象以及内部的teacher对象都已经被序列化了,于是只保存对应的序列号。读取的时候根据序列号恢复对象。
四、自定义序列化机制
综上,我们已经介绍完了基本的序列化与反序列化的知识。但是往往我们会有一些特殊的要求,这种默认的序列化机制虽然已经很完善了,但是有些时候还是不能满足我们的需求。所以我们看看如何自定义序列化机制。自定义序列化机制中,我们会使用到一个关键字,它也是我们之前在看源码的时候经常遇到的,transient。将字段声明transient,等于是告诉默认的序列化机制,这个字段你不要给我写到流中去,我会自己处理的。
public class Student implements Serializable { String name; transient int age; public String toString(){ return this.name + ":" + this.age; } } public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("h