序列化與反序列化

序列化與反序列化是開發過程當中不可或缺的一步,簡單來講,序列化是將對象轉換成字節流的過程,而反序列化的是將字節流恢復成對象的過程。二者的關係以下:html

序列化與反序列化是一個標準(具體參考XDR:外部數據表示標準 RFC 1014),它是編程語言的一種共性,只是有些編程語言是內置的(如Java,PHP等),有些語言是經過第三方庫來實現的(如C/C++)。前端

使用場景

  • 對象的持久化(將對象內容保存到數據庫或文件中)
  • 遠程數據傳輸(將對象發送給其餘計算機系統)

爲何須要序列化與序列化?

序列化與序列化主要解決的是數據的一致性問題。簡單來講,就是輸入數據與輸出數據是同樣的。java

對於數據的本地持久化,只須要將數據轉換爲字符串進行保存便可是實現,但對於遠程的數據傳輸,因爲操做系統,硬件等差別,會出現內存大小端,內存對齊等問題,致使接收端沒法正確解析數據,爲了解決這種問題,Sun Microsystems在20世紀80年代提出了XDR規範,於1995年正式成爲IETF標準。git

Java中的序列化與反序列化

Java語言內置了序列化和反序列化,經過Serializable接口實現。github

public class Account implements Serializable {

	private int age;
	private long birthday;
	private String name;
}
複製代碼

序列化兼容性

序列化的兼容性指的是對象的結構變化(如增刪字段,修改字段,字段修飾符的改變等)對序列化的影響。爲了可以識別對象結構的變化,Serializable使用serialVersionUID字段來標識對象的結構。默認狀況下,它會根據對象的數據結構自動生成,結構發生變化後,它的值也會跟隨變化。虛擬機在反序列化的時候會檢查serialVersionUID的值,若是字節碼中的serialVersionUID和要被轉換的類型的serialVersionUID不一致,就沒法進行正常的反序列化。算法

示例:將Account對象保存到文件中,而後在Account類中添加address字段,再從文件中讀取以前保存的內容。數據庫

// 將Account對象保存到文件中
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(account);
oos.flush();

// 修改Account對象的結構
public class Account implements Serializable {

	private int age;
	private long birthday;
	private String name;
	private String address;
	
	public Account(int age, String name) {
	    this.age = age;
	    this.name = name;
	}
}   

// 讀取Account的內容
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
Account account2 = (Account)ois.readObject();
複製代碼

因爲在保存Account對象後修改了Account的結構,會致使serialVersionUID的值發生變化,在讀文件(反序列化)的時候就會出錯。因此爲了更好的兼容性,在序列化的時候,最好將serialVersionUID的值設置爲固定的。編程

public class Account implements Serializable {

    private static final long serialVersionUID = 1L;
    
    private int age;
    private long birthday;
    private String name;
}
複製代碼

序列化的存儲規則

Java中的序列化在將對象持久化(序列化)的時候,爲了節省磁盤空間,對於相同的對象會進行優化。當屢次保存相同的對象時,其實保存的只是第一個對象的引用。json

// 將account對象保存兩次,第二次保存時修改其用戶名
Account account = new Account("Freeman");
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(account);
System.out.println("fileSize=" +file.length());
account.setUserName("Tom");
oos.writeObject(account);
System.out.println("fileSize=" +file.length());

// 讀取兩次保存的account對象
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
Account account2 = (Account)ois.readObject();
Account account3 = (Account)ois.readObject();
System.out.println("account2.name=" + account2.getUserName() + "\n  account3.name=" + account3.getUserName() + "\naccount2==account3 -> " + account2.equals(account3));
複製代碼

輸出結果:後端

account2.name=Freeman  
account3.name=Freeman 
account2==account3 -> true
複製代碼

因此在對同一個對象進行屢次序列化的時候,最好經過clone一個新的對象再進行序列化。

序列化對單例的影響

反序列化的時候,JVM會根據序列化生成的內容構造新的對象,對於實現了Serializable的單例類來講,這至關於開放了構造方法。爲了保證單例類實例的惟一性,咱們須要重寫resolveObject方法。

/**
 * 在反序列化的時候被調用
 * @return 返回根據字節碼建立的新對象
 * @throws ObjectStreamException
 */
private Object readResolve()throws ObjectStreamException {
    return instance;
}
複製代碼

控制序列化過程

雖然直接使用Serializable很方便,但有時咱們並不想序列化全部的字段,如標識選中狀態的isSelected字段,涉及安全問題的password字段等。此時可經過經過如下方法實現:

  1. 給不想序列化的字段添加static或transient修飾詞:

Java中的序列化保存的只是對象的成員變量,既不包括static成員(static成員屬於類),也不包括成員方法。同時Java爲了讓序列化更靈活,提供了transient關鍵字,用來關閉字段的序列化。

public class Account implements Serializable {

    private static final long serialVersionUID = 1L;

    private String userName;
    private static String idcard;
    private transient String password;
}
複製代碼
  1. 直接使用Externalizable接口控制序列化過程:

Externalizable也是Java提供的序列化接口,與Serializable不一樣的是,默認狀況下,它不會序列化任何成員變量,全部的序列化,反序列化工做都須要手動完成。

public class Account implements Externalizable {

    private static final long serialVersionUID = 1L;
    
	private String userName;
	private String idcard;
	private String password;
	
	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		out.writeObject(userName);
	}
	
	@Override
	public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
		userName = (String) in.readObject();
	}
}
複製代碼
  1. 本身實現序列化/反序列化過程

    public class Account implements Serializable {

    private static final long serialVersionUID = 1L;
     
     private String userName;
     private transient String idcard;
     private String password;
     
     private void writeObject(ObjectOutputStream oos)throws IOException {
     	// 調用默認的序列化方法,序列化非transient/static字段
     	oos.defaultWriteObject();
     	oos.writeObject(idcard);
     }
     
     private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
     	// 調用默認的反序列化方法,發序列化非transient/static字段
     	ois.defaultReadObject();
     	idcard = (String)ois.readObject();
     }
    複製代碼

    }

關於Java序列化算法的詳細介紹可參考:Java序列化算法透析

Java序列化注意事項

  1. 經過Serializable序列化的對象,在反序列化的時候,直接根據字節碼構造對象,並不會調用對象的構造方法;
  2. 經過Serializable序列化子類時,若是父類沒有實現Serializable接口,那麼父類須要提供默認的構造方法,不然在反序列化的時候拋出java.io.NotSerializableException異常;
  3. 經過Externalizale實現序列化時,反序列化的時候須要調用對象的默認構造方法;
  4. 因爲Externalizale默認狀況下不會對任何成員變量進行序列化,因此transient關鍵字只能在Serializable序列化方式中使用;

數據交換協議

序列化與反序列化爲數據交換提供了可能,可是由於傳遞的是字節碼,可讀性差。在應用層開發過程當中不易調試,爲了解決這種問題,最直接的想法就是將對象的內容轉換爲字符串的形式進行傳遞。具體的傳輸格式可自行定義,但自定義格式有一個很大的問題——兼容性,若是引入其餘系統的模塊,就須要對數據格式進行轉換,維護其餘的系統時,還要先了解一下它的序列化方式。爲了統一數據傳輸的格式,出現了幾種數據交換協議,如:JSON, Protobuf,XML。這些數據交換協議可視爲是應用層面的序列化/反序列化。

JSON

JSON(JavaScript Object Notation)是一種輕量級,徹底獨立於語言的數據交換格式。目前被普遍應用在先後端的數據交互中。

語法

JSON中的元素都是鍵值對——key:value形式,鍵值對之間以":"分隔,每一個鍵需用雙引號引發來,值的類型爲String時也須要雙引號。其中value的類型包括:對象,數組,值,每種類型具備不一樣的語法表示。

對象

對象是一個無序的鍵值對集合。以"{"開始,以"}"結束, 每一個成員以","分隔。例如:

"value" : {
    "name": "Freeman",
    "gender": 1
}
複製代碼
數組

數組是一個有序的集合,以"["開始,以"]"結束,成員之間以","分隔。例如:

"value" : [
    {
        "name": "zhangsan",
        "gender": 1
    },
    {
        "name": "lisi",
        "gender": 2
    }
]
複製代碼

值類型表示JSON中的基本類型,包括String,Number(byte, short, int, long, float, double), boolean。

"name": "Freeman"
"gender": 1
"registered": false
"article": null
複製代碼

==注意==:對象,數組,值這三種元素可互相嵌套!

{
    "code": 1,
    "msg": "success",
    "data": [
        {
            "name": "zhangsan",
            "gender": 1
        },
        {
            "name": "lisi",
            "gender": 2
        }
    ]
}
複製代碼

對於JSON,目前流行的第三方庫有Gson, fastjson:關於Gson的詳細介紹,參考Gson使用教程

Protobuf

Protobuf是Google實現的一種與語言無關,與平臺無關,可擴展的序列化方式,比XML更小,更快,使用更簡單。

Protobuf具備很高的效率,而且幾乎爲主流的開發語言都提供了支持,具體參考Protobuf開發文檔

在Android中使用Protobuf,須要protobuf-gradle-plugin插件,具體使用查看其項目說明。

XML

XML(Extensible Markup Language)可擴展標記語言,經過標籤描述數據。示例以下:

<?xml version="1.0" encoding="UTF-8"?>
<person>
    <name>Freeman</name>
    <gender>1</gender>
</person>
複製代碼

使用這種方式傳輸數據時,只須要將對象轉換成這種標籤形式,在接收到數據後,將其轉換成相應的對象。

關於JAVA開發中對XML的解析可參考四種生成和解析XML文檔的方法詳解

數據交換協議如何選擇

從性能,數據大小,可讀性三方面進行比較,結果以下:

協議 性能 數據大小 可讀性
JSON
Protobuf
XML

對於數據量不是很大,實時性不是特別高的交互,JSON徹底能夠知足要求,畢竟它的可讀性高,出現問題容易定位(注:它是目前前端,app和後端交換數據使用的主流協議)。而對於實時性要求很高,或數據量大的場景,可以使用Protobuf協議。具體數據交換協議的比較可參考github.com/eishay/jvm-…

相關文章
相關標籤/搜索