首頁  >  文章  >  Java  >  詳解Java物件序列化與反序列化的範例程式碼

詳解Java物件序列化與反序列化的範例程式碼

黄舟
黄舟原創
2017-03-31 10:37:471377瀏覽

這篇文章主要介紹了Java 物件序列化與反序列化,小編覺得蠻不錯的,現在分享給大家,也給大家做個參考。一起跟著小編過來看看吧

之前的文章中我們介紹過有關字節流字符流的使用,當時我們對於將一個對象輸出到流中的操作,使用DataOutputStream流將該對像中的每個屬性值逐一輸出到流中,讀出時相反。在我們看來這種行為實在是繁瑣,尤其是在這個物件中屬性值很多的時候。基於此,Java中物件的序列化機制就可以很好的解決這種操作。本篇就簡單的介紹Java物件序列化,主要內容如下:

  1. 簡潔的程式碼實作

  2. ##序列化實作的基本演算法

  3. 兩個特殊的情況

  4. 自訂序列化機制

  5. 序列化的版本控制

一、簡潔的程式碼實作

在介紹物件序列化的使用方法之前,先看看我們之前是怎麼儲存一個物件類型的資料的。

//简单定义一个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);
 }
}
//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);
 }

輸出結果: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);
 }

寫入檔案時,只用了一條語句就是writeObject,讀取時也是只用了一條語句readObject。而Student中的那些set,get方法都用不到了。是不是很簡潔呢?接下來介紹實作細節。

二、實現序列化的基本演算法

在這個機制中,每個物件都是對應著唯一的一個序號,而每個對像在被保存的時候也是根據這個序號來對應著每個不同的對象,而對象序列化就是指利用了每個對象的序號進行保存和讀取的。首先以寫對像到流中為例,對於每個對象,第一次遇到的時候會將這個對象的基本資訊保存到流中,如果當前遇到的對像已經被保存過了,就不會再保存這些信息,轉而記錄此物件的序號(因為資料沒必要重複保存)。對於讀取的情況,從流中遇到的每個對象,如果第一次遇到,直接輸出,如果讀取到的是某個對象的序號,就會找到相關聯的對象,輸出。


說明幾點,一個物件要想是可序列化的,就必須實作

介面 java.io.Serializable;,這是一個標記接口,不用實作任何的方法。而我們的ObjectOutputStream流,就是一個可以將物件資訊轉為位元組的流,建構子如下:

public ObjectOutputStream(OutputStream out)

也就是所有位元組流都可以作為參數傳入,相容一切位元組操作。在這個流中定義了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("相同对象");
 }

結果是很顯而易見的,輸出了相同對象。我們在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");

 }

输出的结果是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("hello.txt"));
 Student stu = new Student();
 stu.name = "walker";stu.age = 21;
 oos.writeObject(stu);

 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt"));
 Student stuR = (Student)ois.readObject();

 System.out.println(stuR);
 }

输出结果:walker:0

我们不是给age字段赋初始值了么,怎么会是0呢?正如我们上文所说的一样,被transient修饰的字段不会被写入流中,自然读取出来就没有值,默认是0。下面看看我们怎么自己来序列化这个age。

//改动过的student类,main方法没有改动,大家可以往上看
public class Student implements Serializable {

 String name;
 transient int age;

 private void writeObject(ObjectOutputStream oos) throws IOException {
 oos.defaultWriteObject();

 oos.writeInt(25);
 }

 private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
 ois.defaultReadObject();

 age = ois.readInt();
 }

 public String toString(){
 return this.name + ":" + this.age;
 }
}

输出结果:walker:25

结果既不是我么初始化的21,也不是0,而是我们在writeObject方法中写的25。现在我们一点一点看看每个步骤的意义。首先,要想要实现自定义序列化,就需要在该对象定义的类中实现两个方法,writeObject和readObject,而且格式必须和上面贴出来的一样,笔者试过改动方法修饰符,结果导致不能成功序列化。这是因为,Java采用反射机制,检查该对象所在的类中有没有实现这两个方法,没有的话就使用默认的ObjectOutputStream中的这个方法序列化所有字段,如果有的话就执行你自己实现的这个方法。

接下来,看看这两个方法实现的细节,先看writeObject方法,参数是ObjectOutputStream 类型的,这个拿到的是我们在main方法中定义的ObjectOutputStream 对象,要不然它怎么知道该把对象写到那个地方去呢?第一行我们调用的是oos.defaultWriteObject();这个方法实现的功能是,将当前对象中所有没有被transient修饰的字段写入流中,第二条语句我们显式的调用了writeInt方法将age的值写入流中。读取的方法类似,此处不再赘述。

五、版本控制

最后我们来看看,序列化过程的的版本控制问题。在我们将一个对象序列化到流中之后,该对象对应的类的结构改变了,如果此时我们再次从流中将之前保存的对象读取出来,会发生什么?这要分情况来说,如果原类中的字段被删除了,那从流中输出的对应的字段将会被忽略。如果原类中增加了某个字段,那新增的字段的值就是默认值。如果字段的类型发生了改变,抛出异常。在Java中每个类都会有一个记录版本号的变量:static final serivalVersionUID = 115616165165L,此处的值只用于演示并不对应任意某个类。这个版本号是根据该类中的字段等一些属性信息计算出来的,唯一性较高。每次读出的时候都会去比较之前和现在的版本号确认是否发生版本不一致情况,如果版本不一致,就会按照上述的情形分别做处理。

对象的序列化就写完了,如果有什么内容不妥的地方,希望大家指出!

以上是詳解Java物件序列化與反序列化的範例程式碼的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn