详解java序列化

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

我们可以通过序列化来保存一个对象的状态(实例变量)到文件中,也可以从这个格式化的文件中很容易地读取对象的状态从而可以恢复我们保存的对象。

用来实现序列化的类都在java.io包中,我们常用的类或接口有:ObjectOutputStream:提供序列化对象并把其写入流的方法
ObjectInputStream:读取流并反序列化对象
Serializable:一个对象想要被序列化,那么它的类就要实现此接口
下面我们先通过一个简单的例子演示一起序列化/反序列化的过程
Book.java
package kevin.seria;
import java.io.Serializable;
public class Book implements Serializable{
private int isbn;
public Book(int isbn) {
super();
this.isbn = isbn;
}
public int getIsbn() {
return isbn;
}
public void setIsbn(int isbn) {
this.isbn = isbn;
}
@Override
public String toString() {
return"Book [isbn=" + isbn + "]";
}
}
package kevin.seria;
import java.io.Serializable;
public class Student implements Serializable {
private Book book;
private String name;
public Student(Book book, String name) {
super();
this.book = book;
= name;
}
public Book getBook() {
return book;
}
public void setBook(Book book) {
this.book = book;
}
public String getName() {
return name;
}
public void setName(String name) {
= name;
}
@Override
public String toString() {
return"Student [book=" + book + ", name=" + name + "]";
}
}
package kevin.seria;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Simulator {
public static void main(String[] args) {
new Simulator().go();
}
private void go(){
Student student = new Student(new Book(2011),"kevin");
try {
ObjectOutputStream out = new
ObjectOutputStream(new FileOutputStream("seria"));
out.writeObject(student); //
System.out.println("object has been written..");
out.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
try{
ObjectInputStream in = new ObjectInputStream(new FileInputStream("seria"));
Student studentRead = (Student) in.readObject();
System.out.println("object read here:");
System.out.println(studentRead);
}catch(FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
这个程序运行的结果如下:
我们可以看到,读取到的对象与保存的对象状态一样。

这里有几点需要说明一下:
1、基本类型的数据可以直接序列化
2、对象要被序列化,它的类必须要实现Serializable接口;如果一个类中有
引用类型的实例变量,这个引用类型也要实现Serializable接口。

比如
上面的例子中,Student类中有一个Book类型的实例就是,要想让
Student的对象成功序列化,那么Book也必须要实现Serializable接口。

3、我们看这个语句:
ObjectOutputStream out = new ObjectOutputStream(new
FileOutputStream("seria"));
我们知道 FileOutputStream类有一个带有两个参数的重载
Constructor——FileOutputStream(String,boolean),其第二个参数如果
为true且String代表的文件存在,那么将把新的内容写到原来文件的
末尾而非重写这个文件,这里我们不能用这个版本的构造函数,也就
是说我们必须重写这个文件,否则在读取这个文件反序列化的过程中
就会抛出异常,导致只有我们第一次写到这个文件中的对象可以被反
序列化,之后程序就会出错。

下面的问题是如果我们上面用到的Book类没有实现Serializable接口,但是我们还想序列化Student类的对象,怎么办。

Java为我们提供了transient这个关键字。

如果一个变量被声明成transient,那么在序列化的过程中,这个变量是会被无视的。

我们还是通过对上面的代码进行小的修改来说明这个问题。

新的Book类不实现Serializable接口
package kevin.seria;
public class Book{
private int isbn;
public Book(int isbn) {
super();
this.isbn = isbn;
}
public int getIsbn() {
return isbn;
}
public void setIsbn(int isbn) {
this.isbn = isbn;
}
@Override
public String toString() {
return"Book [isbn=" + isbn + "]";
}
}
因为我们要序列化Student类的对象,所以我们必须实现Serializable接口,然而Book 是Student的一个实例变量,它的类没有实现Serializable接口,所以为了顺序完成序列化,我们把这个实例变量声明为transient以在序列化的过程中跳过它。

package kevin.seria;
import java.io.Serializable;
public class Student implements Serializable {
private transient Book book;
private String name;
public Student(Book book, String name) {
super();
this.book = book;
= name;
}
public Book getBook() {
return book;
}
public void setBook(Book book) {
this.book = book;
}
public String getName() {
return name;
}
public void setName(String name) {
= name;
}
@Override
public String toString() {
return"Student [book=" + book + ", name=" + name + "]";
}
}
Simulator.java和上面的一样,我们看一下运行结果:
可以看到,student对象被成功的序列化了。

因为序列化过程中跳过了Book实例,所以当恢复对象状态的时候,它被赋予了默认值null,这也就意味着我们不能使用它。

那如果Book类没有实现Serializable接口,但我们还想对它的状态进行保存,这可以实现吗?答案是肯定的,到底如何肯定,请听小弟我慢慢道来。

Java针对这种情况有一种特殊的机制——一组私有(回调)方法(这个我们马上在代码中看到),可以在要被序列化的类中实现它们,在序列化和的序列化的过程中它们会被自动调用。

所以在这组方法中我们可以调用
ObjectOutputStream/ObjectInputStream的一些有用方法来实现对象状态的保存。

下面还是通过例子来说明:
Book类和Simulator类都不变,我们来看一下新的Student类:
package kevin.seria;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class Student implements Serializable {
private transient Book book;
private String name;
public Student(Book book, String name) {
super();
this.book = book;
= name;
}
public Book getBook() {
return book;
}
public void setBook(Book book) {
this.book = book;
}
public String getName() {
return name;
}
public void setName(String name) {
= name;
}
//下面这两个方法就是那组特殊的私有方法,它们会在序列化、反序列化的过程中被自动调用
//我们必须保证方法的签名和下面的两个方法完全相同
//这个方法会在序列化的过程中被调用
private void writeObject(ObjectOutputStream out){
try {
out.defaultWriteObject(); //这个方法会把这当前中非静态变量和非transient变量写到流中
//在这里我们就把name写到了流中。

//因为我们要保存Book的状态,所以我们还要想办法把其状态写入流中
out.writeInt(book.getIsbn());//ObjectOutputStream中提供了写基本类型数据的方法
//out.close();//注意,这句千万不能有,否刚将直接导致写入失败
} catch (IOException e) {
e.printStackTrace();
}
}
//这个方法会在反序列化的过程中被调用
private void readObject(ObjectInputStream in){
try {
in.defaultReadObject(); //和defaultWriteObject()方法
相对应,默认的反序列化方法,会从流中读取
//非静态变量和非transient变量
int isbn = in.readInt(); //用这个方法来读取一个int型值,这里我们是读取书号
book = new Book(isbn); //这里我们就通过读取的保存的状
态构造了一个和原来一样的Book对象
//in.close();同样的这句也不能有
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return"Student [book=" + book + ", name=" + name + "]";
}
}
好,看下程序运行的结果,我们这次期望的是Book的状态得到了保存,ok ,check it out
OH YES!正如预料的一样,我们成功了。

呵呵。

要注意的点我在代码的注释中有说明,请好好看下代码。

还有一点我在代码中没写出来,就是一定要注意写入和读取的顺序一定要对应。

像上面如果你是先写基本类型数据的话,那在读取的时候也一定要先读取基本类型的数据,这个原因我想大家都清楚,文件是有position的。

最后,还有两个问题:
1、如果一个类没有实现Serializable接口,但是它的基类实现了,这个类可不可
以序列化?
2、和上面相反,如果一个类实现了Serializable接口,但是它的父类没有实现,
这个类可不可以序列化?
第1个问题:一个类实现了某接口,那么它的所有子类都间接实现了此接口,所以它可以被序列化。

第2个问题:Object是每个类的超类,但是它没有实现 Serializable接口,但是我们照样在序列化对象,所以说明一个类要序列化,它的父类不一定要实现Serializable接口。

但是在父类中定义的状态能被正确的保存以及读取吗?这个我将在下一篇文章中用一个例子来说明(马上就更新),请列位看官多多关注,呵呵。

还有一点,序列化保存对象的状态,而静态(static)变量不是对象的状态,所以它们不会被序列化。

好的,继续为大家带来上一篇文章——详解java序列化(一)
/moreevan/article/details/6697777中最后第2个问题的解答。

第2个问题:Object是每个类的超类,但是它没有实现 Serializable接口,但是我们照样在序列化对象,所以说明一个类要序列化,它的父类不一定要实现Serializable接口。

但是在父类中定义的状态能被正确的保存以及读取吗?
我们还是围绕上面用过的那些类来做一些修改,看下面这个例子。

Book.java这个类和上次的一样,不实现Serializable接口
package kevin.seria;
public class Book{
private int isbn;
public Book(int isbn) {
super();
this.isbn = isbn;
public int getIsbn() {
return isbn;
}
public void setIsbn(int isbn) {
this.isbn = isbn;
}
@Override
public String toString() {
return"Book [isbn=" + isbn + "]";
}
}
这里我们新定义一个类NewBook继承Book类,并且实现 Serializable接口,下面看定义
package kevin.seria;
import java.io.Serializable;
public class NewBook extends Book implements Serializable{ private String author;
public NewBook(int isbn,String author) {
super(isbn);
this.author = author;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
@Override
public String toString() {
return"NewBook [author=" + author + super.toString()
+ "]";
}
}
然后,我们把Student类中Book类型的实例变量修改成NewBook类型,修改后的Student类
package kevin.seria;
import java.io.Serializable;
public class Student implements Serializable {
private NewBook book;
private String name;
public Student(NewBook book, String name) {
super();
this.book = book;
= name;
}
public NewBook getBook() {
return book;
}
public void setBook(NewBook book) {
this.book = book;
}
public String getName() {
return name;
}
public void setName(String name) {
= name;
}
@Override
public String toString() {
return"Student [book=" + book + ", name=" + name + "]";
}
}
Simulator类的内容不变,不过我还是把代码贴出来,可能有的童鞋并没有看上一篇文章:
package kevin.seria;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Simulator {
public static void main(String[] args) {
new Simulator().go();
}
private void go(){
Student student = new Student(new
NewBook(2011,"moree"),"kevin");
try {
ObjectOutputStream out = new
ObjectOutputStream(new FileOutputStream("seria"));
out.writeObject(student); //
System.out.println("object has been written..");
out.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
try{
ObjectInputStream in = new ObjectInputStream(new FileInputStream("seria"));
Student studentRead = (Student) in.readObject();
System.out.println("object read here:");
System.out.println(studentRead);
}catch(FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
好,我们运行这个程序,看下输出结果:
从结果可以看出,对象写成功了,但在读取的过程中出现了问题,具体的异常原因:no valid constructor,即没有有效的构造函数,那么到底是哪个类没有有效的构造函数呢,到底需要一个什么样的构造函数呢?
对于这种情况,即父类没有实现Serializable接口时,但其子类实现了此接口,那么这个子类是可以序列化的,但是在反序列化的过程中会调用父类的无参构造函数,上面异常抛出的原因就是因为我们在Book类中没有一个无参的构造函数。

好,那我们下面就为Book类添加一个默认的构造函数。

package kevin.seria;
public class Book{
private int isbn;
public Book(){
isbn=888;
System.out.println("Book class no-arg constructor invoked..");
}
public Book(int isbn) {
super();
this.isbn = isbn;
}
public int getIsbn() {
return isbn;
}
public void setIsbn(int isbn) {
this.isbn = isbn;
}
@Override
public String toString() {
return"Book [isbn=" + isbn + "]";
}
}
再来执行一次程序,看输出结果如何:
我们可以看到在反序列化的过程中调用了Book类的无参构造执行一个初始化的操作。

好,我们总结一下:如果父类没有实现Serializable接口,但其子类实现了此接口,那么这个子类是可以序列化的,但是在反序列化的过程中会调用父类的无参构造函数,所以在其直接父类(注意是直接父类)中必须有一个无参的构造函数。

好,对于第2个问题的讨论就到这里,接下来我们提出第3个问题:
如果将一个对象写入某文件(比如是a),那么之后对这个对象进行一些修改,然后把修改的对象再写入文件a,那么文件a中会包含该对象的两个版本吗?
至于包不包含,我们代码见分晓:
我们修改Simulator类如下:
package kevin.seria;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Simulator {
public static void main(String[] args) {
new Simulator().go();
}
private void go(){
try {
ObjectOutputStream out = new
ObjectOutputStream(new FileOutputStream("seria"));
Student student1 = new Student(new
NewBook(2011,"moree"),"kevin");
out.writeObject(student1); //
student1.setName("Jordan");
out.writeObject(student1);
student1.setName("Paul");
out.writeObject(student1);
System.out.println("object has been written..");
out.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
try{
ObjectInputStream in = new ObjectInputStream(new FileInputStream("seria"));
Student s1 = (Student)in.readObject();
Student s2 = (Student)in.readObject();
Student s3 = (Student)in.readObject();
System.out.println("Objects read here: ");
System.out.println("Student1's name:
"+s1.getName());
System.out.println("Student2's name:
"+s2.getName());
System.out.println("Student3's name:
"+s3.getName());
}catch(FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
我们希望输出三个人的名字:kevin,Jordan,Paul,那看一下它到底是不是如我们所愿呢:
事与愿违啊,它输出了三个kevin,这证明我们对student名字的修改并没有被写入。

原因是序列化输出过程跟踪写入流的对象,试图将同一个对象写入流时,不会导致该对象被复制,而只是将一个句柄写入流,该句柄指向流中相同对象的第一个对象出现的位置。

那我们如何来避免这种情况,让它输出三个人名呢,方法是在writeObject()之前调用out.reset()方法,这个方法的作用是清除流中保存的写入对象的记录。

我们还是通过代码来看下效果。

try {
ObjectOutputStream out = new
ObjectOutputStream(new FileOutputStream("seria"));
Student student1 = new Student(new
NewBook(2011,"moree"),"kevin");
out.writeObject(student1); //
out.reset();
student1.setName("Jordan");
out.writeObject(student1);
out.reset();
student1.setName("Paul");
out.writeObject(student1);
System.out.println("object has been written..");
out.close();
} catch (FileNotFoundException e) {
这样修改以后就会输出我们期望的结果了。

好,第3个问题的讨论到此为止,如果想深入了解java序列化,可以看下专门讨论这方面的书。

8/18/2011 by Kevin
(End)。

相关文档
最新文档