面試官:說說你對序列化的理解

本文主要內容java

5d1707fc4b49a48d047d8e049ada0ab4.jpg

背景

在Java語言中,程序運行的時候,會產生不少對象,而對象信息也只是在程序運行的時候纔在內存中保持其狀態,一旦程序中止,內存釋放,對象也就不存在了。算法

怎麼能讓對象永久的保存下來呢?--------對象序列化json

何爲序列化和反序列化?數組

  • 序列化:對象到IO數據流

    e23940375dcf62e32f64bb3d574fd2b7.jpg

  • 反序列化:IO數據流到對象

    da8a5120939780ed2f6391cf77360f65.jpg

有哪些使用場景?

Java平臺容許咱們在內存中建立可複用的Java對象,但通常狀況下,只有當JVM處於運行時,這些對象纔可能存在,即,這些對象的生命週期不會比JVM的生命週期更長。但在現實應用中,就可能要求在JVM中止運行以後可以保存(持久化)指定的對象,並在未來從新讀取被保存的對象。Java對象序列化就可以幫助咱們實現該功能。安全

使用Java對象序列化,在保存對象時,會把其狀態保存爲一組字節,在將來,再將這些字節組裝成對象。必須注意地是,對象序列化保存的是對象的"狀態",即它的成員變量。由此可知,對象序列化不會關注類中的靜態變量。微信

除了在持久化對象時會用到對象序列化以外,當使用RMI(遠程方法調用),或在網絡中傳遞對象時,都會用到對象序列化。網絡

Java序列化API爲處理對象序列化提供了一個標準機制,該API簡單易用。併發

不少框架中都有用到,好比典型的dubbo框架中使用了序列化。框架

序列化有什麼做用?

序列化機制容許將實現序列化的Java對象轉換位字節序列,這些字節序列能夠保存在磁盤上,或經過網絡傳輸,以達到之後恢復成原來的對象。序列化機制使得對象能夠脫離程序的運行而獨立存在。dom

序列化實現方式

Java語言中,常見實現序列化的方式有兩種:

  • 實現Serializable接口
  • 實現Externalizable接口

下面咱們就來詳細的說說這兩種實現方式。

實現Serializable接口

建立一個User類實現Serializable接口 ,實現序列化,大體步驟爲:

  1. 對象實體類實現Serializable 標記接口。
  2. 建立序列化輸出流對象ObjectOutputStream,該對象的建立依賴於其它輸出流對象,一般咱們將對象序列化爲文件存儲,因此這裏用文件相關的輸出流對象 FileOutputStream。
  3. 經過ObjectOutputStream 的 writeObject()方法將對象序列化爲文件。
  4. 關閉流。

如下就是code:

    package com.tian.my_code.test.clone;
    
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.ObjectOutputStream;
    import java.io.Serializable;
    
    public class User implements Serializable {
        private int age;
        private String name;
    
        public User() {
        }
    
        public User(int age, String name) {
            this.age = age;
            this.name = name;
        }
        //set get省略
        public static void main(String[] args) {
            try {
                ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("user.txt"));
                User user=new User(22,"老田");
                objectOutputStream.writeObject(user);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

建立一個User對象,而後把User對象保存的user.txt中了。

反序列化

大體有如下三個步驟:

  1. 建立輸入流對象ObjectOutputStream。一樣依賴於其它輸入流對象,這裏是文件輸入流 FileInputStream。
  2. 經過 ObjectInputStream 的 readObject()方法,將文件中的對象讀取到內存。
  3. 關閉流。

下面咱們再進行反序列化code:

    package com.tian.my_code.test.clone;
    
    import java.io.*;
    
    public class SeriTest {
        public static void main(String[] args) {
            try {
                ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.txt"));
                User user=(User) ois.readObject();
                System.out.println(user.getName());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

運行這段代碼,輸出結果:

1637615e1614b519a5ee8ba65298b993.jpg

使用IDEA打開user.tst文件:

aa7a95953a65929e156e80c205129d15.jpg

使用編輯器16機制查看

3b7ed478e939ea13f995c60975b79be6.jpg

關於文件內容我們就不用太關心了,繼續說咱們的重點。

序列化是把User對象存放到文件裏了,而後反序列化就是讀取文件內容並建立對象。

A端把對象User保存到文件user.txt中,B端就能夠經過網絡或者其餘方式讀取到這個文件,再進行反序列化,得到A端建立的User對象。

拓展

若是B端拿到的User屬性若是有變化呢?好比說:增長一個字段

    private String address;

再次進行反序列化就會報錯

6d9f4edbe5dcf7b61f8fbcdb0b174dd6.jpg

添加serialVersionUID

    package com.tian.my_code.test.clone;
    
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.ObjectOutputStream;
    import java.io.Serializable;
    
    public class User implements Serializable{
        private static final long serialVersionUID = 2012965743695714769L;
        private int age;
        private String name;
    
        public User() {
        }
    
        public User(int age, String name) {
            this.age = age;
            this.name = name;
        }
    
        // set get   省略
    
        public static void main(String[] args) {
            try {
                ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("user.txt"));
                User user=new User(22,"老田");
                objectOutputStream.writeObject(user);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

再次執行反序列化,運行結果正常

82b32e287f07c3cd9d3e9d951b48dc6d.jpg

而後咱們再次加上字段和對應的get/set方法

    private String address;

再次執行反序列化

69393f06c750ad985cf86201fcfe8f9d.jpg

反序列化成功。

若是可序列化類未顯式聲明 serialVersionUID,則序列化運行時將基於該類的各個方面計算該類的默認 serialVersionUID 值,如「Java(TM) 對象序列化規範」中所述。

不過,強烈建議 全部可序列化類都顯式聲明 serialVersionUID 值,緣由是計算默認的 serialVersionUID對類的詳細信息具備較高的敏感性,根據編譯器實現的不一樣可能千差萬別,這樣在反序列化過程當中可能會致使意外的 InvalidClassException。

所以,爲保證 serialVersionUID值跨不一樣 Java 編譯器實現的一致性,序列化類必須聲明一個明確的 serialVersionUID值。

強烈建議使用 private 修飾符顯示聲明 serialVersionUID(若是可能),緣由是這種聲明僅應用於直接聲明類 -- serialVersionUID字段做爲繼承成員沒有用處。數組類不能聲明一個明確的 serialVersionUID,所以它們老是具備默認的計算值,可是數組類沒有匹配 serialVersionUID值的要求。

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

IDEA中如何快捷添加serialVersionUID?

ab948162b6251d0a68aca6fc22135dad.jpg

咱們的類實現Serializable接口,鼠標放在類上,Alt+Enter鍵就能夠添加了。

實現Externalizable接口

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

07356b54a9af8615c4e3268e3fc9db1e.jpg

cf248a3f8aec6d7e7cfe76caf8dbba00.jpg

@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    
}

Externalizable是Serializable的子接口。

public interface Externalizable extends java.io.Serializable {

繼續使用前面的User,代碼進行改造:

    package com.tian.my_code.test.clone;
    
    import java.io.*;
    
    public class User implements Externalizable {
        private int age;
        private String name;
    
        public User() {
        }
    
        public User(int age, String name) {
            this.age = age;
            this.name = name;
        }
    
        //set get
    
    
        public static void main(String[] args) {
            try {
                ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("user.txt"));
                User user = new User(22"老田");
                objectOutputStream.writeObject(user);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public void writeExternal(ObjectOutput out) throws IOException {
            //將name反轉後寫入二進制流
            StringBuffer reverse = new StringBuffer(name).reverse();
            out.writeObject(reverse);
            out.writeInt(age);
        }
    
        @Override
        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            //將讀取的字符串反轉後賦值給name實例變量
            this.name = ((StringBuffer) in.readObject()).reverse().toString();
            //將讀取到的int類型值付給age
            this.age = in.readInt();
        }
    }
    

執行序列化,而後再次執行反序列化,輸出:

85f6861bee6f1f599148e92c8c607162.jpg

注意

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

兩種方式對比

下圖爲兩種實現方式的對比:

0a33d7d44b74b2c70ea05565e58b917c.jpg

序列化只有兩種方式嗎?

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

序列化算法

  • 全部保存到磁盤的對象都有一個序列化編碼號。
  • 當程序試圖序列化一個對象時,會先檢查此對象是否已經序列化過,只有此對象從未(在此虛擬機)被序列化過,纔會將此對象序列化爲字節序列輸出。
  • 若是此對象已經序列化過,則直接輸出編號便可。

自定義序列化

有些時候,咱們有這樣的需求,某些屬性不須要序列化。使用transient關鍵字選擇不須要序列化的字段。

繼續使用前面的代碼進行改造,在age字段上添加transient修飾:

    package com.tian.my_code.test.clone;
    
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.ObjectOutputStream;
    import java.io.Serializable;
    
    public class User implements Serializable{
        private transient int age;
        private String name;
    
    
        public User() {
        }
    
        public User(int age, String name) {
            this.age = age;
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public static void main(String[] args) {
            try {
                ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("user.txt"));
                User user=new User(22,"老田");
                objectOutputStream.writeObject(user);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    ```

序列化,而後進行反序列化:
```java
    package com.tian.my_code.test.clone;
    
    import java.io.*;
    
    public class SeriTest {
        public static void main(String[] args) {
            try {
                ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.txt"));
                User user=(User) ois.readObject();
                System.out.println(user.getName());
                System.out.println(user.getAge());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

運行輸出:

241063a260371e7b5d47debd92b3679c.jpg

從輸出咱們看到,使用transient修飾的屬性,Java序列化時,會忽略掉此字段,因此反序列化出的對象,被transient修飾的屬性是默認值。

對於引用類型,值是null;基本類型,值是0;boolean類型,值是false。

探索

到此序列化內容算講完了,可是,若是隻停留在這個層面,是沒法應對實際工做中的問題的。

好比模型對象持有其它對象的引用怎麼處理,引用類型若是是複雜些的集合類型怎麼處理?

上面的User中持有String引用類型的,照樣序列化沒問題,那麼若是是咱們自定義的引用類呢?

好比下面的場景:

    package com.tian.my_code.test.clone;
    
    public class UserAddress {
        private int provinceCode;
        private int cityCode;
    
        public UserAddress() {
        }
    
        public UserAddress(int provinceCode, int cityCode) {
            this.provinceCode = provinceCode;
            this.cityCode = cityCode;
        }
    
        public int getProvinceCode() {
            return provinceCode;
        }
    
        public void setProvinceCode(int provinceCode) {
            this.provinceCode = provinceCode;
        }
    
        public int getCityCode() {
            return cityCode;
        }
    
        public void setCityCode(int cityCode) {
            this.cityCode = cityCode;
        }
    }

而後在User中添加一個UserAddress的屬性:

    package com.tian.my_code.test.clone;
    
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.ObjectOutputStream;
    import java.io.Serializable;
    
    public class User implements Serializable{
        private static final long serialVersionUID = -2445226500651941044L;
        private int age;
        private String name;
        private UserAddress userAddress;
    
        public User() {
        }
    
        public User(int age, String name) {
            this.age = age;
            this.name = name;
        }
        //get set
        
        public static void main(String[] args) {
            try {
                ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("user.txt"));
                User user=new User(22,"老田");
                UserAddress userAddress=new UserAddress(10001,10001001);
                user.setUserAddress(userAddress);
                objectOutputStream.writeObject(user);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

運行上面代碼:

ff3eb7b7bbdcbf57e00f07f445f4e2d9.jpg

拋出了 java.io.NotSerializableException 異常。很明顯在告訴咱們,UserAddress沒有實現序列化接口。待UserAddress類實現序列化接口後:

    package com.tian.my_code.test.clone;
    
    import java.io.Serializable;
    
    public class UserAddress implements Serializable {
        private static final long serialVersionUID = 5128703296815173156L;
        private int provinceCode;
        private int cityCode;
    
        public UserAddress() {
        }
    
        public UserAddress(int provinceCode, int cityCode) {
            this.provinceCode = provinceCode;
            this.cityCode = cityCode;
        }
        //get set
    }

再次運行,正常不報錯了。

反序列化代碼:

    package com.tian.my_code.test.clone;
    
    import java.io.*;
    
    public class SeriTest {
        public static void main(String[] args) {
            try {
                ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.txt"));
                User user=(User) ois.readObject();
                System.out.println(user.getName());
                System.out.println(user.getAge());
                System.out.println(user.getUserAddress().getProvinceCode());
                System.out.println(user.getUserAddress().getCityCode());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

運行結果:

af15153b46be0d93b1f93af865da3e8d.jpg

典型運用場景

    public final class String implements java.io.SerializableComparable<String>, CharSequence {
        private static final long serialVersionUID = -6849794470754667710L;
    }
    public class HashMap<K,Vextends AbstractMap<K,V>  implements Map<K,V>, CloneableSerializable {
        private static final long serialVersionUID = 362498820763181265L;
    }
    public class ArrayList<Eextends AbstractList<E>  implements List<E>, RandomAccessCloneablejava.io.Serializable{
        private static final long serialVersionUID = 8683452581122892189L;
    }
    .....

不少經常使用類都實現了序列化接口。

再次拓展

上面說的transient 反序列化的時候是默認值,可是你會發現,幾種經常使用集合類ArrayList、HashMap、LinkedList等數據存儲字段,居然都被 transient  修飾了,然而在實際操做中咱們用集合類型存儲的數據卻能夠被正常的序列化和反序列化?

cd811e29b606d965cbf08924442fdec8.jpg

真至關然仍是在源碼裏。實際上,各個集合類型對於序列化和反序列化是有單獨的實現的,並無採用虛擬機默認的方式。這裏以 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[].classcapacity);
                //檢測是否須要對數組擴容操做
                ensureCapacityInternal(size);
                Object[] a = elementData;
                // 按順序反序列化數組中的值
                for (int i=0; i<size; i++) {
                    a[i] = s.readObject();
                }
            }
        }

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

原來,咱們以前自覺得集合能成功序列化也只是簡單的實現了標記接口都只是表象,表象背後有各個集合類有不一樣的深意。因此,一樣的思路,讀者朋友能夠本身去分析下 HashMap以及其它集合類中自行控制序列化和反序列化的箇中門道了,感興趣的小夥伴能夠自行去查看一番。

序列化注意事項

一、序列化時,只對對象的狀態進行保存,而無論對象的方法;

二、當一個父類實現序列化,子類自動實現序列化,不須要顯式實現Serializable接口;

三、當一個對象的實例變量引用其餘對象,序列化該對象時也把引用對象進行序列化;

四、並不是全部的對象均可以序列化,至於爲何不能夠,有不少緣由了,好比:

  • 安全方面的緣由,好比一個對象擁有private,public等field,對於一個要傳輸的對象,好比寫到文件,或者進行RMI傳輸等等,在序列化進行傳輸的過程當中,這個對象的private等域是不受保護的;
  • 資源分配方面的緣由,好比socket,thread類,若是能夠序列化,進行傳輸或者保存,也沒法對他們進行從新的資源分配,並且,也是沒有必要這樣實現;

五、聲明爲static和transient類型的成員數據不能被序列化。由於static表明類的狀態,transient表明對象的臨時數據。

六、序列化運行時使用一個稱爲 serialVersionUID 的版本號與每一個可序列化類相關聯,該序列號在反序列化過程當中用於驗證序列化對象的發送者和接收者是否爲該對象加載了與序列化兼容的類。爲它賦予明確的值。顯式地定義serialVersionUID有兩種用途:

  • 在某些場合,但願類的不一樣版本對序列化兼容,所以須要確保類的不一樣版本具備相同的serialVersionUID;
  • 在某些場合,不但願類的不一樣版本對序列化兼容,所以須要確保類的不一樣版本具備不一樣的serialVersionUID。

七、Java有不少基礎類已經實現了serializable接口,好比String,Vector等。可是也有一些沒有實現serializable接口的;

八、若是一個對象的成員變量是一個對象,那麼這個對象的數據成員也會被保存!這是能用序列化解決深拷貝的重要緣由;

總結

什麼是序列化?序列化Java中經常使用實現方式有哪些?兩種實現序列化方式的對比,序列化算法?如何自定義序列化?Java集合框架中序列化是如何實現的?

這幾個點若是沒有get到,麻煩請再次閱讀,或者加我微信進羣裏你們一塊兒聊。

相關文章
相關標籤/搜索