1.什麼是序列化和反序列化?
- 序列化: 將java對象轉換成字節序列的過程
- 反序列化:將在序列化過程中所生成的字節序列轉換成java對象的過程
在實際生活中,需要將對象持久化,需要的時候再重新讀取出來,通過對象序列化,可以將對象的狀態保存爲字節數組,需要的時候再將字節數組反序列化爲對象。此外,兩個進程行行遠程通信時,需要互相發送各種類型的數據,比如圖片、視頻、音頻、文字等等,這些數據都是以二進制序列的形式在網絡上傳送。那java進程之間的通信也是如此。通過java序列化和反序列化來實現。即把這個java對象轉換爲字節序列,然後在網絡上傳送,接收方再把字節序列恢復爲java對象的過程。
2.如何實現Java序列化與反序列化?
前提:實現了Serializable或Externalizable接口的類的對象才能被序列化。否則拋出NotSerializableException異常。
有以下三種方式:
- 實現了Serializable接口。使用ObjectOutputStream和ObjcetInputStream對對象的非transient實例變量進行序列化和反序列化。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* @author 旺仔牛奶
* 2017年8月30日
*/
public class ApplyTransient {
public static void main(String[] args) {
User user = new User();
user.setUserPhone("1234567891");
user.setUserName("旺仔牛奶");
user.setPasswd("caka");
System.out.println("Before Serializable: ");
System.out.println("username: " + user.getUserName());
System.out.println("userPhone: " + user.getUserPhone());
System.err.println("password: " + user.getPasswd());
//序列化操作
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("E:/wangzai.txt"));
os.writeObject(user); // 將User對象寫進文件
os.flush();//刷新流到文件
os.close();//關閉流
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
try {
//反序列化操作
ObjectInputStream is = new ObjectInputStream(new FileInputStream("E:/wangzai.txt"));
user = (User) is.readObject(); // 從流中讀取User的數據
is.close();//關閉流
System.out.println("\nAfter Serializable: ");
System.out.println("username: " + user.getUserName());
System.out.println("userPhone: " + user.getPhone());
System.err.println("password: " + user.getPasswd());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
//User實體類
class User implements Serializable {
private static final long serialVersionUID = 8294180014912103005L;
public String userPhone;
public static String userName;
private transient String passwd;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getUserPhone() {
return userPhone;
}
public void setUserPhone(String userPhone) {
this.userPhone = userPhone;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
}
執行結果:
Before Serializable:
username: 旺仔牛奶
userPhone: 1234567891
password: caka
After Serializable:
username: 旺仔牛奶的牛奶
userPhone: 1234567891
password: null
問題:password反序列化的值爲什麼爲空?username反序列化後的值爲什麼被臨時改變的值替換?
上面兩個問題涉及到java序列化需要注意的兩個點:
- 靜態成員變量屬於類不屬於對象,所以不會參與序列化(對象序列化保存的是對象的“狀態”,也就是它的成員變量,因此序列化不會關注靜態變量)
- 用transient關鍵字標記的成員變量不參與序列化(在被反序列化後,transient 變量的值被設爲初始值,如 int 型的是 0,對象型的是 null)
答案是因爲username取出的值不是反序列化得到的而是JVM中得到的,正如上例,如果是反序列化得到的,那麼臨時修改username的值是不會影響反序列化取得的值的。但username的值卻被臨時改變的值替換掉了,所以並不與第一個注意點衝突,username的值能被取出是因爲在JVM中存放。
2.實現了Serializable接口,並且還定義了private類型的writeObject()和readObject()。
- 如果需要默認序列化方法或者默認反序列化方法,則可以在這兩個方法中調用ObjectOutputStream的defaultWriteObject()方法或者ObjectOutputStream的defaultReadObject()方法。也就是上列中默認的序列化方法。
- 如果需要用戶自定義序列化方式,從而控制序列化的行爲。則使用ObjectOutputStream調用對象的writeObject(ObjectOutputStream out)方法進行序列化。用ObjectInputStream調用對象的readObject(ObjectInputStream in)進行反序列化。
例子如下:
import java.io.IOException;
import java.io.ObjectStreamException;
import java.io.Serializable;
/**
* @description
* @author 旺仔牛奶
* 2017年8月30日
*/
public class ApplyTransient implements Serializable {
/**
* 生成序列號標識
*/
private static final long serialVersionUID = -4083503801443301445L;
private int id;
private String name;
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;
}
/**
* 序列化時,
* 首先系統會先調用writeReplace方法,在這個階段,
* 可以進行自己操作,將需要進行序列化的對象換成我們指定的對象.
* 一般很少重寫該方法
* @return
* @throws ObjectStreamException
*/
private Object writeReplace() throws ObjectStreamException {
System.out.println("writeReplace invoked");
return this;
}
/**
*接着系統將調用writeObject方法,
* 來將對象中的屬性一個個進行序列化,
* 我們可以在這個方法中控制住哪些屬性需要序列化.
* 這裏只序列化name屬性
* @param out
* @throws IOException
*/
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
System.out.println("writeObject invoked");
out.writeObject(this.name == null ? "旺仔牛奶" : this.name);
}
/**
* 反序列化時,系統會調用readObject方法,將我們剛剛在writeObject方法序列化好的屬性,
* 反序列化回來.然後通過readResolve方法,我們也可以指定系統返回給我們特定的對象
* 可以不是writeReplace序列化時的對象,可以指定其他對象.
* @param in
* @throws IOException
* @throws ClassNotFoundException
*/
private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException {
System.out.println("readObject invoked");
this.name = (String) in.readObject();
System.out.println("got name:" + name);
}
/**
* 通過readResolve方法,我們也可以指定系統返回給我們特定的對象
* 可以不是writeReplace序列化時的對象,可以指定其他對象.
* 一般很少重寫該方法
* @return
* @throws ObjectStreamException
*/
private Object readResolve() throws ObjectStreamException {
System.out.println("readResolve invoked");
return this;
}
}
3.實現了Externalnalizable接口,且必須實現public類型的readExternal()和writeExternal()方法。
ObjectOutputStream調用對象的writeExternal(ObjectOutput out))的方法進行序列化。ObjectInputStream會調用對象的readExternal(ObjectInput in)的方法進行反序列化。
舉例如下:
import java.io.Externalizable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
/**
* @author 旺仔牛奶
* 2017年8月30日
*/
public class ApplyTransient implements Externalizable {
private static final long serialVersionUID = -4083503801443301445L;
private transient String content = "旺仔牛奶";
@Override public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(content);
}
@Override public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException {
content = (String) in.readObject();
}
public static void main(String[] args) throws Exception {
ApplyTransient applyTransientt = new ApplyTransient();
//序列化
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(new File("wangzai")));
out.writeObject(applyTransientt);
//反序列化
ObjectInput in = new ObjectInputStream(new FileInputStream(new File("wangzai")));
applyTransientt = (ApplyTransient) in.readObject();
System.out.println(applyTransientt.content);
out.close(); in.close();
}
}
注意:
上面的三個例子中都顯式的申明瞭serialVersionUID(唯一的序列版本號),每個可序列化的類都有一個唯一標識號與其相關聯。
如果沒有顯式的指定私有靜態final的long類型的serialVersionUID,系統會自動生成一個標識號(會受類名、接口名、成員的影響而發生變化)。如果一旦改變了這些信息,自動生成的序列版本會發生變化。因此如果沒有顯示聲明,兼容性會遭到破壞,導致運行時出現InvalidClassException異常。所以,強烈建議所有可序列化類都顯式聲明serialVersionUID
顯式地定義serialVersionUID有兩種用途:
1)在某些場合,希望類的不同版本對序列化兼容,因此需要確保類的不同版本具有相同的serialVersionUID;
2)在某些場合,不希望類的不同版本對序列化兼容,因此需要確保類的不同版本具有不同的serialVersionUID
以上三種例子可見,對象序列化和反序列化的步驟如下:
對象序列化步驟:
1.創建一個對象輸出流
2.通過對象輸出流的writeObject(Objectobj)方法寫對象
如:
ObjectOutputStream out = newObjectOutputStream(new FileOutputStream(filePath));//filePath:文件路徑
out.writeObject(obj);//obj:要寫入文件的對象
對象反序列化步驟:
1.創建一個對象輸入流
2.通過對象輸入流的readObject(j)方法讀取對象.(讀取對象時是按照對列方式進行的,既先進先出)
如:
ObjectInputStream in = new ObjectInputStream (newFileInputStream(filePath));
in.readObject();//讀取對象
4.Java實現幾種序列化方式
Java實現幾種序列化方式包括Java原生以流的方法進行的序列化、Json序列化、FastJson序列化、Protobuff序列化。
- Java原生序列化方法即通過Java原生流(InputStream和OutputStream之間的轉化)的方式進行轉化。需要注意的是JavaBean實體類必須實現Serializable接口,否則無法序列化。比如上面的第一個例子。
- Json序列化一般會使用jackson包,通過ObjectMapper類來進行一些操作,比如將對象轉化爲byte數組或者將json串轉化爲對象。比如調用一個服務器接口,通常的請求爲xxx.json?a=xxx&b=xxx的形式。
- fastjson序列化是由阿里開發的一個性能很好的Java
語言實現的 Json解析器和生成器。完全支持java bean、集合、Map、日期、Enum,支持範型和自省。無依賴,使用時候需引入FastJson第三方jar包。
- ProtocolBuffer序列化是一種輕便高效的結構化數據存儲格式,可以用於結構化數據序列化。適合做數據存儲或 RPC 數據交換格式。可用於通訊協議、數據存儲等領域的語言無關、平臺無關、可擴展的序列化結構數據格式。