java序列化,看這篇就夠了

1、序列化的含義、意義及使用場景2、序列化實現的方式一、Serializable1.1 普通序列化1.2 成員是引用的序列化1.3 同一對象序列化屢次的機制1.4 java序列化算法潛在的問題1.5 可選的自定義序列化二、Externalizable:強制自定義序列化三、兩種序列化對比3、序列化版本號serialVersionUID4、總結java

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

  • 序列化:將對象寫入到IO流中
  • 反序列化:從IO流中恢復對象
  • 意義:序列化機制容許將實現序列化的Java對象轉換位字節序列,這些字節序列能夠保存在磁盤上,或經過網絡傳輸,以達到之後恢復成原來的對象。序列化機制使得對象能夠脫離程序的運行而獨立存在。
  • 使用場景:全部可在網絡上傳輸的對象都必須是可序列化的,好比RMI(remote method invoke,即遠程方法調用),傳入的參數或返回的對象都是可序列化的,不然會出錯;全部須要保存到磁盤的java對象都必須是可序列化的。一般建議:程序建立的每一個JavaBean類都實現Serializeable接口。

2、序列化實現的方式

若是須要將某個對象保存到磁盤上或者經過網絡傳輸,那麼這個類應該實現Serializable接口或者Externalizable接口之一。ios

一、Serializable

1.1 普通序列化

Serializable接口是一個標記接口,不用實現任何方法。一旦實現了此接口,該類的對象就是可序列化的。程序員

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

  • 步驟二:調用ObjectOutputStream對象的writeObject輸出可序列化對象。數組

    public class Person implements Serializable {
      private String name;
      private int age;
      //我不提供無參構造器
      public Person(String name, int age) {
          this.name = name;
          this.age = age;
      }

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

    public class WriteObject {
      public static void main(String[] args) {
          try (//建立一個ObjectOutputStream輸出流
               ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) {
              //將對象序列化到文件s
              Person person = new Person("9龍"23);
              oos.writeObject(person);
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
    }
    複製代碼
  1. 反序列化步驟:
  • 步驟一:建立一個ObjectInputStream輸入流;markdown

  • 步驟二:調用ObjectInputStream對象的readObject()獲得序列化的對象。網絡

    咱們將上面序列化到person.txt的person對象反序列化回來jvm

    public class Person implements Serializable {
      private String name;
      private int age;
      //我不提供無參構造器
      public Person(String name, int age) {
          System.out.println("反序列化,你調用我了嗎?");
          this.name = name;
          this.age = age;
      }

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

    public class ReadObject {
      public static void main(String[] args) {
          try (//建立一個ObjectInputStream輸入流
               ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.txt"))) {
              Person brady = (Person) ois.readObject();
              System.out.println(brady);
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
    }
    //輸出結果
    //Person{name='9龍', age=23}
    複製代碼

    waht???? 輸出告訴咱們,反序列化並不會調用構造方法。反序列的對象是由JVM本身生成的對象,不經過構造方法生成。ide

1.2 成員是引用的序列化

若是一個可序列化的類的成員不是基本類型,也不是String類型,那這個引用類型也必須是可序列化的;不然,會致使此類不能序列化。性能

看例子,咱們新增一個Teacher類。將Person去掉實現Serializable接口代碼。

public class Person{
    //省略相關屬性與方法
}
public class Teacher implements Serializable {

    private String name;
    private Person person;

    public Teacher(String name, Person person) {
        this.name = name;
        this.person = person;
    }

     public static void main(String[] args) throws Exception {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {
            Person person = new Person("路飛"20);
            Teacher teacher = new Teacher("雷利", person);
            oos.writeObject(teacher);
        }
    }
}
複製代碼

咱們看到程序直接報錯,由於Person類的對象是不可序列化的,這致使了Teacher的對象不可序列化

1.3 同一對象序列化屢次的機制

同一對象序列化屢次,會將這個對象序列化屢次嗎?答案是否認的。

public class WriteTeacher {
    public static void main(String[] args) throws Exception {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {
            Person person = new Person("路飛"20);
            Teacher t1 = new Teacher("雷利", person);
            Teacher t2 = new Teacher("紅髮香克斯", person);
            //依次將4個對象寫入輸入流
            oos.writeObject(t1);
            oos.writeObject(t2);
            oos.writeObject(person);
            oos.writeObject(t2);
        }
    }
}
複製代碼

依次將t一、t二、person、t2對象序列化到文件teacher.txt文件中。

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

public class ReadTeacher {
    public static void main(String[] args) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("teacher.txt"))) {
            Teacher t1 = (Teacher) ois.readObject();
            Teacher t2 = (Teacher) ois.readObject();
            Person p = (Person) ois.readObject();
            Teacher t3 = (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 (Exception e) {
            e.printStackTrace();
        }
    }
}
//輸出結果
//false
//true
//true
//true
//true
複製代碼

從輸出結果能夠看出,Java序列化同一對象,並不會將此對象序列化屢次獲得多個對象。

  • Java序列化算法
  1. 全部保存到磁盤的對象都有一個序列化編碼號

  2. 當程序試圖序列化一個對象時,會先檢查此對象是否已經序列化過,只有此對象從未(在此虛擬機)被序列化過,纔會將此對象序列化爲字節序列輸出。

  3. 若是此對象已經序列化過,則直接輸出編號便可。

    圖示上述序列化過程。

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

因爲java序利化算法不會重複序列化同一個對象,只會記錄已序列化對象的編號。若是序列化一個可變對象(對象內的內容可更改)後,更改了對象內容,再次序列化,並不會再次將此對象轉換爲字節序列,而只是保存序列化編號。

public class WriteObject {
    public static void main(String[] args) {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
             ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) {
            //第一次序列化person
            Person person = new Person("9龍"23);
            oos.writeObject(person);
            System.out.println(person);

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

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

    public class Person implements Serializable {
       //不須要序列化名字與年齡
       private transient String name;
       private transient int age;
       private int height;
       private transient boolean singlehood;
       public Person(String name, int age) {
           this.name = name;
           this.age = age;
       }
       //省略get,set方法
    }

    public class TransientTest {
       public static void main(String[] args) throws Exception {
           try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
                ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) {
               Person person = new Person("9龍"23);
               person.setHeight(185);
               System.out.println(person);
               oos.writeObject(person);
               Person p1 = (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提供了可選的自定義序列化。能夠進行控制序列化的方式,或者對序列化數據進行編碼加密等。

    private void writeObject(java.io.ObjectOutputStream out) throws IOException;
    private void readObject(java.io.ObjectIutputStream in) throws IOException,ClassNotFoundException
    ;
    private void readObjectNoData() throws ObjectStreamException;
    複製代碼

    經過重寫writeObject與readObject方法,能夠本身選擇哪些屬性須要序列化, 哪些屬性不須要。若是writeObject使用某種規則序列化,則相應的readObject須要相反的規則反序列化,以便能正確反序列化出對象。這裏展現對名字進行反轉加密。

    public class Person implements Serializable {
       private String name;
       private int age;
       //省略構造方法,get及set方法

       private void writeObject(ObjectOutputStream out) throws IOException {
           //將名字反轉寫入二進制流
           out.writeObject(new StringBuffer(this.name).reverse());
           out.writeInt(age);
       }

       private void readObject(ObjectInputStream ins) throws IOException,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方法。此方法可將任意對象代替目標序列化對象

      public class Person implements Serializable {
        private String name;
        private int age;
        //省略構造方法,get及set方法

        private Object writeReplace() throws ObjectStreamException {
            ArrayList<Object> list = new ArrayList<>(2);
            list.add(this.name);
            list.add(this.age);
            return list;
        }

         public static void main(String[] args) throws Exception {
            try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
                 ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) {
                Person person = new Person("9龍"23);
                oos.writeObject(person);
                ArrayList list = (ArrayList)ios.readObject();
                System.out.println(list);
            }
        }
      }
      //輸出結果
      //[9龍, 23]
      複製代碼
    • readResolve:反序列化時替換反序列化出的對象,反序列化出來的對象被當即丟棄。此方法在readeObject後調用。

      public class Person implements Serializable {
          private String name;
          private int age;
          //省略構造方法,get及set方法
           private Object readResolve() throws ObjectStreamException{
              return new ("brady"23);
          }
          public static void main(String[] args) throws Exception {
              try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
                   ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) {
                  Person person = new Person("9龍"23);
                  oos.writeObject(person);
                  HashMap map = (HashMap)ios.readObject();
                  System.out.println(map);
              }
          }
      }
      //輸出結果
      //{brady=23}
      複製代碼

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

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

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

經過實現Externalizable接口,必須實現writeExternal、readExternal方法。

public interface Externalizable extends java.io.Serializable {
     void writeExternal(ObjectOutput out) throws IOException;
     void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
複製代碼
public class ExPerson implements Externalizable {

    private String name;
    private int age;
    //注意,必須加上pulic 無參構造器
    public ExPerson() {
    }

    public ExPerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        //將name反轉後寫入二進制流
        StringBuffer reverse = new StringBuffer(name).reverse();
        System.out.println(reverse.toString());
        out.writeObject(reverse);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        //將讀取的字符串反轉後賦值給name實例變量
        this.name = ((StringBuffer) in.readObject()).reverse().toString();
        System.out.println(name);
        this.age = in.readInt();
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ExPerson.txt"));
             ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ExPerson.txt"))) {
            oos.writeObject(new ExPerson("brady"23));
            ExPerson ep = (ExPerson) ois.readObject();
            System.out.println(ep);
        }
    }
}
//輸出結果
//ydarb
//brady
//ExPerson{name='brady', age=23}
複製代碼

注意:Externalizable接口不一樣於Serializable接口,實現此接口必須實現接口中的兩個方法實現自定義序列化,這是強制性的;特別之處是必須提供pulic的無參構造器,由於在反序列化的時候須要反射建立對象。

三、兩種序列化對比

實現Serializable接口 實現Externalizable接口
系統自動存儲必要的信息 程序員決定存儲哪些信息
Java內建支持,易於實現,只須要實現該接口便可,無需任何代碼支持 必須實現接口內的兩個方法
性能略差 性能略好

雖然Externalizable接口帶來了必定的性能提高,但變成複雜度也提升了,因此通常經過實現Serializable接口進行序列化。

3、序列化版本號serialVersionUID

咱們知道,反序列化必須擁有class文件,但隨着項目的升級,class文件也會升級,序列化怎麼保證升級先後的兼容性呢?

java序列化提供了一個private static final long serialVersionUID 的序列化版本號,只有版本號相同,即便更改了序列化屬性,對象也能夠正確被反序列化回來。

public class Person implements Serializable {
    //序列化版本號
    private static final long serialVersionUID = 1111013L;
    private String name;
    private int age;
    //省略構造方法及get,set
}
複製代碼

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

序列化版本號可自由指定,若是不指定,JVM會根據類信息本身計算一個版本號,這樣隨着class的升級,就沒法正確反序列化;不指定版本號另外一個明顯隱患是,不利於jvm間的移植,可能class文件沒有更改,但不一樣jvm可能計算的規則不同,這樣也會致使沒法反序列化。

什麼狀況下須要修改serialVersionUID呢?分三種狀況。

  • 若是隻是修改了方法,反序列化不容影響,則無需修改版本號;
  • 若是隻是修改了靜態變量,瞬態變量(transient修飾的變量),反序列化不受影響,無需修改版本號;
  • 若是修改了非瞬態變量,則可能致使反序列化失敗。若是新類中實例變量的類型與序列化時類的類型不一致,則會反序列化失敗,這時候須要更改serialVersionUID。若是隻是新增了實例變量,則反序列化回來新增的是默認值;若是減小了實例變量,反序列化時會忽略掉減小的實例變量。

4、總結

  1. 全部須要網絡傳輸的對象都須要實現序列化接口,經過建議全部的javaBean都實現Serializable接口。
  2. 對象的類名、實例變量(包括基本類型,數組,對其餘對象的引用)都會被序列化;方法、類變量、transient實例變量都不會被序列化。
  3. 若是想讓某個變量不被序列化,使用transient修飾。
  4. 序列化對象的引用類型成員變量,也必須是可序列化的,不然,會報錯。
  5. 反序列化時必須有序列化對象的class文件。
  6. 當經過文件、網絡來讀取序列化後的對象時,必須按照實際寫入的順序讀取。
  7. 單例類序列化,須要重寫readResolve()方法;不然會破壞單例原則。
  8. 同一對象序列化屢次,只有第一次序列化爲二進制流,之後都只是保存序列化編號,不會重複序列化。
  9. 建議全部可序列化的類加上serialVersionUID 版本號,方便項目升級。
相關文章
相關標籤/搜索