Java序列化(Serializable)與反序列化詳解

indexman發表於2018-01-08

什麼是序列化?

Java序列化是在JDK 1.1中引入的,是Java核心的重要特性之一。
Java序列化API允許我們將一個物件轉換為流,並通過網路傳送,或將其存入檔案或資料庫以便未來使用,
反序列化則是將物件流轉換為實際程式中使用的Java物件的過程。

序列化有啥用?

1.暫存大物件
2.Java物件需要持久化的時候
3.需要在網路,例如socket中傳輸Java物件
    因為資料只能夠以二進位制的形式在網路中進行傳輸,因此當把物件通過網路傳送出去之前需要先序列化成二進位制資料,在接收  端讀到二進位制資料之後反序列化成Java物件
4.深度克隆(複製)
5.跨虛擬機器通訊

程式碼怎麼寫?


1.序列化例子

實體類:Student.java

package com.dylan.serialization;

import java.io.Serializable;

/**
 * 學生類
 *
 * @author xusucheng
 * @create 2018-01-08
 **/
public class Student implements Serializable {

    private int id;
    private String name;
    private String gender;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public Student(int id, String name, String gender) {
        this.id = id;
        this.name = name;
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", gender='" + gender + '\'' +
                '}';
    }
}
package com.dylan.serialization;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * 序列化例子
 *
 * @author xusucheng
 * @create 2018-01-08
 **/
public class SerializationDemo {
    public static void main(String[] args) throws IOException{
        //學生1
        Student s1 = new Student(101,"Jack","Male");
        //學生2
        Student s2 = new Student(102,"Lily","Female");

        List<Student> list = new ArrayList<>();
        list.add(s1);
        list.add(s2);

        //建立檔案流
        FileOutputStream fos = new FileOutputStream("D:\\test\\student01.ser");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(list);

        os.close();
        System.out.println("序列化完成!");
    }
}




2.反序列化例子

package com.dylan.serialization;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * 反序列化例子
 *
 * @author xusucheng
 * @create 2018-01-08
 **/
public class DeserializationDemo {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("D:\\test\\student01.ser");
        ObjectInputStream is = new ObjectInputStream(fis);
        Object obj = null;
        List<Student> list = new ArrayList<>();
        try {
            list = (List<Student>)is.readObject();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        is.close();

        //遍歷list,輸出
        for (Student student:list){
            System.out.println(student.toString());
        }
    }
}



3.serialVersionUID的作用

可以看到上面序列化例子中的學生實體類只是實現了Serializable介面,假如我現在接到客戶需求,要調整Student類,增加學生年齡屬性,調整後如下:

public class Student implements Serializable {

    private int id;
    private String name;
    private String gender;
    private int age;
...
此時,我在去執行反序列化程式,結果報錯了:


啥玩意?就是說Stuent類調整後,其序列化版本ID發生了變化,java又是根據這個ID來判斷是不是同一個類。ID不一樣則無法反序列化!

事實上如果一個類沒有定義serialVersionUID,它會自動計算出來並分配給該類。當發生以下情況,這個ID就會改變:

  • 在類中新增一些新的變數。
    將變數從transient轉變為非tansient,對於序列化來說,就像是新加入了一個變數而已
    將變數從靜態的轉變為非靜態的,對於序列化來說,就也像是新加入了一個變數而已

這個時候就需要我們手動指定這個serialVersionUID了,它告訴jvm,嗨,這個學生類我更新了一下,反序列化的時候記得儘量向後相容,別報錯。通常我們有以下幾種方式:

serialVersionUID生成方式:

1)直接寫固定的1L;

2)利用JDK自帶serialver命令:切到類路徑下


3)IDEA生成serialVersionUID

Intellij IDEA 預設沒啟用這個功能

Preferences->Editor->Inspections->Serialization issues->Serializable class without serialVersionUID 勾上
應用之後就開啟了檢測功能

在你的class名前(一定是游標移動到類名前):按(mac:option+return)或者( win:Alt+Enter) 就會提示自動建立 serialVersionUID 了。


加上serialVersionUID後的實體類如下:

public class Student implements Serializable {

    private static final long serialVersionUID = -2110227637642817458L;

    private int id;
    private String name;
    private String gender;
    private int age;
...
再次執行反序列化又恢復正常了:

Student{id=101, name='Jack', gender='Male'}
Student{id=102, name='Lily', gender='Female'}

在這裡有一個建議,如果沒有特殊需求,就是用預設的 1L 就可以,這樣可以確保程式碼一致時反序列化成功。

那麼隨機生成的序列化 ID 有什麼作用呢,有些時候,通過改變序列化 ID 可以用來限制某些使用者的使用。


4.static和transient屬性無法序列化

transient翻譯過來就是短暫的,瞬時的。如果在可序列化的類中某個成員變數加了這個修飾,則說明它只在jvm執行時才儲存值,序列化時不會儲存的。

為了證明這一點,我們調整一下Student類,加入1個靜態變數mess和1個transient變數password。調整後的類如下:

package com.dylan.serialization;

import java.io.Serializable;

/**
 * 學生類
 *
 * @author xusucheng
 * @create 2018-01-08
 **/
public class Student implements Serializable {

    private static final long serialVersionUID = -2110227637642817458L;
    private static String mess="null";

    private int id;
    private String name;
    private String gender;
    private int age;
    private transient String password;

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public Student(int id, String name, String gender) {
        this.id = id;
        this.name = name;
        this.gender = gender;
        mess = "something";
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", gender='" + gender + '\'' +
                ", age=" + age +
                ", password='" + password + '\'' +
                ", mess='" + mess + '\'' +
                '}';
    }
}

再次執行序列化,反序列化後:

結論沒問題。


5.序列化實戰:Socket傳輸序列化物件例子

package com.dylan.serialization;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author xusucheng
 * @create 2018-01-08
 **/
public class Server {
    public static void main(String[] args) {

        //The client is used to handle connections with a client once a connection is
        //established.
        Socket client = null;

        //The following two objects handles our Serialization operations, ObjectOutputStream
        //writes an object to the stream. ObjectInputStream reads an object from the stream.
        ObjectOutputStream out = null;
        ObjectInputStream in = null;

        try {
            ServerSocket server = new ServerSocket(8888);
            client = server.accept();
            out = new ObjectOutputStream(client.getOutputStream());
            in = new ObjectInputStream(client.getInputStream());

            Student student = (Student) in.readObject();
            System.out.println(student);

            // close resources
            out.close();
            in.close();
            client.close();
            server.close();

        } catch (IOException e) {
            e.printStackTrace();
            System.exit(-1);
        }
        catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        }
    }
}

package com.dylan.serialization;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.net.UnknownHostException;

/**
 * @author xusucheng
 * @create 2018-01-08
 **/
public class Client {
    public static void main(String[] args) {
        Socket client = null;
        ObjectOutputStream out = null;
        ObjectInputStream in = null;

        try {

            client = new Socket("127.0.0.1", 8888);
            out = new ObjectOutputStream(client.getOutputStream());
            in = new ObjectInputStream(client.getInputStream());

            Student student = new Student(991,"Dylan","Male");
            out.writeObject(student);
            out.flush();

            out.close();
            in.close();
            client.close();

        } catch (UnknownHostException e) {
            e.printStackTrace();
            System.exit(1);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }

    }
}

說明:假如Student類沒有實現序列化介面,在執行Client程式時會報錯:


正如我們前面所講,網路只能傳輸二進位制資料,Java物件要想傳輸必須先序列化。


執行效果:




以上只是講解了一下序列化的常用知識點,還有些深入的部分,以後有空再總結。





相關文章