1. 程式人生 > 其它 >Android序列化(1)Serializable

Android序列化(1)Serializable

概念

說到Java,萬物皆物件。物件,是一個比較抽象的概念,他就是類存活在記憶體中的一個例項,有狀態和行為,一旦JVM停止執行,物件的狀態也會隨之丟失。那麼如何將這個物件當前狀態進行一個記錄,使其可以進行儲存和傳輸呢?這就要用到序列化了。

  • 序列化(Serialization)
    把物件轉換為位元組序列的過程稱為物件的序列化,把物件的狀態保持下來,寫入到磁碟或者其他介質中。在此過程中,先將物件的公共欄位和私有欄位以及類的名稱(包括類所在的程式集)轉換為位元組流,然後再把位元組流寫入資料流。在隨後對物件進行反序列化時,將創建出與原物件完全相同的副本。

  • 反序列化(Deserialize)
    把位元組序列恢復為物件的過程稱為物件的反序列化

    ,把已存在在磁碟或者其他介質中的物件,讀取到記憶體中,以便後續操作。

Java 物件序列化是 JDK 1.1 中引入的一組開創性特性之一,用於作為一種將 Java 物件的狀態轉換為位元組陣列,以便儲存或傳輸的機制,以後,仍可以將位元組陣列轉換回 Java 物件原有的狀態。

實際上,序列化的思想是 “凍結” 物件狀態,傳輸物件狀態(寫到磁碟、通過網路傳輸等等),然後 “解凍” 狀態,重新獲得可用的 Java 物件。所有這些事情的發生有點像是魔術,這要歸功於 ObjectInputStream 和 ObjectOutputStream 類、完全保真的元資料以及程式設計師願意用 Serializable 標識介面標記他們的類,從而 “參與” 這個過程。

Android序列化

Android 序列化有兩種方式,實現Serializable 或 Parcelable。今天先搞懂 Java 中自帶的序列化方式 Serializable。

Serializable 是 java.io 包中定義的、用於實現 Java 類的序列化操作而提供的一個語義級別的介面,程式碼裡面除了註釋幾乎啥也沒有,僅僅是個介面:

package java.io;

// Android-added: Notes about serialVersionUID, using serialization judiciously, JSON.
/**
 * // 此處省略150行註釋...
 *
 * @author  unascribed
 * @see java.io.ObjectOutputStream
 * @see java.io.ObjectInputStream
 * @see java.io.ObjectOutput
 * @see java.io.ObjectInput
 * @see java.io.Externalizable
 * @since   JDK1.1
 */
public interface Serializable { }

既然啥都沒有,實現序列化和反序列化為什麼要實現Serializable介面?

在Java中實現了 Serializable 介面後, JVM 會在底層幫我們實現序列化和反序列化,如果我們不實現Serializable介面,那自己去寫一套序列化和反序列化程式碼也行,至於具體怎麼寫,Google一下你就知道了。

只要我們實現 Serializable 介面,那麼這個類就可以被 ObjectOutputStream 轉換為位元組流,也就是進行了序列化。那麼就來走一把,建立個簡單的學生物件,有年齡、姓名和地址等屬性。

public class Student implements Serializable {
    //private static final long serialVersionUID = 1L;

    private static String FLAG = "9527";// 靜態屬性
    private int age;
    private String name;
    private String address;
    transient private String car;// 忽略序列化
    //private String addTip;

    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 String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getCar() {
        return car;
    }

    public void setCar(String car) {
        this.car = car;
    }

    /*public String getAddTip() {
        return addTip;
    }

    public void setAddTip(String addTip) {
        this.addTip = addTip;
    }*/

    @Override
    public String toString() {
        return "[Student:" +
                "age='" + age + '\'' +
                ",name='" + name + '\'' +
                ",address='" + address + '\'' +
                ",car='" + car + '\'' +
                ",FLAG='" + FLAG + '\'' +
                ']';
    }
}

再來一個測試類:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerializableTest {

    public static void main(String[] args) throws Exception {
        // 初始化一個物件例項
        Student student = new Student();
        student.setAge(45);
        student.setAddress("suzhoujie");
        student.setName("xiaoming");

        // 序列化
        serializeStudent(student);

        // 反序列化
        Student dStudent = deserializeStudent();
        System.out.println(dStudent.toString());
    }

    /**
     * 序列化
     *
     * @param student 物件
     * @throws Exception IO異常
     */
    private static void serializeStudent(Student student) throws Exception {
        // ObjectOutputStream 物件輸出流,將 student 物件儲存到D盤的 student.txt 檔案中,完成對 student 物件的序列化操作
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("d:/student.txt")));
        oos.writeObject(student);// 對引數指定的obj物件進行序列化,把得到的位元組序列寫到一個目標輸出流中。
        System.out.println("Student 物件序列化成功!");
        oos.close();
    }

    /**
     * 反序列化
     *
     * @return 物件
     * @throws Exception IO異常
     */
    private static Student deserializeStudent() throws Exception {
        // ObjectInputStream 代表物件輸入流,
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:/student.txt")));
        Student student = (Student) ois.readObject();// 從一個源輸入流中讀取位元組序列,再把它們反序列化為一個物件,並將其返回。
        System.out.println("Student 物件反序列化成功!");
        return student;
    }
}

執行 main() 方法後在控制檯能看到序列化和發序列化,都沒啥問題:

Student 物件序列化成功!
Student 物件反序列化成功!
[Student:age='45',name='xiaoming',address='suzhoujie',car='null',FLAG='9527']

Process finished with exit code 0

在D盤生成了一個檔案student.txt,雖然後綴定義為txt,其實這只是個位元組流檔案,記事本開啟都是亂碼格式的
在這裡插入圖片描述
可能也能看到一些能識別的東西,但是已經是亂掉了。

注意到日誌 transient 修飾的 car欄位被抹掉了,避免了學生的攀比風氣, transient 這個修飾符就可以抹去一些不需要或者不必要參與序列化的欄位。

這個靜態變數 FLAG 也被序列化啦?no no no……

為了驗證靜態變數是否參與,我們修改一下測試檔案

    public static void main(String[] args) throws Exception {
        // 初始化一個物件例項
        Student student = new Student();
        student.setAge(45);
        student.setAddress("suzhoujie");
        student.setName("xiaoming");

        // 序列化
        serializeStudent(student);

//        // 反序列化
//        Student dStudent = deserializeStudent();
//        System.out.println(dStudent.toString());
    }

先遮蔽掉反序列化,執行:

Student 物件序列化成功!

Process finished with exit code 0

序列化沒問題,修改FLAG的值為666,再遮蔽掉序列化,只執行反序列化。

    public static void main(String[] args) throws Exception {
        // 初始化一個物件例項
//        Student student = new Student();
//        student.setAge(45);
//        student.setAddress("suzhoujie");
//        student.setName("xiaoming");
//
//        // 序列化
//        serializeStudent(student);

        // 反序列化
        Student dStudent = deserializeStudent();
        System.out.println(dStudent.toString());
    }
Student 物件反序列化成功!
[Student:age='45',name='xiaoming',address='suzhoujie',car='null',FLAG='666']

Process finished with exit code 0

FLAG的值變成了修改後的666,剛剛序列化的9527沒有讀出來。而是剛剛修改的666,如果可以的話應該是覆蓋這個666,是9527才對。序列化是針對物件而言的,靜態成員變數屬於類不屬於物件,而static屬性優先於物件存在,隨著類的載入而載入,所以,這個靜態 static 的屬性不參與序列化。

實現Serializable介面就算了, 為什麼還要顯示指定serialVersionUID的值?

Student 類中還有個serialVersionUID屬性被註釋掉了,接下來聊聊這個。

先看效果,還是這個類物件,只執行序列化,把物件存到本地去;再開啟註釋掉的 addTip 屬性,只執行反序列化方法,拋錨了:

Exception in thread "main" java.io.InvalidClassException: com.stock.messenger.entity.Student; local class incompatible: stream classdesc serialVersionUID = 5110753772711222189, local class serialVersionUID = -6906862297427533202
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
	at com.stock.messenger.entity.SerializableTest.deserializeStudent(SerializableTest.java:49)
	at com.stock.messenger.entity.SerializableTest.main(SerializableTest.java:22)

返回一個 InvalidClassException 的異常,並告知了serialVersionUID不匹配導致。原來坑在這裡!

也不難理解,因為開始的物件裡面是沒有明確的給這個 serialVersionUID 賦值,但是,Java會自動的給賦值的,這個值跟這個物件的屬性相關計算出來的。儲存的時候,也就是序列化的時候,那時候還沒有這個addTip屬性呢。所以,自動生成的serialVersionUID 這個值,在反序列化的時候 Java 自動生成的這個 serialVersionUID 值是不同的,就拋異常啦。

序列化執行時使用一個稱為 serialVersionUID 的版本號與每個可序列化類相關聯,該***在反序列化過程中用於驗證序列化物件的傳送者和接收者是否為該物件載入了與序列化相容的類。如果接收者載入的該物件的類的 serialVersionUID 與對應的傳送者的類的版本號不同,則反序列化將會導致 InvalidClassException。可序列化類可以通過宣告名為 “serialVersionUID” 的欄位(該欄位必須是靜態 (static)、最終 (final) 的 long 型欄位)顯式宣告其自己的 serialVersionUID。

如果可序列化類未顯式宣告 serialVersionUID,則序列化執行時將基於該類的各個方面計算該類的預設 serialVersionUID 值,然後與屬性一起序列化,再進行持久化或網路傳輸。如“Java™ 物件序列化規範”中所述。不過,強烈建議所有可序列化類都顯式宣告 serialVersionUID 值,原因是計算預設的 serialVersionUID 對類的詳細資訊具有較高的敏感性,根據編譯器實現的不同可能千差萬別,這樣在反序列化過程中可能會導致意外的 InvalidClassException。因此,為保證 serialVersionUID 值跨不同 java 編譯器實現的一致性,序列化類必須宣告一個明確的 serialVersionUID 值。還強烈建議使用 private 修飾符顯示宣告 serialVersionUID(如果可能),原因是這種宣告僅應用於直接宣告類 – serialVersionUID 欄位作為繼承成員沒有用處。陣列類不能宣告一個明確的 serialVersionUID,因此它們總是具有預設的計算值,但是陣列類沒有匹配 serialVersionUID 值的要求。在反序列化時, JVM會再根據屬性自動生成一個新版serialVersionUID,然後將這個新版 serialVersionUID 與序列化時生成的舊版 serialVersionUID 進行比較,如果相同則反序列化成功,否則報錯。如果顯示指定了serialVersionUID,JVM在序列化和反序列化時仍然都會生成一個serialVersionUID,但值為我們顯示指定的值,這樣在反序列化時新舊版本的serialVersionUID就一致了。

在實際開發中,不顯示指定 serialVersionUID 的情況會導致什麼問題? 如果我們的類寫完後不再修改,那當然不會有問題,但這在實際開發中是不可能的,我們的類會不斷迭代,一旦類被修改了,那舊物件反序列化就會報錯。所以在實際開發中,我們都會顯示指定一個serialVersionUID,值是多少無所謂,只要不變就行。

如果開始時候把 serialVersionUID = 1L;賦值,那再執行這個流程就不會有問題了,只是給這個 addTip 欄位賦值為null。

Student 物件反序列化成功!
[Student:age='45',name='xiaoming',address='suzhoujie',car='null',addTip='null',car='null',FLAG='666']

Process finished with exit code 0

這個就是為了物件升級做判斷用的功能,在實現這個 Serializable 介面的時候,一定要記得給這個 serialVersionUID 賦值。你可以不用自己去賦值,Java 會給你賦值一長串數字,但是,這個就會出現上面的 bug,很不安全,所以,還得自己手動的來,可以簡單的賦值個 1L,這就可以。

是不是有人會問,serialVersionUID 也被 static 修飾,為什麼 serialVersionUID 會被序列化?其實 serialVersionUID 屬性並沒有被序列化,JVM 在序列化物件時會自動生成一個 serialVersionUID,然後將我們顯示指定的 serialVersionUID 屬性值賦給自動生成的serialVersionUID。

當屬性是物件的時候,如果這個物件,沒實現序列化介面也會出問題,給學生新加一個工具物件 Tool

public class Tool {
    private String name;

    public String getName() {
        return name;
    }

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

// Student 新增Tool物件
private Tool tool;

public Tool getTool() {
    return tool;
}

public void setTool(Tool tool) {
    this.tool = tool;
}
//...

執行序列化就會看到這些錯誤:

Exception in thread "main" java.io.NotSerializableException: com.stock.messenger.entity.Tool
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
	at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
	at java.io.ObjectOutputStream.defaultWriteObject(ObjectOutputStream.java:441)
	at com.stock.messenger.entity.Student.writeObject(Student.java:83)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1140)
	at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1496)
	at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
	at com.stock.messenger.entity.SerializableTest.serializeStudent(SerializableTest.java:38)
	at com.stock.messenger.entity.SerializableTest.main(SerializableTest.java:22)

將 Tool 物件實現 Serializable,又可正常使用

public class Tool implements Serializable {
//...

下面是摘自 JDK API 文件裡面關於介面 Serializable 的描述

類通過實現 java.io.Serializable 介面以啟用其序列化功能。
未實現此介面的類將無法使其任何狀態序列化或反序列化。
可序列化類的所有子型別本身都是可序列化的。因為實現介面也是間接的等同於繼承。
序列化介面沒有方法或欄位,僅用於標識可序列化的語義。

模糊化序列化資料

物件某些屬性是敏感的資訊,我們可以考慮在序列化之前模糊化該資料,將數位迴圈左移一位,然後在反序列化之後復位。(您可以開發更安全的演算法,當前這個演算法只是作為一個例子。)

讓 Java 開發人員詫異並感到不快的是,序列化二進位制格式完全編寫在文件中,並且完全可逆。實際上,只需將二進位制序列化流的內容轉儲到控制檯,就足以看清類是什麼樣 子,以及它包含什麼內容。

這對於安全性有著不良影響。例如,當通過 RMI 進行遠端方法呼叫時,通過連線傳送的物件中的任何 private 欄位幾乎都是以明文的方式出現在套接字流中,這顯然容易招致哪怕最簡單的安全問題。

幸運的是,序列化允許 “hook” 序列化過程,並在序列化之前和反序列化之後保護(或模糊化)欄位資料。可以通過在 Serializable 物件上提供一個 writeObject 方法來做到這一點。

為了 “hook” 序列化過程,我們將在 Student 上實現一個 writeObject 方法;為了 “hook” 反序列化過程,我們將在同一個類上實現一個 readObject 方法。重要的是這兩個方法的細節要正確。

Student 中複寫下面兩個方法:

private void writeObject(ObjectOutputStream stream) throws IOException {
    // 加密,"Encrypt"/obscure the sensitive data
    age = age << 2;
    stream.defaultWriteObject();
}

private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
    stream.defaultReadObject();
    // 解密,"Decrypt"/de-obscure the sensitive data
    age = age >> 2;
}

執行結果完全沒有問題,也能正確的讀取出資料。

如果需要檢視被模糊化的資料,總是可以檢視序列化資料流/檔案。而且,由於該格式被完全文件化,即使不能訪問類本身,也仍可以讀取序列化流中的內容。

更多Tips可以檢視:關於 Java 物件序列化您不知道的 5 件事

推薦閱讀