Java序列化與反序列化中,你可能會忽略的細節知識點

前言

在很早之前學習序列化的時候有寫過一篇關於Java序列化的博客,不過那只是簡單的使用,入門者歡迎移步:http://blog.beifengtz.com/article/36。上週在工作時遇到了一個序列化的問題,就是父子類序列化對其值的保存問題,關於序列化有很多細節知識,這篇文章就仔細學習一下Java中的序列化吧。

一、爲什麼要序列化

現在企業中的系統大多都不是單語言編寫的,一個平臺可能有Java、Python、Cpp、Lua等語言編寫而成,如果在其內部或者這個平臺與其他平臺進行數據交互時,必須要有統一的數據格式,各個語言的數據必須經過序列化成這些統一格式後才能被其他系統所識別,一般序列化的結果是一個二進制數據,當接收方系統收到這個二進制數據後必須經過反序列化轉換成自己能識別的數據。當然除了網絡傳輸外,序列化也是一種持久化的手段,你可以序列化成一個二進制文件來進行數據存儲。多語言支持的序列化格式常見的有XML、JSON、ProtoBuf等。

Java語言中也有自己支持的序列化方式,一般使用序列化都是在對象持久化中,網絡傳輸更多的是使用上面所說的那三種常見的序列化格式。

二、先看一個Demo

序列化的對象:

/**
 * @author beifengtz
 * <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
 * <p>location: serializ.javase_learning</p>
 * Created in 10:41 2019/10/13
 */
public class User implements Serializable {

    private static final long serialVersionUID = -6849794470754667710L;

    private String name;
    private transient String gender;
    private int age;
    private long regTime;

    public User(String name, String gender, int age, long regTime) {
        this.name = name;
        this.gender = gender;
        this.age = age;
        this.regTime = regTime;
    }

    public User(String name, int age, long regTime) {
        this.name = name;
        this.age = age;
        this.regTime = regTime;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", gender='" + gender + '\'' +
                ", age=" + age +
                ", regTime=" + regTime +
                '}';
    }
}

序列化與反序列化:

/**
 * @author beifengtz
 * <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
 * <p>location: serialization.javase_learning</p>
 * Created in 10:46 2019/10/13
 */
public class SerializationDemo {
    public static void main(String[] args) {
        User user = new User("beifengtz", "男", 100, System.currentTimeMillis() / 1000);

        System.out.println("序列化之前:");
        System.out.println(user);

        //  將對象序列化進文件
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("serFile"));
            oos.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                assert oos != null;
                oos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        //  從文件中反序列化成對象
        File file = new File("serFile");
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(file));
            User serUser = (User) ois.readObject();
            System.out.println("序列化之後:");
            System.out.println(serUser);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                assert ois != null;
                ois.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

最後控制檯輸出的結果:

序列化之前:
User{name='beifengtz', gender='男', age=100, regTime=1570935427}
序列化之後:
User{name='beifengtz', gender='null', age=100, regTime=1570935427}

然後我們再看中間生成的序列化文件serFile,直接打開之後是亂碼,用二進制編輯器打開裏面全是16進制字符。

三、序列化的ID

序列化方和反序列化方要想成功進行一次數據傳輸,必須要保證三個條件:

  1. 類全路徑必須一樣,比如都是com.beifengtz.User
  2. 類功能代碼必須一樣
  3. 序列化ID必須一樣,比如都是private static final long serialVersionUID = 1L

以上三個條件缺少一個均無法成功序列化,其中如果沒有定義序列化ID虛擬機會隨機生成一個ID,但是這樣對於程序的可控性並不高,畢竟序列化是服務於多端的。下面舉一個IBM Developer中一篇文章舉的例子,在實際應用中的使用案例:

Facade模式中,Facade Object是爲應用程序提供統一的訪問接口,案例程序中的 Client 客戶端使用了該模式,案例程序結構圖如圖所示:

https://www.ibm.com/developerworks/cn/java/j-lo-serial/image003.gif

Client 端通過 Façade Object 纔可以與業務邏輯對象進行交互。而客戶端的 Façade Object 不能直接由 Client 生成,而是需要 Server 端生成,然後序列化後通過網絡將二進制對象數據傳給 Client,Client 負責反序列化得到 Façade 對象。該模式可以使得 Client 端程序的使用需要服務器端的許可,同時 Client 端和服務器端的 Façade Object 類需要保持一致。當服務器端想要進行版本更新時,只要將服務器端的 Façade Object 類的序列化 ID 再次生成,當 Client 端反序列化 Façade Object 就會失敗,也就是強制 Client 端從服務器端獲取最新程序。

四、父子類序列化

  1. 序列化時,只對對象的狀態進行保存,而不管對象的方法;
  2. 父類實現Serializable,子類自動實現序列化,當序列化子類時,父類的屬性值也會被保存,因此子類無需顯示實現Serializable;
  3. 父類未實現Serializable,子類實現Serializable,當序列化子類時,父類的屬性值不會被保存,並且父類必須有無參構造(因爲反序列化時不存在父類屬性值,實例化對象時只有子類屬性值)。

這裏就不寫實際的例子了,有興趣可以自行去驗證。

五、自定義序列化

在序列化過程中,虛擬機會試圖調用對象類裏的 writeObject 和 readObject 方法,進行用戶自定義的序列化和反序列化,如果沒有這樣的方法,則默認調用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程,比如可以在序列化的過程中動態改變序列化的數值。

可以看看下面這個樣例:

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

    private String account = "beifengtz";
    private String password = "123456";

    public String getAccount() {
        return account;
    }

    public String getPassword() {
        return password;
    }

    private void readObject(ObjectInputStream in) {
        try {
            System.out.println("------開始反序列化------");
            ObjectInputStream.GetField readField = in.readFields();
            Object obj = readField.get("password", "");
            System.out.println("要解密的密碼:" + obj);
            password = String.valueOf(Integer.parseInt((String) obj) / 1000);
            System.out.println("解密後的密碼:" + password);
            System.out.println("------反序列化結束------");
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    private void writeObject(ObjectOutputStream out) {
        try {
            System.out.println("------開始序列化------");
            ObjectOutputStream.PutField putField = out.putFields();
            String encrypt = String.valueOf(Integer.parseInt(password) * 1000);
            putField.put("password", encrypt);
            putField.put("account", account);
            System.out.println("真實密碼:" + password);
            System.out.println("加密後的密碼:" + encrypt);
            out.writeFields();
            System.out.println("------序列化結束-----");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        try {

            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serFile"));
            out.writeObject(new Test());
            out.close();

            ObjectInputStream in = new ObjectInputStream(new FileInputStream("serFile"));
            Test test = (Test) in.readObject();
            in.close();
            System.out.println("反序列化後接收到的密碼:" + test.getPassword());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

看結果

------開始序列化------
真實密碼:123456
加密後的密碼:123456000
------序列化結束-----
------開始反序列化------
要解密的密碼:123456000
解密後的密碼:123456
------反序列化結束------
反序列化後接收到的密碼:123456

六、多對象序列化的存儲

對於JDK的序列化並不是簡單的二進制文本追加存儲,而是有一些優化的。其多對象序列化存儲方式如下:

  1. 如果多次存儲的對象是不同類的對象,序列化後的二進制內容直接追加在文本中;
  2. 如果多次存儲的對象是同一個類的同一個對象,並且其屬性完全相同,在第一次寫入二進制之後,後面的序列化內容僅僅保存引用和控制信息,其餘相同信息複用,並非文本追加;
  3. 如果多次存儲的對象是同一個類的同一個對象,但是在多次寫入期間有改動其對象內容,虛擬機根據引用關係知道已經有一個相同對象已經寫入文件,僅保存第一次寫入的對象,第一次序列化之後的對象修改無法被保存;
  4. 如果多次存儲的對象是同一個類的不同對象,在序列化時也會複用類信息,僅保存這不同對象的不同屬性的引用和控制信息,相同屬性複用。

看一下下面四種不同場景,你是否有遇到過?

以下四種場景都基於下面兩個類來進行序列化測試:

Test1 類

public class Test1 implements Serializable {
    private Integer value;

    public Test1(Integer value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Test1{" +
                "value=" + value +
                '}';
    }
}

Test2 類

public class Test2 implements Serializable {
    private String content;

    public Test2(String content) {
        this.content = content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return "Test2{" +
                "content='" + content + '\'' +
                '}';
    }
}

6.1 多次寫入同一個類的同一個對象

樣例:

public static void main(String[] args) {
    try {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serFile"));
        Test2 test = new Test2("1");
        out.writeObject(test);
        out.flush();
        System.out.println("二進制文件長度:" + new File("serFile").length());
        out.writeObject(test);
        out.flush();
        out.close();
        System.out.println("二進制文件長度:" + new File("serFile").length());
    } catch (IOException e) {
        e.printStackTrace();
    }


    try {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("serFile"));
        Test2 test_1 = (Test2) in.readObject();
        Test2 test_2 = (Test2) in.readObject();

        System.out.println(test_1);
        System.out.println(test_2);
        System.out.println(test_1 == test_2);
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }
}

執行結果:

二進制文件長度:75
二進制文件長度:80
Test2{content='1'}
Test2{content='1'}
true

6.2 多次寫入同一個類的同一個對象(先後修改屬性)

樣例:

public static void main(String[] args) {
    try {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serFile"));
        Test2 test = new Test2("1");
        out.writeObject(test);
        out.flush();
        System.out.println("二進制文件長度:" + new File("serFile").length());
        test.setContent("2");   //  修改test屬性內容
        out.writeObject(test);
        out.flush();
        out.close();
        System.out.println("二進制文件長度:" + new File("serFile").length());
    } catch (IOException e) {
        e.printStackTrace();
    }


    try {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("serFile"));
        Test2 test_1 = (Test2) in.readObject();
        Test2 test_2 = (Test2) in.readObject();

        System.out.println(test_1);
        System.out.println(test_2);
        System.out.println(test_1 == test_2);
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }
}

執行結果:

二進制文件長度:75
二進制文件長度:80
Test2{content='1'}
Test2{content='1'}
true

6.3 多次寫入同一個類的不同對象

樣例:

public static void main(String[] args) {
    try {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serFile"));
        out.writeObject(new Test2("1"));
        out.flush();
        System.out.println("二進制文件長度:" + new File("serFile").length());
        out.writeObject(new Test2("2"));
        out.flush();
        out.close();
        System.out.println("二進制文件長度:" + new File("serFile").length());
    } catch (IOException e) {
        e.printStackTrace();
    }


    try {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("serFile"));
        Test2 test_1 = (Test2) in.readObject();
        Test2 test_2 = (Test2) in.readObject();

        System.out.println(test_1);
        System.out.println(test_2);
        System.out.println(test_1 == test_2);
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }
}

執行結果:

二進制文件長度:75
二進制文件長度:85
Test2{content='1'}
Test2{content='2'}
false

6.4 多次寫入不同類的對象

樣例:

public static void main(String[] args) {
    try {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serFile"));
        out.writeObject(new Test1(1));
        out.flush();
        System.out.println("二進制文件長度:" + new File("serFile").length());
        out.writeObject(new Test2("2"));
        out.flush();
        out.close();
        System.out.println("二進制文件長度:" + new File("serFile").length());
    } catch (IOException e) {
        e.printStackTrace();
    }


    try {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("serFile"));
        Test1 test_1 = (Test1) in.readObject();
        Test2 test_2 = (Test2) in.readObject();

        System.out.println(test_1);
        System.out.println(test_2);
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }
}

執行結果:

二進制文件長度:147
二進制文件長度:218
Test1{value=1}
Test2{content='2'}

Java序列化知識總結

  1. 需要被序列化的對象必須實現Serializable接口,這是一個聲明式接口,無任何屬性和方法,如果不實現該接口會報錯:NotSerializableException
  2. 通過ObjectOutputStream和ObjectInputStream對對象進行序列化及反序列化
  3. 被transient關鍵字修飾的屬性值不會被保存進序列化文件,故反序列化後的屬性值是變量類型的默認值。比如這裏String的gender就是null
  4. 序列化不保存靜態變量
  5. 虛擬機是否允許反序列化,不僅取決於類路徑和功能代碼是否一致,一個非常重要的一點是兩個類的序列化 ID 是否一致(private static final long serialVersionUID)
  6. 對於多對象序列化的存儲,並非簡單的二進制內容追加,虛擬機對其有一定的優化,可減少磁盤空間佔用或網絡傳輸內容大小
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章