Java序列化 Serializable和Externalizable淺析

Serializable接口

public interface Serializable {
}

JDK源碼裏,Serializable接口只是一個空接口(標記接口),實際上,實現了Serializable接口的類,其序列化和反序列化過程,都是全自動的。

ObjectOutputStream#writeObject() 與 NotSerializableException

一個類必須實現Serializable接口才能序列化它的對象,不然調用ObjectOutputStream的writeObject方法會報錯。

import java.io.*;

class book {
    private String bookName;
    private int bookNum;

    public book(String bookName,int bookNum) {
        this.bookName = bookName;
        this.bookNum = bookNum;
    }
}

public class test {
    public static void write() throws IOException {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.out"));
        out.writeObject(new book("如何實現財務自由",1));
    }
    
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        write();
    }
}

如上程序,運行後在ObjectOutputStream.writeObject()處報錯Exception in thread “main” java.io.NotSerializableException: book。

當類定義加上Serializable後,程序不再報錯:

class book implements Serializable {
    private String bookName;
    private int bookNum;

    public book(String bookName,int bookNum) {
        this.bookName = bookName;
        this.bookNum = bookNum;
    }
}

ObjectInputStream#readObject() 與 ClassNotFoundException

在運行下面程序之前,先把圖中的book.class給刪掉。
在這裏插入圖片描述

import java.io.*;

public class test1 {

    public static void read() throws IOException, ClassNotFoundException {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("book.out"));
        book b = (book) in.readObject();
        System.out.println(b);
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        read();
    }
}

如上程序,運行後在ObjectInputStream.readObject()處報錯Exception in thread “main” java.lang.ClassNotFoundException: book。注意報錯是在readObject方法裏,而與強轉類型無關。

Serializable接口的類,不會調用任何構造器

需要注意的是,實現了Serializable接口的類的對象,在序列化的過程中,沒有調用過任何構造器,包括默認構造器。

import java.io.*;

class writer implements Serializable{
    String writerName;
    int age;

    writer(String writerName, int age) {
        this.writerName = writerName;
        this.age = age;
    }
}

class book implements Serializable {
    private String bookName;
    private int bookNum;
    private writer w;

    public book() {System.out.println("這是默認構造器");}

    public book(String bookName,int bookNum, writer w) {
        System.out.println("這是有參構造器");
        this.bookName = bookName;
        this.bookNum = bookNum;
        this.w = w;
    }

}

public class test {
    public static void write() throws IOException {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.out"));
        // 只有下面new的時候,才調用了有參構造器一下
        out.writeObject(new book("如何實現財務自由",1, new writer("巴菲特",56)));  
    }

    public static void read() throws IOException, ClassNotFoundException {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("book.out"));
        book b = (book) in.readObject();  //這裏並沒有調用任何構造器
        System.out.println(b);
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        write();
        read();
    }
}/*輸出:
這是有參構造器
book@27d6c5e0
*/

通過輸出可以看出,反序列化的過程中,任何構造器都沒有調用過。

非基本類型成員變量,需實現Serializable接口

在最開始的例子中,book類有兩個成員變量,其中有個String類型的成員變量,很明顯,String不是一個基本類型。那讓我們來看一下String的類定義:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {

發現作爲非基本類型的String確實實現了Serializable接口,但這不足以證明“非基本類型成員變量,需實現Serializable接口”。所以我們再反向驗證一下:

import java.io.*;

class writer {  //需實現Serializable接口
    String writerName;
    int age;

    writer(String writerName, int age) {
        this.writerName = writerName;
        this.age = age;
    }
}

class book implements Serializable {
    private String bookName;
    private int bookNum;
    private writer w;  //添加一個writer成員

    public book(String bookName,int bookNum, writer w) {
        this.bookName = bookName;
        this.bookNum = bookNum;
        this.w = w;
    }

}

public class test {
    public static void write() throws IOException {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.out"));
        out.writeObject(new book("如何實現財務自由",1, new writer("巴菲特",56)));
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        write();
    }
}
  • 現令book類新增一個成員變量爲自定義類writer,且writer沒有實現Serializable接口。
  • 程序運行後,報錯Exception in thread “main” java.io.NotSerializableException: writer。
  • 爲writer類實現Serializable接口後,程序能運行成功。

泛型類型的成員變量,需實現Serializable接口

import java.io.*;

class Item {  //需實現Serializable接口
    String itemName;
    int quality;

    Item(String itemName, int quality) {
        this.itemName = itemName;
        this.quality = quality;
    }
}

class Factory<I> implements Serializable{
    I item;
    Factory(I item) {
        this.item = item;
    }
}

public class test2 {
    public static void main(String[] args) throws IOException {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.out"));
        out.writeObject(new Factory<Item>(new Item("共享充電寶",2)));
    }
}

同樣的,成員變量的類型爲泛型類型,這個泛型類型也得實現Serializable接口。上面程序直接運行報錯,但Item類加上Serializable接口後,則可運行成功。

import java.io.*;

class Key {}  //需實現Serializable接口

class MyHashSet<K> implements Serializable{
    static class Node<K> {  //需實現Serializable接口
        K key;
        Node(K key) {
            this.key = key;
        }
    }

    Node<K>[] nodes;
    MyHashSet(K... keys ) {
        nodes = new Node[keys.length];
        int index = 0;
        for(K k : keys) {
            nodes[index++] = new Node<K>(k);
        }
    }
}

public class test2 {
    public static void main(String[] args) throws IOException {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.out"));
        out.writeObject(new MyHashSet<Key>( new Key(), new Key()));
    }
}

另一個例子,同樣的,直接運行會報錯,除非你把兩個類定義(MyHashSet$NodeKey)添加上Serializable接口。

import java.io.*;
import java.util.*;

public class test3 {
    static class Key {}  //需實現Serializable接口
    static class Value {}  //需實現Serializable接口

    public static void main(String[] args) throws IOException {
        HashMap<Key,Value> map = new HashMap<Key,Value>();
        map.put(new Key(), new Value());
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.out"));
        out.writeObject(map);
    }
}

直接使用HashMap,兩個泛型類型是自定義的類,同樣沒有實現Serializable接口。直接運行會報錯。

序列化保存所有對象網

一個對象包含若干個成員變量,成員變量又可能包含它的成員變量,這些持有的引用最終形成了一個無形的對象網,但Java的序列化過程卻能把整個對象網絡包含的所有對象全部序列化下來。

import java.io.*;
import java.util.*;

class Data implements Serializable{
    private int n;
    public Data(int n) { this.n = n; }
    public String toString() { return Integer.toString(n); }
}

public class Worm implements Serializable {
    private static Random rand = new Random(47);
    private Data[] d = {
            new Data(rand.nextInt(10)),
            new Data(rand.nextInt(10)),
            new Data(rand.nextInt(10))
    };
    private Worm next;
    private char c;
    // Value of i == number of segments
    public Worm(int i, char x) {
        print("Worm constructor: " + i);
        c = x;
        if(--i > 0)
            next = new Worm(i, (char)(x + 1));
    }
    public Worm() {
        print("Default constructor");
    }
    public String toString() {
        StringBuilder result = new StringBuilder(":");
        result.append(c);
        result.append("(");
        for(Data dat : d)
            result.append(dat);
        result.append(")");
        if(next != null)
            result.append(next);
        return result.toString();
    }
    public static void print(Object o) { System.out.println(o); }

    public static void main(String[] args)
            throws ClassNotFoundException, IOException {
        Worm w = new Worm(6, 'a');
        print("w = " + w);
        ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("worm.out"));
        out.writeObject("Worm storage\n");
        out.writeObject(w);
        out.close(); // Also flushes output
        ObjectInputStream in = new ObjectInputStream(
                new FileInputStream("worm.out"));
        String s = (String)in.readObject();
        Worm w2 = (Worm)in.readObject();
        print(s + "w2 = " + w2);
        ByteArrayOutputStream bout =
                new ByteArrayOutputStream();
        ObjectOutputStream out2 = new ObjectOutputStream(bout);
        out2.writeObject("Worm storage\n");
        out2.writeObject(w);
        out2.flush();
        ObjectInputStream in2 = new ObjectInputStream(
                new ByteArrayInputStream(bout.toByteArray()));
        s = (String)in2.readObject();
        Worm w3 = (Worm)in2.readObject();
        print(s + "w3 = " + w3);
    }
} /* Output:
Worm constructor: 6
Worm constructor: 5
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worm constructor: 1
w = :a(853):b(119):c(802):d(788):e(199):f(881)
Worm storage
w2 = :a(853):b(119):c(802):d(788):e(199):f(881)  //打印出的結果,和序列化之前的一樣
Worm storage
w3 = :a(853):b(119):c(802):d(788):e(199):f(881)
*///:~
  • Worm類的構造器調用new,這說明是構造器的遞歸調用。不過有着if(--i > 0)的遞歸終點,所以整個遞歸過程只會執行 主函數中給定的數字的次數。
  • Worm對象有一個Worm成員變量,它們之間就形成了一個對象網。而且Worm對象還有着Data[]成員,整個對象網絡包含的對象還不少。
  • 但即使是這樣複雜的對象網絡,Java的自動化序列化過程也能把所有對象都保存下來。這一點可以從打印結果看出來。
  • 注意到,第二次的序列化和反序列化,使用到了ByteArrayOutputStream/ByteArrayInputStream,這和之前沒有區別,只是之前的過程是把字節流寫入到了文件裏,現在是把字節流寫入到了內存中。

transient與writeObject/readObject

transient關鍵字

對象一旦經過序列化,即使是私有屬性也會執行序列化,由於反射技術的存在,私有屬性等同於裸奔在所有人面前。所以,爲了保證某個屬性根本不會執行序列化步驟,必須使用transient關鍵字。

import java.io.*;

class book implements Serializable {
    private String bookName;
    private int bookNum;
    private transient String writerName;  //此屬性不參與序列化

    @Override
    public String toString() {
        return "book{" +
                "bookName='" + bookName + '\'' +
                ", bookNum=" + bookNum +
                ", writerName='" + writerName + '\'' +
                '}';
    }
    
    public book(String bookName,int bookNum,String writerName) {
        this.bookName = bookName;
        this.bookNum = bookNum;
        this.writerName = writerName;
    }

}

public class test {
    public static void write() throws IOException {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.out"));
        out.writeObject(new book("如何實現財務自由",1, "巴菲特"));
    }

    public static void read() throws IOException, ClassNotFoundException {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("book.out"));
        book b = (book) in.readObject();
        System.out.println(b);
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        write();
        read();
    }
}/*輸出:
book{bookName='如何實現財務自由', bookNum=1, writerName='null'}
*/

可見,在整個透明的序列化過程之後,讀取出來的writerName爲null。這說明帶有transient關鍵字的成員變量,不會參與到序列化的過程中,所以,在反序列化之後,transient關鍵字的成員變量也只有初始化的值。

writeObject/readObject方法

  • 我們現在知道,當一個類實現了Serializable接口,那麼在調用ObjectOutputStream.writeObject(該類的對象)時,整個序列化過程會全自動進行:序列化每個成員變量,如果成員變量又持有了別的對象的引用,那麼這些對象又會被序列化,當然這個過程會除開帶有transient關鍵字的成員變量
  • 但如果我們提供了writeObject/readObject方法後,那麼在調用ObjectOutputStream.writeObject(該類的對象)時,將不再執行這個全自動過程,轉而執行你自己實現的這兩個方法。注意這兩個方法的簽名必須和如下簽名一模一樣,這有點神奇,其實它是使用到了反射技術來檢測方法的簽名。
    private void writeObject(java.io.ObjectOutputStream s)
        throws IOException {}
    private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException {}
  • 如果你已經提供了這兩個特定簽名的方法,但你還是想使用一下 序列化的全自動過程,那麼你可以在writeObject/readObject方法裏,調用s.defaultWriteObject();s.defaultReadObject();
import java.io.*;

class book implements Serializable {
    private String bookName;
    private int bookNum;
    private transient String writerName;

    @Override
    public String toString() {
        return "book{" +
                "bookName='" + bookName + '\'' +
                ", bookNum=" + bookNum +
                ", writerName='" + writerName + '\'' +
                '}';
    }

    public book(String bookName,int bookNum,String writerName) {
        this.bookName = bookName;
        this.bookNum = bookNum;
        this.writerName = writerName;
    }

    private void writeObject(java.io.ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        s.writeObject("序列化後:"+writerName);
    }
    private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        writerName = (String) s.readObject();
    }
}

public class test {
    public static void write() throws IOException {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.out"));
        out.writeObject(new book("如何實現財務自由",1, "巴菲特"));
    }

    public static void read() throws IOException, ClassNotFoundException {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("book.out"));
        book b = (book) in.readObject();
        System.out.println(b);
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        write();
        read();
    }
}}/*輸出:
book{bookName='如何實現財務自由', bookNum=1, writerName='序列化後:巴菲特'}
*/
  • 因爲提供了兩種特定簽名的writeObject/readObject方法,所以不會走 全自動序列化過程了,轉而執行你自己的邏輯。
  • 在writeObject/readObject方法裏,首先調用了s.defaultWriteObject();s.defaultReadObject();,所以還是執行了 全自動序列化過程,但帶有transient關鍵字的屬性沒有序列化進去。
  • 爲了保證帶有transient關鍵字的屬性能夠參與序列化,需要自己實現邏輯,即自己調用ObjectOutputStream.writeObject()ObjectInputStream.readObject()

HashMap的序列化過程

如下爲HashMap的序列化過程:

    transient Node<K,V>[] table;  //存儲節點的table,但它又是必須序列化的成員

    private void writeObject(java.io.ObjectOutputStream s)
        throws IOException {
        int buckets = capacity();
        // Write out the threshold, loadfactor, and any hidden stuff
        s.defaultWriteObject();  //執行自動化過程
        s.writeInt(buckets);  //寫入容量
        s.writeInt(size);  //寫入大小
        internalWriteEntries(s);
    }

    void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
        Node<K,V>[] tab;
        if (size > 0 && (tab = table) != null) {
            for (int i = 0; i < tab.length; ++i) {  //遍歷整個table數組
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    s.writeObject(e.key);  //先寫入當前節點的key
                    s.writeObject(e.value);  //再寫入當前節點的value
                }
            }
        }
    }

如下爲HashMap的反序列化過程:

//注意,該函數我省略了部分代碼
    private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException {
        s.defaultReadObject();  //執行自動化過程
        s.readInt();                // 讀出容量,但直接忽略掉
        int mappings = s.readInt(); // 讀出大小

        // Read the keys and values, and put the mappings in the HashMap
        for (int i = 0; i < mappings; i++) {  //遍歷整個table數組
            @SuppressWarnings("unchecked")
                K key = (K) s.readObject();  //先讀出當前節點的key
            @SuppressWarnings("unchecked")
                V value = (V) s.readObject();  //先讀出當前節點的value
            putVal(hash(key), key, value, false, false);
        }
    }

Externalizable接口

public interface Externalizable extends java.io.Serializable {
	void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

看來這個Externalizable不再是個空接口了,人家可是有方法的。

必須有public的默認構造器

import java.io.*;

class writer implements Serializable{
    String writerName;
    int age;

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

    writer(String writerName, int age) {
        this.writerName = writerName;
        this.age = age;
    }
}

class book implements Externalizable {
    private String bookName;
    private int bookNum;
    private writer w;

    @Override
    public String toString() {
        return "book{" +
                "bookName='" + bookName + '\'' +
                ", bookNum=" + bookNum +
                ", writer='" + w + '\'' +
                '}';
    }

    // public book () {}   //解開註釋以運行成功

    public book(String bookName,int bookNum,writer w) {
        this.bookName = bookName;
        this.bookNum = bookNum;
        this.w = w;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(bookName);
        out.writeInt(bookNum);
        out.writeObject(w);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        bookName = (String) in.readObject();
        bookNum = in.readInt();
        w = (writer) in.readObject();
    }
}

public class test {
    public static void write() throws IOException {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.out"));
        out.writeObject(new book("如何實現財務自由",1, new writer("巴菲特",56)));
    }

    public static void read() throws IOException, ClassNotFoundException {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("book.out"));
        book b = (book) in.readObject();
        System.out.println(b);
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        write();
        read();
    }
}
  • 當你直接運行此程序,發現會報錯java.io.InvalidClassException: book; no valid constructor,除非你提供一個public的默認構造器。
  • 當你實現的是Externalizable接口時,所有成員都得自己實現序列化的邏輯,不然就不會參與序列化之中。
  • transient關鍵字就毫無意義了,特定簽名的writeObject/readObject方法也毫無意義了。s.defaultWriteObject();s.defaultReadObject();也不能使用了,因爲參數變成了ObjectOutputObjectInput接口了。
  • 而且注意,成員變量在定義時的初始化也會執行。(這好像是廢話,因爲肯定得先執行字段定義時初始化,再調用構造器)。修改部分代碼以驗證:
class writer implements Serializable{
    String writerName;
    int age;

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

    writer(String writerName, int age) {
        System.out.println("調用了writer的構造器");
        this.writerName = writerName;
        this.age = age;
    }
}

class book implements Externalizable {
    private String bookName;
    private int bookNum;
    private writer w = new writer("索羅斯",59);  //字段定義時的初始化
...
/*輸出:
調用了writer的構造器
調用了writer的構造器
調用了writer的構造器   //第三次打印,是反序列化時,調用默認構造器之前打印出來的
book{bookName='如何實現財務自由', bookNum=1, writer='writer{writerName='巴菲特', age=56}'}
*/

serialVersionUID

當你實現了Serializable接口,你需要添加一個靜態變量serialVersionUID,這個靜態變量是用來驗證版本一致性的,當傳來的字節流中的serialVersionUID,和本地class文件的serialVersionUID不一致時,反序列化就會報錯。

如果你沒有顯式地添加靜態變量serialVersionUID,那麼程序會爲你隱式地自動生成一個serialVersionUID,但這卻帶來了極大的隱患。

import java.io.*;

class book implements Serializable {
    private String bookName;
    private int bookNum;
    // private int a;

    public book(String bookName,int bookNum) {
        this.bookName = bookName;
        this.bookNum = bookNum;
    }
}

public class test {
    public static void write() throws IOException {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.out"));
        out.writeObject(new book("如何實現財務自由",1));
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        write();
    }
}

如上程序執行完畢後,取消掉private int a;的註釋。然後再執行如下代碼:

import java.io.*;

public class test1 {

    public static void read() throws IOException, ClassNotFoundException {
        new book("打工是不可能打工的",1);
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("book.out"));
        book b = (book) in.readObject();
        System.out.println(b);
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        read();
    }
}
  • new book("打工是不可能打工的",1);這裏必須執行,不然不會更新本地的class文件。由於book多了一個變量,serialVersionUID發生了改變。
  • 運行後報錯:java.io.InvalidClassException: book; local class incompatible: stream classdesc serialVersionUID = 3988735506709559426, local class serialVersionUID = 6344372820327052271
  • 同樣的,Externalizable接口,也最好給定serialVersionUID,不然可能會發生同樣的錯誤。

未實現Serializable的父類,不參與序列化

import java.io.*;
import java.util.*;

class Father {
    String FatherName;

    public Father(String fatherName) {
        FatherName = fatherName;
    }
}

class Son extends Father implements Serializable {
    String SonName;

    public Son(String fatherName, String sonName) {
        super(fatherName);
        SonName = sonName;
    }

    @Override
    public String toString() {
        return "Son{" +
                "FatherName='" + FatherName + '\'' +
                ", SonName='" + SonName + '\'' +
                '}';
    }
}

public class test4 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.out"));
        out.writeObject(new Son("爸爸","兒子"));
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.out"));
        Son s = (Son) in.readObject();
        System.out.println(s);
    }
}
  • 如上代碼,父類並沒有實現Serializable接口,直接運行時,會報錯java.io.InvalidClassException: Son; no valid constructor,看來是沒有默認構造器鬧的。
  • 當你給如上代碼的Father類加上implements Serializable時,程序就能直接運行成功了。且打印結果爲Son{FatherName='爸爸', SonName='兒子'}
import java.io.*;
import java.util.*;

class Father {
    String FatherName;
    public Father() {}
    public Father(String fatherName) {
        FatherName = fatherName;
    }
}

class Son extends Father implements Serializable {
    String SonName;
    public Son() {
        super();
    }
    public Son(String fatherName, String sonName) {
        super(fatherName);
        SonName = sonName;
    }

    @Override
    public String toString() {
        return "Son{" +
                "FatherName='" + FatherName + '\'' +
                ", SonName='" + SonName + '\'' +
                '}';
    }
}

public class test4 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.out"));
        out.writeObject(new Son("爸爸","兒子"));
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.out"));
        Son s = (Son) in.readObject();
        System.out.println(s);
    }
}
  • 經過爲父類和子類都添加上默認構造器後,程序能夠運行成功。但注意,默認構造器可以不是public的了,private都可以。
  • 打印結果爲Son{FatherName='null', SonName='兒子'},可見FatherName爲null跟序列化沒啥關係,都是因爲你默認構造器啥也沒幹造成的。
  • 因爲父類沒有實現Serializable接口,所以程序是不會序列化父類對象的,但一個子類對象存在的前提就是得先有父類對象,所以就強制讓你加一個子類的默認構造器,而子類的任何構造器都得調用到父類的構造器,所以,這樣就保證了父類構造器能被調用。

父類實現了Serializable接口,子類也會是Serializable的

這個好像是廢話,總之,從繼承樹上來說,子類是間接繼承到Serializable了,所以子類肯定也可以序列化的。下一章的Shape和它的三個子類就體現了這一點。

靜態變量不參與到序列化過程中

import java.io.*;
import java.util.*;

abstract class Shape implements Serializable {
    public static final int RED = 1, BLUE = 2, GREEN = 3;
    private int xPos, yPos, dimension;
    private static Random rand = new Random(47);
    private static int counter = 0;
    public abstract void setColor(int newColor);
    public abstract int getColor();
    public Shape(int xVal, int yVal, int dim) {
        xPos = xVal;
        yPos = yVal;
        dimension = dim;
    }
    public String toString() {
        return getClass() +
                "color[" + getColor() + "] xPos[" + xPos +
                "] yPos[" + yPos + "] dim[" + dimension + "]\n";
    }
    public static Shape randomFactory() {
        int xVal = rand.nextInt(100);
        int yVal = rand.nextInt(100);
        int dim = rand.nextInt(100);
        switch(counter++ % 3) {
            default:
            case 0: return new Circle(xVal, yVal, dim);
            case 1: return new Square(xVal, yVal, dim);
            case 2: return new Line(xVal, yVal, dim);
        }
    }
}

class Circle extends Shape {
    private static int color = RED;
    public Circle(int xVal, int yVal, int dim) {
        super(xVal, yVal, dim);
    }
    public void setColor(int newColor) { color = newColor; }
    public int getColor() { return color; }
}

class Square extends Shape {
    private static int color;
    public Square(int xVal, int yVal, int dim) {
        super(xVal, yVal, dim);
        color = RED;
    }
    public void setColor(int newColor) { color = newColor; }
    public int getColor() { return color; }
}

class Line extends Shape {
    private static int color = RED;
    public static void
    serializeStaticState(ObjectOutputStream os)
            throws IOException { os.writeInt(color); }
    public static void
    deserializeStaticState(ObjectInputStream os)
            throws IOException { color = os.readInt(); }
    public Line(int xVal, int yVal, int dim) {
        super(xVal, yVal, dim);
    }
    public void setColor(int newColor) { color = newColor; }
    public int getColor() { return color; }
}

public class StoreCADState {
    public static void main(String[] args) throws Exception {
        List<Shape> shapes = new ArrayList<Shape>();
        // Make some shapes:
        for(int i = 0; i < 10; i++)
            shapes.add(Shape.randomFactory());
        // Set all the static colors to GREEN:
        for(int i = 0; i < 10; i++)
            ((Shape)shapes.get(i)).setColor(Shape.GREEN);

        List<Class<? extends Shape>> shapeTypes =
                new ArrayList<Class<? extends Shape>>();
        // Add references to the class objects:
        shapeTypes.add(Circle.class);
        shapeTypes.add(Square.class);
        shapeTypes.add(Line.class);

        // Save the state vector:
        ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream("CADState.out"));
        out.writeObject(shapeTypes);
        Line.serializeStaticState(out);
        out.writeObject(shapes);
        // Display the shapes:
        System.out.println(shapes);
    }
} /* Output:
[class Circlecolor[3] xPos[58] yPos[55] dim[93]
, class Squarecolor[3] xPos[61] yPos[61] dim[29]
, class Linecolor[3] xPos[68] yPos[0] dim[22]
, class Circlecolor[3] xPos[7] yPos[88] dim[28]
, class Squarecolor[3] xPos[51] yPos[89] dim[9]
, class Linecolor[3] xPos[78] yPos[98] dim[61]
, class Circlecolor[3] xPos[20] yPos[58] dim[16]
, class Squarecolor[3] xPos[40] yPos[11] dim[22]
, class Linecolor[3] xPos[4] yPos[83] dim[6]
, class Circlecolor[3] xPos[75] yPos[10] dim[42]
]
*///:~
  • 這個例子來自Java編程思想,稍作改動。簡單介紹一下就是:有個Shape父類,其有三個子類,這三個子類都新增了一個靜態變量color,此代碼把三個子類的Class對象序列化到文件,以觀察反序列後靜態變量。
  • 靜態變量color會在字段定義就初始化,緊接着會被靜態代碼塊修改(代碼中沒有靜態代碼塊)。再然後,就是當構造器調用或函數調用時,也會改變靜態變量(提供的成員方法setColor可修改,Square的構造器裏會修改,Line的靜態方法deserializeStaticState也會修改)。
  • 在調用for(int i = 0; i < 10; i++) ((Shape)shapes.get(i)).setColor(Shape.GREEN);的循環後,三個子類的靜態變量color都會被修改爲3,以至於在之後的打印結果裏,所有靜態變量都是3。在這個循環後,我們將Class對象放入List,然後將Class對象的List序列化,以觀察反序列化後,序列化之前靜態變量的修改是否能體現。
import java.io.*;
import java.util.*;

public class RecoverCADState {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) throws Exception {
        ObjectInputStream in = new ObjectInputStream(
                new FileInputStream("CADState.out"));
        // Read in the same order they were written:
        List<Class<? extends Shape>> shapeTypes =
                (List<Class<? extends Shape>>)in.readObject();
        Line.deserializeStaticState(in);
        List<Shape> shapes = (List<Shape>)in.readObject();
        System.out.println(shapes);
    }
} /* Output:
[class Circlecolor[1] xPos[58] yPos[55] dim[93]
, class Squarecolor[0] xPos[61] yPos[61] dim[29]
, class Linecolor[3] xPos[68] yPos[0] dim[22]
, class Circlecolor[1] xPos[7] yPos[88] dim[28]
, class Squarecolor[0] xPos[51] yPos[89] dim[9]
, class Linecolor[3] xPos[78] yPos[98] dim[61]
, class Circlecolor[1] xPos[20] yPos[58] dim[16]
, class Squarecolor[0] xPos[40] yPos[11] dim[22]
, class Linecolor[3] xPos[4] yPos[83] dim[6]
, class Circlecolor[1] xPos[75] yPos[10] dim[42]
]
*///:~
  • 現在把序列化後的東西都反序列化取出來,觀察打印結果:
    • Circlecolor爲1,這是因爲Circle的color在字段定義時就已經初始化爲1了。
    • Squarecolor爲0,這是因爲Square的color在字段定義時沒有初始化,但java會給它一個初始值0。
    • Linecolor爲3,這是因爲你調用了deserializeStaticState,將序列化時存儲的一個int值取出來了,並賦值給color靜態變量了。不然Linecolor也爲1。
  • 綜上,幾乎可以認爲, 序列化時並沒有存儲靜態變量,因爲反序列化時得到的靜態變量,也只是從靜態變量字段定義和靜態代碼塊得到的。
  • 但上面這個程序並沒有從List<Class<? extends Shape>> shapeTypes裏得到靜態變量,好像有點佐證不足,所以請看下面這個程序:
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.*;

public class RecoverCADState {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) throws Exception {
        ObjectInputStream in = new ObjectInputStream(
                new FileInputStream("CADState.out"));
        // Read in the same order they were written:
        List<Class<? extends Shape>> shapeTypes =
                (List<Class<? extends Shape>>)in.readObject();
        for(Class<? extends Shape> clazz: shapeTypes) {
            System.out.println(clazz.getSimpleName());
            Field field = clazz.getDeclaredField("color");
            field.setAccessible(true);
            for(Field f: clazz.getFields()) {
                System.out.print(f.getName()+" ");
            }
            System.out.println();
            System.out.println(String.format("當前Class對象的類名是%s,通過Class對象獲得靜態變量color爲%d",
                    clazz.getSimpleName(), (Integer)field.get(clazz)));  // (Integer)field.get(clazz)通過Class對象來獲得靜態變量
        }

        Circle circle = new Circle(1,2,3);
        System.out.println("調用構造器後 Circle類的color靜態變量爲"+circle.getColor());
        Square square = new Square(1,2,3);
        System.out.println("調用構造器後 Square類的color靜態變量爲"+square.getColor());
        Line line = new Line(1,2,3);
        System.out.println("調用構造器後 Line類的color靜態變量爲"+line.getColor());

        //Line.deserializeStaticState(in);
        in.readInt();
        List<Shape> shapes = (List<Shape>)in.readObject();
        System.out.println(shapes);
    }
} /* Output:
Circle
RED BLUE GREEN 
當前Class對象的類名是Circle,通過Class對象獲得靜態變量color爲1
Square
RED BLUE GREEN 
當前Class對象的類名是Square,通過Class對象獲得靜態變量color爲0
Line
RED BLUE GREEN 
當前Class對象的類名是Line,通過Class對象獲得靜態變量color爲1
調用構造器後 Circle類的color靜態變量爲1
調用構造器後 Square類的color靜態變量爲1
調用構造器後 Line類的color靜態變量爲1
[class Circlecolor[1] xPos[58] yPos[55] dim[93]
, class Squarecolor[1] xPos[61] yPos[61] dim[29]
, class Linecolor[1] xPos[68] yPos[0] dim[22]
, class Circlecolor[1] xPos[7] yPos[88] dim[28]
, class Squarecolor[1] xPos[51] yPos[89] dim[9]
, class Linecolor[1] xPos[78] yPos[98] dim[61]
, class Circlecolor[1] xPos[20] yPos[58] dim[16]
, class Squarecolor[1] xPos[40] yPos[11] dim[22]
, class Linecolor[1] xPos[4] yPos[83] dim[6]
, class Circlecolor[1] xPos[75] yPos[10] dim[42]
]
*///:~
  • 此程序終於使用到了List<Class<? extends Shape>> shapeTypes,並且從裏面的各個Class對象獲得到了靜態變量color,從打印結果看,終於坐實了:序列化時不存儲靜態變量,反序列化時得到的靜態變量只是從靜態變量字段定義和靜態代碼塊獲得的,換句話說,是從本地class文件中獲得的,因爲靜態變量字段定義和靜態代碼塊就存儲在本地class文件裏。
  • 不再調用Line.deserializeStaticState(in),所以Line的靜態變量也不會修改了。但必須調用in.readInt()以保證後面能正常讀取。
  • 最後打印對象時,Squarecolor爲1,是因爲調用了它的構造器。

Serializable與深拷貝/淺拷貝

//: io/MyWorld.java
import java.io.*;
import java.util.*;
import static net.mindview.util.Print.*;

class House implements Serializable {}

class Animal implements Serializable {
  private String name;
  private House preferredHouse;
  Animal(String nm, House h) {
    name = nm;
    preferredHouse = h;
  }
  public String toString() {
    return name + "[" + super.toString() +  //super.toString()爲了打印出子類對象的父類對象的地址
      "], " + preferredHouse + "\n";
  }
}

public class MyWorld {
  public static void main(String[] args)
  throws IOException, ClassNotFoundException {
    House house = new House();
    List<Animal> animals = new ArrayList<Animal>();
    animals.add(new Animal("Bosco the dog", house));
    animals.add(new Animal("Ralph the hamster", house));
    animals.add(new Animal("Molly the cat", house));
    print("animals: " + animals);
    ByteArrayOutputStream buf1 =
      new ByteArrayOutputStream();
    ObjectOutputStream o1 = new ObjectOutputStream(buf1);
    o1.writeObject(animals);
    o1.writeObject(animals); // Write a 2nd set
    // Write to a different stream:
    ByteArrayOutputStream buf2 =
      new ByteArrayOutputStream();
    ObjectOutputStream o2 = new ObjectOutputStream(buf2);
    o2.writeObject(animals);
    // Now get them back:
    ObjectInputStream in1 = new ObjectInputStream(
      new ByteArrayInputStream(buf1.toByteArray()));
    ObjectInputStream in2 = new ObjectInputStream(
      new ByteArrayInputStream(buf2.toByteArray()));
    List
      animals1 = (List)in1.readObject(),
      animals2 = (List)in1.readObject(),
      animals3 = (List)in2.readObject();
    print("animals1: " + animals1);
    print("animals2: " + animals2);
    print("animals3: " + animals3);
  }
} /* Output: (Sample)
animals: [Bosco the dog[Animal@addbf1], House@42e816
, Ralph the hamster[Animal@9304b1], House@42e816
, Molly the cat[Animal@190d11], House@42e816
]
animals1: [Bosco the dog[Animal@de6f34], House@156ee8e
, Ralph the hamster[Animal@47b480], House@156ee8e
, Molly the cat[Animal@19b49e6], House@156ee8e
]
animals2: [Bosco the dog[Animal@de6f34], House@156ee8e
, Ralph the hamster[Animal@47b480], House@156ee8e
, Molly the cat[Animal@19b49e6], House@156ee8e
]
animals3: [Bosco the dog[Animal@10d448], House@e0e1c6
, Ralph the hamster[Animal@6ca1c], House@e0e1c6
, Molly the cat[Animal@1bf216a], House@e0e1c6
]
*///:~
  • 代碼來自Java編程思想,我覺得很經典。
  • List中添加了三個Animal對象,三者持有的House對象引用是一樣的,也就是同一個House對象。
  • 從打印結果來看,對比animals和animals1:發現animals1反序列化取出的對象,完全是不同的了,也就是說,反序列化實現了深拷貝。而且反序列化還保持了同樣的關係——三個Animal對象持有同一個House對象。
  • 從打印結果來看,對比animals1和animals2:發現兩個打印結果完全一樣,可以說是完全的淺拷貝。原因是這二者的輸出流是同一個ByteArrayOutputStream buf1,對於同一個輸出流,相同對象再次寫入,不會發生拷貝。
  • 從打印結果來看,對比animals2和animals3:發現這二者又是深拷貝了,因爲這二者對應着兩個不同的輸出流ByteArrayOutputStream buf1ByteArrayOutputStream buf2,不同的輸出流相當於兩個隔離的空間,自然會重新生成對象。
  • 把ByteArrayOutputStream換成FileOutputStream也是一樣的。

其他

  • 類的靜態變量不會參與到序列化過程中,也就是說,反序列化時,靜態變量總是會從本地class文件讀取出來。但serialVersionUID這個靜態變量是個特例,在序列化過程中輸出的字節流中,將帶有serialVersionUID的信息。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章