1. 程式人生 > >第七十五條 考慮使用自定義的序列化形式

第七十五條 考慮使用自定義的序列化形式

序列化使用起來比價方便,但有一些常見的細節需要注意,比如說定義 serialVersionUID 值,關鍵字 transient 的用法,下面就用例子來說明

定義一個bean,實現序列化的介面,

public class Student implements Serializable {
    int age;
    String address;


    public Student(int age, String address) {
        this.age = age;
        this.address = address;
    }
}    

在main中執行序列化寫入本地的方法

    static final String PATH = "e:/data.txt";
    public static void main(String[] args) throws Exception {

        write();

    }

    private static void write() throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
                PATH));
        Student student = new Student(25, "中國");
        oos.writeObject(student);
        oos.close();
    }
執行過後,發現電腦E盤多了個文字檔案,開啟txt文字,裡面內容為  sr -com.example.cn.desigin.utils.JavaTest$Student       I ageL addresstLjava/lang/String;xp   t 涓浗,說明把物件以位元組流的形式存在了文字中。我們再反序列化一下,看看能否還原成物件,執行以下程式碼

    private static void read() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
                PATH));
        Student student = (Student) ois.readObject();
        System.out.println("age=" + student.age + ";address=" + student.address);
        ois.close();
    }

打印出的內容為 age=25;address=中國,說明反序列化成功。這樣寫,看似沒問題,實際上有隱患。如果我們的 Student 類,以後不會做任何屬性的擴充套件,也不會在裡面新增空格之類的,總之就是不會再去修改這個類,連個空格都不加之類的,那麼可以這樣寫;如果不敢保證,比如說肯能再擴充套件一個 性別 的屬性,那麼一旦 Student 的類變化了,E盤中txt文字內容反序列化的時候,就會出錯了。那麼怎麼辦呢?這時候 serialVersionUID 就登場了,我們在 Student 中宣告它就可以了,private static final long serialVersionUID = 1L; 或者讓系統自動生成它的值,在我的電腦上是 private static final long serialVersionUID = 6392945738859063583L;

public class Student implements Serializable {
    private static final long serialVersionUID = 6392945738859063583L;
    int age;
    String address;
    int sex;


    public Student(int age, String address, int sex) {
        this.age = age;
        this.address = address;
        this.sex = sex;
    }
}

如此,序列化文字中沒有這個屬性的值時,反序列化以後,值時預設值,String 型別為 null, int 型別為 0 ,依次類推。

預設的序列化會把所有屬性全都記錄到文字中,如果說Student中,如果我們不想把 address 屬性序列化怎麼辦?一種方法是儲存字串,把物件通過 Gson 等第三方工具類把物件轉換為json 型別的字串,然後把 address 屬性及對應的值刪掉,json串支援刪除節點的功能,然後儲存字串,使用的時候取出字串,然後再通過 Gson 轉換為物件。這種方法繁瑣但比較保險,它支援物件Student 的包名字的變換及類名的變化,缺點是比較繁瑣,總之如果你的bean物件經常變化包名的話,這是一個不錯的方法,如果bean是萬年位置不變的話,可以用第二種方法。第二種方法就是序列化提供的關鍵字 transient ,哪個屬性不需要被序列化就用它來修飾即可,比如


public class Student implements Serializable {
    private static final long serialVersionUID = 6392945738859063583L;
    int age;
    transient String address;


    public Student(int age, String address) {
        this.age = age;
        this.address = address;
    }
}

序列化文字內容為  sr -com.example.cn.desigin.utils.JavaTest$StudentX窷G9? I agexp   ,反序列化以後,列印物件值為 age=25;address=null, 如此,證明此方案可以。以上是預設的序列化,即系統給咱們預設的道路,按照這條路走就可以了。如果你不想走常規路,或者預設的路滿足不了你們公司的需求,那麼可以自定義序列化格式,形成自己的定製版。想自己定製,成為自己的定製版,那麼只需要編寫 writeObject 和 readObject 方法即可,還以 Student 為例,如下

public class Student implements Serializable {
    private static final long serialVersionUID = 6392945738859063583L;
    int age;
    String address;


    public Student(int age, String address) {
        this.age = age;
        this.address = address;
    }

    //JAVA BEAN自定義的writeObject方法
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeInt(age);
        out.writeObject(address);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        this.age = in.readInt();
        this.address = in.readObject().toString();
    }

}

執行後,儲存到本地的序列化的值為  sr -com.example.cn.desigin.utils.JavaTest$StudentX窷G9? I ageL addresst Ljava/lang/String;xpw   t 涓浗x, 反序列後列印的物件的值為 age=25;address=中國。 如果只想序列化 age 屬性,那麼不要把 address 寫入即可, 把 out.writeObject(address); 和 this.address = in.readObject().toString(); 這兩行程式碼註釋掉即可,序列化的值為  sr -com.example.cn.desigin.utils.JavaTest$StudentX窷G9? I ageL addresst Ljava/lang/String;xpw   x, 反序列化物件值為 age=25;address=null,這是第三種不想序列化某個屬性的方法,定製版。

使用定製版需要注意些事項,writeObject 和 readObject 方法中, write 和 read 物件屬性時,一定要對上順序,順序不能錯亂,否則就錯了。

下面稍微講一下原理,我們發現,Student 物件的父類是 Object,裡面沒有 writeObject(ObjectOutputStream out)方法,那麼Student 中的這個方法就不是重寫了,怎麼回事呢?一步步看吧, 我們呼叫 oos.writeObject(student); 方法,看一下原始碼

    public final void writeObject(Object object) throws IOException {
        writeObject(object, false);
    }

這個方法,會呼叫 writeObjectInternal(object, unshared, true, true); 方法,把 student 引用繼續往下傳, 這個方法有兩行比較關鍵的程式碼,
    Class<?> objClass = object.getClass();
    ObjectStreamClass clDesc = ObjectStreamClass.lookupStreamClass(objClass);

看看靜態方法 ,裡面用到了Map快取技術,

    static ObjectStreamClass lookupStreamClass(Class<?> cl) {
        WeakHashMap<Class<?>, ObjectStreamClass> tlc = getCache();
        ObjectStreamClass cachedValue = tlc.get(cl);
        if (cachedValue == null) {
            cachedValue = createClassDesc(cl);
            tlc.put(cl, cachedValue);
        }
        return cachedValue;
    }

看一下 createClassDesc(cl) 方法中的關鍵程式碼

    private static ObjectStreamClass createClassDesc(Class<?> cl) {

        ObjectStreamClass result = new ObjectStreamClass();


        result.methodWriteReplace = findMethod(cl, "writeReplace");
        result.methodReadResolve = findMethod(cl, "readResolve");
        result.methodWriteObject = findPrivateMethod(cl, "writeObject", WRITE_PARAM_TYPES);
        result.methodReadObject = findPrivateMethod(cl, "readObject", READ_PARAM_TYPES);
        result.methodReadObjectNoData = findPrivateMethod(cl, "readObjectNoData", EmptyArray.CLASS);
        if (result.hasMethodWriteObject()) {
            flags |= ObjectStreamConstants.SC_WRITE_METHOD;
        }
        result.setFlags(flags);

        return result;
    }

可看到這就明白了,原來是通過反射來檢查 bean 中是否有重寫這幾個方法,通過反射來呼叫方法,所以自定義序列化時,我們自己寫這兩個方法,而不是重寫,因為父類沒有。

細心的同學會發現,下面的方法有所不同,

        private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeInt(age);
        out.writeObject(address);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        this.age = in.readInt();
        this.address = in.readObject().toString();
    }
 

序列化時,多了個 out.defaultWriteObject(); 方法, 反序列化時,多了個 in.defaultReadObject(); 方法,那麼這兩個方法是幹嘛用的呢?很明顯,它們倆是對應著的,在下對這一塊也不是很瞭解,按照個人的體會,這兩個方法是相對的,要麼都存在,要麼都不存在; defaultWriteObject() 和 defaultReadObject() 是系統預設的序列化, out.writeInt(age); out.writeObject(address);這個是自己自定義的,可以理解為 他們是 父類和子類 方法中的關係,相同於實現父類方法同時,又擴充套件了子類的方法,如果 defaultWriteObject() 和自定義序列化中同時操作了 age的值,例如

        private void writeObject(ObjectOutputStream out) throws IOException {
            out.defaultWriteObject();
            out.writeInt(age + 10);
            out.writeObject(address);
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            in.defaultReadObject();
            this.age = in.readInt();
            this.address = in.readObject().toString();
            this.age = age - 1;
        }


按照 Student student = new Student(23, "中國"); oos.writeObject(student); 此時,自定義為準,比如傳入的age是23,out.defaultWriteObject();對應的就是23,但我們自定義時,把age的值增加了10,變為33,然後序列化,此時序列化本地文字中的值是 33; 然後反序列化時,  in.defaultReadObject(); 讀出來的是 33 ,在下面有減去了1,即置為 32。
執行結果, 是    age:32  address:中國 。