1. 程式人生 > >物件的序列化和反序列化

物件的序列化和反序列化

物件的序列化和反序列化

序列化 (Serialization)將物件的狀態資訊轉換為可以儲存或傳輸的形式的過程。在序列化期間,物件將其當前狀態寫入到臨時或永續性儲存區。以後,可以通過從儲存區中讀取或反序列化物件的狀態,重新建立該物件。

當你建立物件時,只要你需要,它就會一直存在,但是當程式終止的時候,那麼這個物件也就隨之消失了,儘管這麼做是有意義的,但是仍舊存在某些的情況,如果物件能夠在程式不執行的情況下仍能存在並且儲存其資訊,那將會是非常有用的。這樣在下次執行程式的同時,該物件能夠被重建並且擁有的資訊與程式上次執行時它所擁有的資訊相同。

簡單來說序列化和反序列化如下

  • 序列化:把物件轉換為位元組序列的過程稱為物件的序列化
  • 反序列化:把位元組序列恢復為物件的過程稱為物件的反序列化

而什麼時候會用到序列化呢?一般在以下的情況中會使用到序列化

  • 物件的持久化:把物件的位元組序列永久地儲存到硬碟上,通常存放在一個檔案中

在很多應用中,需要對某些物件進行序列化,讓它們離開記憶體空間,入住物理硬碟,以便長期儲存。比如最常見的是Web伺服器中的Session物件,當有 10萬用戶併發訪問,就有可能出現10萬個Session物件,記憶體可能吃不消,於是Web容器就會把一些seesion先序列化到硬碟中,等要用了,再把儲存在硬碟中的物件還原到記憶體中。

  • 遠端呼叫:在網路上傳送物件的位元組序列

當兩個程序在進行遠端通訊時,彼此可以傳送各種型別的資料。無論是何種型別的資料,都會以二進位制序列的形式在網路上傳送。傳送方需要把這個Java物件轉換為位元組序列,才能在網路上傳送;接收方則需要把位元組序列再恢復為Java物件。

序列化的基本實現

只要物件實現了Serializable介面,物件的序列化就會變得十分簡單。要序列化一個物件首先要建立某些OutputStream物件,然後將其封裝在一個ObejctOutputStream物件內,這時只需要呼叫writeObject()即可將物件序列化,並將其傳送給OutputStream

物件序列化是基於位元組的,所以要使用InputStreamOutputStream繼承層次結構

如果要反向上面的過程(即將一個序列還原為一個物件),需要將一個InputStream封裝在ObjectInputStream內,然後呼叫readObject(),和往常一樣,我們最後獲得是一個引用,它指向了一個向上轉型的Object,所以必須向下轉型才能直接設定它們。

物件序列化不僅能夠將實現了介面的那個類進行序列化,也能夠將其引用的物件也例項化,以此類推。這種情況可以被稱之為物件網。單個物件可與之建立連線。

下面我們舉個例子可以看到在序列化和反序列過程中,物件網中的連線的物件資訊都沒有變。

public class TestSerializable {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        String fileName = "/Users/hupengfei/mytest.sql";
        Worm w = new Worm(6,'a');
        System.out.println("w:"+w);
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(fileName));
        out.writeObject("Worm Storage\n");
        out.writeObject(w);
        out.close();
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(fileName));
        String s = (String) in.readObject();
        Worm w2 = (Worm) in.readObject();
        System.out.println(s+"w2:"+w2);
    }
}

class Data implements Serializable{
    private Integer i ;
    public Data(Integer i ){
        this.i = i;
    }

    @Override
    public String toString() {
        return i.toString();
    }
}

class Worm implements Serializable{

    private static final long serialVersionUID = 8033549288339500180L;

    private static Random random = new Random(47);

    private Data [] d = {
            new Data(random.nextInt(10)),
            new Data(random.nextInt(10)),
            new Data(random.nextInt(10))
    };

    private Worm next;
    private char c;
    public Worm(int i ,char x){
        System.out.println("Worm Constructor:"+i);
        c = x;
        if (--i>0){
            next = new Worm(i,(char)(x+1));
        }
    }

    public Worm(){
        System.out.println("Default Constructor");
    }

    @Override
    public String toString() {
        StringBuffer result = new StringBuffer(":");
        result.append(c);
        result.append("(");
        for (Data data: d){
            result.append(data);
        }
        result.append(")");
        if (next!=null){
            result.append(next);
        }
        return result.toString();
    }
}

可以看到列印資訊如下

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)

在生成Data物件時是用隨機數初始化的,從輸出中可以看出,被還原後的物件確實包含了原物件中的所有連結。

上面我們舉了個如何進行序列化的例子,其中或許看到了serialVersionUID這個欄位,如果不加的話,那麼系統會自動的生成一個,而如果修改了類的話,哪怕加一個空格那麼這個serialVersionUID也會改變,那麼在反序列化的時候就會報錯,因為在反序列化的時候會將serialVersionUID和之前的serialVersionUID進行對比,只有相同的時候才會反序列化成功。所以還是建議顯視的定義一個serialVersionUID

transient(瞬時)關鍵字

當我們在對序列化進行控制的時候,可能需要某個欄位不想讓Java進行序列化機制進行儲存其資訊與恢復。如果一個物件的欄位儲存了我們不希望將其序列化的敏感資訊(例如密碼)。儘管我們使用private關鍵字但是如果經過序列化,那麼在進行反序列化的時候也是能將資訊給恢復過來的。我們舉個例子如下:

我們定義個Student

class Student implements Serializable{
    private static final long serialVersionUID = 1734284264262085307L;
    private String password;
------get set 方法
}

然後將其序列化到檔案中然後再從檔案中反序列化

public static void main(String[] args) throws IOException, ClassNotFoundException {
    String fileName="/Users/hupengfei/mytest.sql";
    Student student = new Student();
    student.setPassword("123456");
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(fileName));
    objectOutputStream.writeObject(student);
    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(fileName));
    Student readStudent = (Student) objectInputStream.readObject();
    System.out.println(readStudent.getPassword());
}

然後發現輸出為

readStudent的password=123456

此時我們如果想password引數在序列化的時候儲存其值,那麼可以加上transient關鍵字,就像下面一樣

private transient String password;

然後輸出如下

readStudent的password=null

發現在序列化的時候引數就已經沒被儲存進去了

參考文章