Java序列化和反序列化,你該知道得更多

  序列化 (Serialization)是將對象的狀態信息轉換爲能夠存儲或傳輸的形式的過程。在序列化期間,對象將其當前狀態寫入到臨時或持久性存儲區。之後,能夠經過從存儲區中讀取或反序列化對象的狀態,從新建立該對象——百度詞條解釋。html

  通俗點的來講,程序運行的時候,會產生不少對象,而對象信息也只是在程序運行的時候纔在內存中保持其狀態,一旦程序中止,內存釋放,對象也就不存在了。怎麼能讓對象永久的保存下來呢?對象序列化,瞭解下——java

一    入門

  在Java的 I/O 類庫中,專門給開發人員提供了兩個類用於對象的序列化和反序列化操做的流類 ObjectOutputStream 和 ObjectInputStream。有了這兩個類的幫助,再依照流的操做步驟一步兩步,簡單的對象的序列化和反序列化就真的很簡單。代碼示例:數據庫

User類:json

public class User implements Serializable {

    private static final long serialVersionUID = -1075318199295234057L;

    //時間標示
    private Date date = new Date();

    private String name;

    private  String password;

    private int age;

    public User() {
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    @Override
    public String toString() {
        return "User{" +
                "序列化存儲時間:" + date +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                ", age=" + age +
                '}';
    }
}
View Code

測試類:數組

//序列化和反序列化
public class SerialTest {
    public static void main(String[] args) throws InterruptedException {

        /**
         * 基本步驟:
         * ① 對象實體類實現Serializable 標記接口
         * ② 建立序列化輸出流對象ObjectOutputStream,該對象的建立依賴於其它輸出流對象,一般咱們將對象序列化爲文件存儲,因此這裏用文件相關的輸出流對象 FileOutputStream
         * ③ 經過ObjectOutputStream 的 writeObject()方法將對象序列化爲文件
         * ④ 關閉流 這裏採用1.7開始的新語法  try-with-resources  而不用本身控制流的關閉
         */
        User user = new User("陳本布衣", "123456", 100);
        try (ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("D:\\user"))) {
            os.writeObject(user);
        } catch (Exception e) {
            e.printStackTrace();
        }

        //先睡5秒
        TimeUnit.SECONDS.sleep(5);

        /**
         * 基本步驟:
         * ① 建立輸入流對象ObjectOutputStream。一樣依賴於其它輸入流對象,這裏是文件輸入流 FileInputStream
         * ② 經過 ObjectInputStream 的 readObject()方法,將文件中的對象讀取到內存
         * ③ 關閉流 同上
         */
        try (ObjectInputStream is = new ObjectInputStream(new FileInputStream("D:\\user"))) {
            User o = (User) is.readObject();
            System.out.println(o);
            System.out.println("當前時間:"+new Date());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

 

  最終你在控制檯上看到應該是符合預期的效果:tomcat

  咱們看到,密碼這樣的敏感信息也被序列化了,反序列化後這種敏感信息就有暴露的風險,而一般敏感信息咱們是不但願保留的,怎麼辦呢,很簡單,給不但願序列化的字段添加 transient 標識,就像這樣: private transient String password; 該字段在序列化時就會被忽略,壞人就看不見敏感信息啦——session

 

二  進階

   以上只是很簡單的入門示例,實際開發中咱們還要面對不少複雜的業務場景。好比模型對象持有其它對象的引用怎麼處理,引用類型若是是複雜些的集合類型怎麼處理?進階的部分,一塊兒來探索一下。併發

  關於第一個問題,其實仔細分析上面的基礎示例已經很明顯了,咱們User類中原本就持有Date,String類的引用,不是同樣的被序列化和反序列化了嗎?若是是咱們本身定義的類,是否是同樣的效果呢?給用戶添加菜單(Menu)來嘗試一下socket

public class Menu {
    private Integer id;
    private String name;
    private String url;

    public Menu() {
    }

    public Menu(Integer id,String name, String url) {
        this.id = id;
        this.name = name;
        this.url = url;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    @Override
    public String toString() {
        return "Menu{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", url='" + url + '\'' +
                '}';
    }
}
View Code
public class User implements Serializable {

    private static final long serialVersionUID = -1075318199295234057L;

    //時間標示
    private Date date = new Date();

    private String name;

    private Menu menu;

    private transient String password;

    private int age;

    public User() {
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public Menu getMenu() {
        return menu;
    }

    public void setMenu(Menu menu) {
        this.menu = menu;
    }


    @Override
    public String toString() {
        return "User{" +
                "序列化存儲時間:" + date +
                ", name='" + name + '\'' +
                ", 菜單:" + menu +
                ", password='" + password + '\'' +
                ", age=" + age +
                '}';
    }
}
View Code

測試代碼:ide

public class SerialTest {
    public static void main(String[] args) throws InterruptedException {
        //序列化
        User user = new User("陳本布衣", "123456", 100);
        Menu menu = new Menu(1,"菜單1","/menu1");
        user.setMenu(menu);
        try (ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("D:\\user"))) {
            os.writeObject(user);
        } catch (Exception e) {
            e.printStackTrace();
        }

        //先睡5秒
        TimeUnit.SECONDS.sleep(5);

        //反序列化
        try (ObjectInputStream is = new ObjectInputStream(new FileInputStream("D:\\user"))) {
            User o = (User) is.readObject();
            System.out.println(o);
            System.out.println("當前時間:"+new Date());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

  測試結果,拋出了 java.io.NotSerializableException 異常。很明顯在告訴咱們,Menu沒有實現序列化接口。待Menu類實現序列化接口後,成功——

  這樣的測試很容易讓咱們觸類旁通,既然序列化必需要實現標記接口 Serializable,那是否是意味着,咱們以前能序列化成功,String、Date等類都實現了該接口呢?很明顯,是的,源碼會給你佐證——

  繼續反三,若是要序列化待集合類型的數據,咱們的集合類型又是否是都實現了序列化接口呢?查看便知——

  以上潦草的貼圖充分的說明了觸類旁通的重要性,咱們能夠清晰的看到,咱們能想到的經常使用集合類型都實現了 Serializable 接口,因而關於帶集合類型的實體類的序列化和反序列化,彷佛也很簡單明瞭。先來將實體中的菜單改成集合形式:  private List<Menu> menus; 而後進行測試——

public class SerialTest {
    public static void main(String[] args) throws InterruptedException {
        //序列化
        User user = new User("陳本布衣", "123456", 100);
        Menu menu = new Menu(1, "菜單1", "/menu1");
        Menu menu2 = new Menu(2, "菜單2", "/menu2");
        List<Menu> menus = new ArrayList<>();
        menus.add(menu);
        menus.add(menu2);
        user.setMenus(menus);
        try (ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("D:\\user"))) {
            os.writeObject(user);
        } catch (Exception e) {
            e.printStackTrace();
        }

        //先睡5秒
        TimeUnit.SECONDS.sleep(5);

        //反序列化
        try (ObjectInputStream is = new ObjectInputStream(new FileInputStream("D:\\user"))) {
            User o = (User) is.readObject();
            System.out.println(o);
            System.out.println("當前時間:" + new Date());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

  結果也很符合預期——

三  顛覆

   博主也是剛剛發現本身被騙了,真的,不騙你!

  上面部分博主說到,各類集合類由於實現了 Serializable  標記接口,因此序列化的時候也不用特殊對待,按照基本步驟就能成功的實現序列化和反序列化;入門的時候博主還說道,若是不想某個字段被序列化,就用 transient  修飾一下,嗯,說的都頗有道理,可是若是你有翻看源碼的良好習慣的話,對於集合類的源碼固然不會陌生。上面貼圖,只是說它們都實現了標記接口,可是它們的存儲數據的字段是下面這樣的:

  

  你會發現,幾種經常使用集合類的數據存儲字段,居然都被 transient  修飾了,然而在實際操做中咱們用集合類型存儲的數據卻能夠被正常的序列化和反序列化?WHAT,這不是啪啪打臉博主的嗎?理論崩塌了,真相在哪裏?真至關然仍是在源碼裏。實際上,各個集合類型對於序列化和反序列化是有單獨的實現的,並無採用虛擬機默認的方式。這裏以 ArrayList中的序列化和反序列化源碼部分爲例分析:

private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
        int expectedModCount = modCount;
        //序列化當前ArrayList中非transient以及非靜態字段
        s.defaultWriteObject();
        //序列化數組實際個數
        s.writeInt(size);
        // 逐個取出數組中的值進行序列化
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }
        //防止在併發的狀況下對元素的修改
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

    private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;
        // 反序列化非transient以及非靜態修飾的字段,其中包含序列化時的數組大小 size
        s.defaultReadObject();
        // 忽略的操做
        s.readInt(); // ignored
        if (size > 0) {
            // 容量計算
            int capacity = calculateCapacity(elementData, size);
            SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
            //檢測是否須要對數組擴容操做
            ensureCapacityInternal(size);
            Object[] a = elementData;
            // 按順序反序列化數組中的值
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }

  讀源碼能夠知道,ArrayList的序列化和反序列化主要思路就是根據集合中實際存儲的元素個數來進行操做,這樣作估計是爲了不沒必要要的空間浪費(由於ArrayList的擴容機制決定了,集合中實際存儲的元素個數確定比集合的可容量要小)。爲了驗證,咱們能夠在單元測試序列化和返序列化的時候,在ArrayLIst的兩個方法中打上斷點,以確認這兩個方法在序列化和返序列化的執行流程中(截圖爲反序列化過程):

 

  原來,咱們以前自覺得集合能成功序列化也只是簡單的實現了標記接口都只是表象,表象背後有各個集合類有不一樣的深意。因此,一樣的思路,讀者朋友能夠本身去分析下 HashMap以及其它集合類中自行控制序列化和反序列化的箇中門道了,博主就不幫你們分析源碼了(zhuang bi hao lei  ^ ^)。

四    發散

  行文至此,豁然開朗的趕腳有木有?可是,你覺得這樣就完了嗎?不不,如下才是真正的高潮呢。學習的過程當中,若是你的思惟夠發散的話,根據源碼,依樣畫葫蘆,其實能夠學到不少東西的。上面,咱們已經分析了集合中序列化和反序列化的兩個方法,而後在查閱各個集合類源碼中的序列化和反序列化方法的時候,只因多看了一眼,博主驚訝的發現,它們的方法簽名都是相同的。這說明什麼?很蹊蹺啊各位。一樣都是實現了序列化標記接口,那麼,我是否是能夠在本身的實體類中一樣的聲明這兩個方法呢?結果很nice,固然是能夠的(前提是要實現序列化接口),可是這會致使默認的序列化失效,同集合中同樣,當你單獨聲明瞭 writeObject 和 readObject 方法以後,至關於覆蓋了默認的序列化方式——

  以上,咱們成功的自定義了序列化實現,但這徹底不影響上層序列化的代碼編寫,你只是更改了默認實現而已。最後,你將很驚喜的在JDK文檔關於Serializable的描述中,找到以前你可能沒啥感受但如今卻體會至深的話:

在序列化和反序列化過程當中須要特殊處理的類必須使用下列準確簽名來實現特殊方法: 

 private void writeObject(java.io.ObjectOutputStream out) throws IOException
 private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
 private void readObjectNoData()  throws ObjectStreamException;
 

  更加豁然開朗了,有木有?表面上看,Serializable只是個看似啥都沒有的空標接口,可是接口背後,虛擬機作了什麼,你未必都看得見。其實,若是要自定義實現的話,咱們還能夠實現 Serializable 的子接口 Externalizable,重寫其中的方法,實現自定義邏輯,不過,用以上的方式,足夠你玩的了。 好了,序列化和和反序列化的問題,就此打住。

五  問答

  ① 實現標記接口後,其中的 serialVersionUID 必需要指定嗎?官方文檔有以下表述:

若是可序列化類未顯式聲明 serialVersionUID,則序列化運行時將基於該類的各個方面計算該類的默認 serialVersionUID 值,如「Java(TM) 對象序列化規範」中所述。
不過,強烈建議 全部可序列化類都顯式聲明 serialVersionUID 值,緣由是計算默認的 serialVersionUID 對類的詳細信息具備較高的敏感性,根據編譯器實現的不一樣可能千差萬別,這樣在反序列化過程當中可能會致使意外的 InvalidClassException。
所以,爲保證 serialVersionUID 值跨不一樣 java 編譯器實現的一致性,序列化類必須聲明一個明確的 serialVersionUID 值。還強烈建議使用 private 修飾符顯示聲明 serialVersionUID(若是可能),緣由是這種聲明僅應用於直接聲明類
-- serialVersionUID 字段做爲繼承成員沒有用處。數組類不能聲明一個明確的 serialVersionUID,所以它們老是具備默認的計算值,可是數組類沒有匹配 serialVersionUID 值的要求。

  因此,儘可能顯示的聲明,這樣序列化的類即便有字段的修改,由於 serialVersionUID 的存在,也能保證反序列化成功。

     ② 難道序列化只有上面的方式?

  固然不是。根據序列化的定義,無論經過什麼方式,只要你能把內存中的對象轉換成能存儲或傳輸的方式,又能反過來恢復它,其實均可以稱爲序列化。所以,咱們經常使用的 Fastjson、Jackson等第三方類庫將對象轉成Json格式文件,也能夠算是一種序列化,用JAXB實現XML格式文件輸出,也能夠算是序列化。因此,千萬不要被思惟侷限,其實現實當中咱們進行了不少序列化和反序列化的操做,涉及不一樣的形態、數據格式等。

  ③ 說一兩個實際場景呢?

  最典型的,在Tom貓中,tomcat服務正常關閉會把session對象序列化到SESSIONS.ser文件中,等下次啓動的時候再把這些session再加載到內存;Socket套接字通訊中,將對象在客戶端和服務端之間傳輸。示例代碼:

public class SocketClient {

    public static void main(String[] args) {
        System.out.println("Socket 客戶端");
        Socket client = null;
        try {
            // 與服務端創建鏈接
            client = new Socket("127.0.0.1", 9527);
            ObjectOutputStream os = new ObjectOutputStream(client.getOutputStream());
            User user = new User("陳本布衣", "123456", 100);
            Menu menu = new Menu(1, "菜單1", "/menu1");
            Menu menu2 = new Menu(2, "菜單2", "/menu2");
            List<Menu> menus = new ArrayList<>();
            menus.add(menu);
            menus.add(menu2);
            user.setMenus(menus);
            // 往服務寫數據
            os.writeObject(user);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
/*****************************************/ public class SocketServer { public static void main(String[] args) { System.out.println("Socket 服務端"); ServerSocket server; try { //服務端監聽端口 server = new ServerSocket(9527); Socket socket = server.accept(); ObjectInputStream is = new ObjectInputStream(socket.getInputStream()); Object o = is.readObject(); System.out.println("傳過來的內容,請收下:"+o); } catch (Exception e) { e.printStackTrace(); } } }

  ④  對象序列化中的持久存儲,和將對象數據保存到數據庫的持久化不是同樣的嗎?

  這其實仍是要區別看待的。由於咱們保存數據庫的方式叫對象(關係)映射,重點在於映射兩個字,也就是說只是將我內存對象和真實的數據庫數據表中的數據進行了映射綁定,並非直接將對象存進了數據庫。

  ⑤ 對象發序列話後,和原來的對象是同一個對象嗎?

  序列化只是對原對象的一個拷貝,保持了原對象各個字段的狀態值,但確定不是同一個對象了。你想,你把對象序列化出去,N久了,你虛擬機都關十次八次了,兩個對象怎麼可能相同?

  ⑥ 你很帥是嗎?

  是。

相關文章
相關標籤/搜索