1. 程式人生 > 實用技巧 >java基礎——序列化

java基礎——序列化

一、序列化的含義、意義及使用場景

  • 序列化:將物件寫入到IO流中
  • 反序列化:從IO流中恢復物件
  • 意義:序列化機制允許將實現序列化的Java物件轉換位位元組序列,這些位元組序列可以儲存在磁碟上,或通過網路傳輸,以達到以後恢復成原來的物件。序列化機制使得物件可以脫離程式的執行而獨立存在。
  • 使用場景:所有可在網路上傳輸的物件都必須是可序列化的,比如RMI(remote method invoke,即遠端方法呼叫),傳入的引數或返回的物件都是可序列化的,否則會出錯;所有需要儲存到磁碟的java物件都必須是可序列化的。通常建議:程式建立的每個JavaBean類都實現Serializeable介面。

二、序列化實現的方式

如果需要將某個物件儲存到磁碟上或者通過網路傳輸,那麼這個類應該實現Serializable介面或者Externalizable介面之一。

1、Serializable

1.1 普通序列化

Serializable介面是一個標記介面,不用實現任何方法。一旦實現了此介面,該類的物件就是可序列化的。

  1. 序列化步驟:
  • 步驟一:建立一個ObjectOutputStream輸出流;

  • 步驟二:呼叫ObjectOutputStream物件的writeObject輸出可序列化物件。

    publicclassPersonimplementsSerializable{
    privateStringname;
    private
    intage;
    //我不提供無參構造器
    publicPerson(Stringname,intage){
    this.name=name;
    this.age=age;
    }

    @Override
    publicStringtoString(){
    return"Person{"+
    "name='"+name+'\''+
    ",age="+age+
    '}';
    }
    }

    publicclassWriteObject{
    publicstaticvoidmain(String[]args){
    try(//建立一個ObjectOutputStream輸出流
    ObjectOutputStreamoos=newObjectOutputStream(newFileOutputStream(
    "object.txt"))){
    //將物件序列化到檔案s
    Personperson=newPerson("9龍",23);
    oos.writeObject(person);
    }catch(Exceptione){
    e.printStackTrace();
    }
    }
    }
  1. 反序列化步驟:
  • 步驟一:建立一個ObjectInputStream輸入流;

  • 步驟二:呼叫ObjectInputStream物件的readObject()得到序列化的物件。

    我們將上面序列化到person.txt的person物件反序列化回來

    publicclassPersonimplementsSerializable{
    privateStringname;
    privateintage;
    //我不提供無參構造器
    publicPerson(Stringname,intage){
    System.out.println("反序列化,你呼叫我了嗎?");
    this.name=name;
    this.age=age;
    }

    @Override
    publicStringtoString(){
    return"Person{"+
    "name='"+name+'\''+
    ",age="+age+
    '}';
    }
    }

    publicclassReadObject{
    publicstaticvoidmain(String[]args){
    try(//建立一個ObjectInputStream輸入流
    ObjectInputStreamois=newObjectInputStream(newFileInputStream("person.txt"))){
    Personbrady=(Person)ois.readObject();
    System.out.println(brady);
    }catch(Exceptione){
    e.printStackTrace();
    }
    }
    }
    //輸出結果
    //Person{name='9龍',age=23}

    waht???? 輸出告訴我們,反序列化並不會呼叫構造方法。反序列的物件是由JVM自己生成的物件,不通過構造方法生成。

1.2 成員是引用的序列化

如果一個可序列化的類的成員不是基本型別,也不是String型別,那這個引用型別也必須是可序列化的;否則,會導致此類不能序列化。

看例子,我們新增一個Teacher類。將Person去掉實現Serializable介面程式碼。

publicclassPerson{
//省略相關屬性與方法
}
publicclassTeacherimplementsSerializable{

privateStringname;
privatePersonperson;

publicTeacher(Stringname,Personperson){
this.name=name;
this.person=person;
}

publicstaticvoidmain(String[]args)throwsException{
try(ObjectOutputStreamoos=newObjectOutputStream(newFileOutputStream("teacher.txt"))){
Personperson=newPerson("路飛",20);
Teacherteacher=newTeacher("雷利",person);
oos.writeObject(teacher);
}
}
}

我們看到程式直接報錯,因為Person類的物件是不可序列化的,這導致了Teacher的物件不可序列化

1.3 同一物件序列化多次的機制

同一物件序列化多次,會將這個物件序列化多次嗎?答案是否定的。

publicclassWriteTeacher{
publicstaticvoidmain(String[]args)throwsException{
try(ObjectOutputStreamoos=newObjectOutputStream(newFileOutputStream("teacher.txt"))){
Personperson=newPerson("路飛",20);
Teachert1=newTeacher("雷利",person);
Teachert2=newTeacher("紅髮香克斯",person);
//依次將4個物件寫入輸入流
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(person);
oos.writeObject(t2);
}
}
}

依次將t1、t2、person、t2物件序列化到檔案teacher.txt檔案中。

注意:反序列化的順序與序列化時的順序一致

publicclassReadTeacher{
publicstaticvoidmain(String[]args){
try(ObjectInputStreamois=newObjectInputStream(newFileInputStream("teacher.txt"))){
Teachert1=(Teacher)ois.readObject();
Teachert2=(Teacher)ois.readObject();
Personp=(Person)ois.readObject();
Teachert3=(Teacher)ois.readObject();
System.out.println(t1==t2);
System.out.println(t1.getPerson()==p);
System.out.println(t2.getPerson()==p);
System.out.println(t2==t3);
System.out.println(t1.getPerson()==t2.getPerson());
}catch(Exceptione){
e.printStackTrace();
}
}
}
//輸出結果
//false
//true
//true
//true
//true

從輸出結果可以看出,Java序列化同一物件,並不會將此物件序列化多次得到多個物件。

  • Java序列化演算法
  1. 所有儲存到磁碟的物件都有一個序列化編碼號

  2. 當程式試圖序列化一個物件時,會先檢查此物件是否已經序列化過,只有此物件從未(在此虛擬機器)被序列化過,才會將此物件序列化為位元組序列輸出。

  3. 如果此物件已經序列化過,則直接輸出編號即可。

    圖示上述序列化過程。

1.4 java序列化演算法潛在的問題

由於java序利化演算法不會重複序列化同一個物件,只會記錄已序列化物件的編號。如果序列化一個可變物件(物件內的內容可更改)後,更改了物件內容,再次序列化,並不會再次將此物件轉換為位元組序列,而只是儲存序列化編號。

publicclassWriteObject{
publicstaticvoidmain(String[]args){
try(ObjectOutputStreamoos=newObjectOutputStream(newFileOutputStream("person.txt"));
ObjectInputStreamios=newObjectInputStream(newFileInputStream("person.txt"))){
//第一次序列化person
Personperson=newPerson("9龍",23);
oos.writeObject(person);
System.out.println(person);

//修改name
person.setName("海賊王");
System.out.println(person);
//第二次序列化person
oos.writeObject(person);

//依次反序列化出p1、p2
Personp1=(Person)ios.readObject();
Personp2=(Person)ios.readObject();
System.out.println(p1==p2);
System.out.println(p1.getName().equals(p2.getName()));
}catch(Exceptione){
e.printStackTrace();
}
}
}
//輸出結果
//Person{name='9龍',age=23}
//Person{name='海賊王',age=23}
//true
//true
1.5 可選的自定義序列化
  1. 有些時候,我們有這樣的需求,某些屬性不需要序列化。使用transient關鍵字選擇不需要序列化的欄位。

    publicclassPersonimplementsSerializable{
    //不需要序列化名字與年齡
    privatetransientStringname;
    privatetransientintage;
    privateintheight;
    privatetransientbooleansinglehood;
    publicPerson(Stringname,intage){
    this.name=name;
    this.age=age;
    }
    //省略get,set方法
    }

    publicclassTransientTest{
    publicstaticvoidmain(String[]args)throwsException{
    try(ObjectOutputStreamoos=newObjectOutputStream(newFileOutputStream("person.txt"));
    ObjectInputStreamios=newObjectInputStream(newFileInputStream("person.txt"))){
    Personperson=newPerson("9龍",23);
    person.setHeight(185);
    System.out.println(person);
    oos.writeObject(person);
    Personp1=(Person)ios.readObject();
    System.out.println(p1);
    }
    }
    }
    //輸出結果
    //Person{name='9龍',age=23',singlehood=true',height=185cm}
    //Person{name='null',age=0',singlehood=false',height=185cm}

    從輸出我們看到,使用transient修飾的屬性,java序列化時,會忽略掉此欄位,所以反序列化出的物件,被transient修飾的屬性是預設值。對於引用型別,值是null;基本型別,值是0;boolean型別,值是false。

  2. 使用transient雖然簡單,但將此屬性完全隔離在了序列化之外。java提供了可選的自定義序列化。可以進行控制序列化的方式,或者對序列化資料進行編碼加密等。

    privatevoidwriteObject(java.io.ObjectOutputStreamout)throwsIOException;
    privatevoidreadObject(java.io.ObjectIutputStreamin)throwsIOException,ClassNotFoundException
    ;
    privatevoidreadObjectNoData()throwsObjectStreamException;

    通過重寫writeObject與readObject方法,可以自己選擇哪些屬性需要序列化, 哪些屬性不需要。如果writeObject使用某種規則序列化,則相應的readObject需要相反的規則反序列化,以便能正確反序列化出物件。這裡展示對名字進行反轉加密。

    publicclassPersonimplementsSerializable{
    privateStringname;
    privateintage;
    //省略構造方法,get及set方法

    privatevoidwriteObject(ObjectOutputStreamout)throwsIOException{
    //將名字反轉寫入二進位制流
    out.writeObject(newStringBuffer(this.name).reverse());
    out.writeInt(age);
    }

    privatevoidreadObject(ObjectInputStreamins)throwsIOException,ClassNotFoundException{
    //將讀出的字串反轉恢復回來
    this.name=((StringBuffer)ins.readObject()).reverse().toString();
    this.age=ins.readInt();
    }
    }

    當序列化流不完整時,readObjectNoData()方法可以用來正確地初始化反序列化的物件。例如,使用不同類接收反序列化物件,或者序列化流被篡改時,系統都會呼叫readObjectNoData()方法來初始化反序列化的物件。

  3. 更徹底的自定義序列化

    ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
    ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

    • writeReplace:在序列化時,會先呼叫此方法,再呼叫writeObject方法。此方法可將任意物件代替目標序列化物件

      publicclassPersonimplementsSerializable{
      privateStringname;
      privateintage;
      //省略構造方法,get及set方法

      privateObjectwriteReplace()throwsObjectStreamException{
      ArrayList<Object>list=newArrayList<>(2);
      list.add(this.name);
      list.add(this.age);
      returnlist;
      }

      publicstaticvoidmain(String[]args)throwsException{
      try(ObjectOutputStreamoos=newObjectOutputStream(newFileOutputStream("person.txt"));
      ObjectInputStreamios=newObjectInputStream(newFileInputStream("person.txt"))){
      Personperson=newPerson("9龍",23);
      oos.writeObject(person);
      ArrayListlist=(ArrayList)ios.readObject();
      System.out.println(list);
      }
      }
      }
      //輸出結果
      //[9龍,23]
    • readResolve:反序列化時替換反序列化出的物件,反序列化出來的物件被立即丟棄。此方法在readeObject後呼叫。

      publicclassPersonimplementsSerializable{
      privateStringname;
      privateintage;
      //省略構造方法,get及set方法
      privateObjectreadResolve()throwsObjectStreamException{
      returnnew("brady",23);
      }
      publicstaticvoidmain(String[]args)throwsException{
      try(ObjectOutputStreamoos=newObjectOutputStream(newFileOutputStream("person.txt"));
      ObjectInputStreamios=newObjectInputStream(newFileInputStream("person.txt"))){
      Personperson=newPerson("9龍",23);
      oos.writeObject(person);
      HashMapmap=(HashMap)ios.readObject();
      System.out.println(map);
      }
      }
      }
      //輸出結果
      //{brady=23}

      readResolve常用來反序列單例類,保證單例類的唯一性。

      注意:readResolve與writeReplace的訪問修飾符可以是private、protected、public,如果父類重寫了這兩個方法,子類都需要根據自身需求重寫,這顯然不是一個好的設計。通常建議對於final修飾的類重寫readResolve方法沒有問題;否則,重寫readResolve使用private修飾。

2、Externalizable:強制自定義序列化

通過實現Externalizable介面,必須實現writeExternal、readExternal方法。

publicinterfaceExternalizableextendsjava.io.Serializable{
voidwriteExternal(ObjectOutputout)throwsIOException;
voidreadExternal(ObjectInputin)throwsIOException,ClassNotFoundException;
}
publicclassExPersonimplementsExternalizable{

privateStringname;
privateintage;
//注意,必須加上pulic無參構造器
publicExPerson(){
}

publicExPerson(Stringname,intage){
this.name=name;
this.age=age;
}

@Override
publicvoidwriteExternal(ObjectOutputout)throwsIOException{
//將name反轉後寫入二進位制流
StringBufferreverse=newStringBuffer(name).reverse();
System.out.println(reverse.toString());
out.writeObject(reverse);
out.writeInt(age);
}

@Override
publicvoidreadExternal(ObjectInputin)throwsIOException,ClassNotFoundException{
//將讀取的字串反轉後賦值給name例項變數
this.name=((StringBuffer)in.readObject()).reverse().toString();
System.out.println(name);
this.age=in.readInt();
}

publicstaticvoidmain(String[]args)throwsIOException,ClassNotFoundException{
try(ObjectOutputStreamoos=newObjectOutputStream(newFileOutputStream("ExPerson.txt"));
ObjectInputStreamois=newObjectInputStream(newFileInputStream("ExPerson.txt"))){
oos.writeObject(newExPerson("brady",23));
ExPersonep=(ExPerson)ois.readObject();
System.out.println(ep);
}
}
}
//輸出結果
//ydarb
//brady
//ExPerson{name='brady',age=23}

注意:Externalizable介面不同於Serializable介面,實現此介面必須實現介面中的兩個方法實現自定義序列化,這是強制性的;特別之處是必須提供pulic的無參構造器,因為在反序列化的時候需要反射建立物件。

3、兩種序列化對比

實現Serializable介面實現Externalizable介面
系統自動儲存必要的資訊 程式設計師決定儲存哪些資訊
Java內建支援,易於實現,只需要實現該介面即可,無需任何程式碼支援 必須實現介面內的兩個方法
效能略差 效能略好

雖然Externalizable介面帶來了一定的效能提升,但變成複雜度也提高了,所以一般通過實現Serializable介面進行序列化。

三、序列化版本號serialVersionUID

我們知道,反序列化必須擁有class檔案,但隨著專案的升級,class檔案也會升級,序列化怎麼保證升級前後的相容性呢?

java序列化提供了一個private static final long serialVersionUID 的序列化版本號,只有版本號相同,即使更改了序列化屬性,物件也可以正確被反序列化回來。

publicclassPersonimplementsSerializable{
//序列化版本號
privatestaticfinallongserialVersionUID=1111013L;
privateStringname;
privateintage;
//省略構造方法及get,set
}

如果反序列化使用的class的版本號與序列化時使用的不一致,反序列化會報InvalidClassException異常。

序列化版本號可自由指定,如果不指定,JVM會根據類資訊自己計算一個版本號,這樣隨著class的升級,就無法正確反序列化;不指定版本號另一個明顯隱患是,不利於jvm間的移植,可能class檔案沒有更改,但不同jvm可能計算的規則不一樣,這樣也會導致無法反序列化。

什麼情況下需要修改serialVersionUID呢?分三種情況。

  • 如果只是修改了方法,反序列化不容影響,則無需修改版本號;
  • 如果只是修改了靜態變數,瞬態變數(transient修飾的變數),反序列化不受影響,無需修改版本號;
  • 如果修改了非瞬態變數,則可能導致反序列化失敗。如果新類中例項變數的型別與序列化時類的型別不一致,則會反序列化失敗,這時候需要更改serialVersionUID。如果只是新增了例項變數,則反序列化回來新增的是預設值;如果減少了例項變數,反序列化時會忽略掉減少的例項變數。

四、總結

  1. 所有需要網路傳輸的物件都需要實現序列化介面,通過建議所有的javaBean都實現Serializable介面。
  2. 物件的類名、例項變數(包括基本型別,陣列,對其他物件的引用)都會被序列化;方法、類變數、transient例項變數都不會被序列化。
  3. 如果想讓某個變數不被序列化,使用transient修飾。
  4. 序列化物件的引用型別成員變數,也必須是可序列化的,否則,會報錯。
  5. 反序列化時必須有序列化物件的class檔案。
  6. 當通過檔案、網路來讀取序列化後的物件時,必須按照實際寫入的順序讀取。
  7. 單例類序列化,需要重寫readResolve()方法;否則會破壞單例原則。
  8. 同一物件序列化多次,只有第一次序列化為二進位制流,以後都只是儲存序列化編號,不會重複序列化。
  9. 建議所有可序列化的類加上serialVersionUID 版本號,方便專案升級。