問:說說你對(duì) Java 的 transient 關(guān)鍵字理解贾节?
答:對(duì)于不需要被序列化的屬性就可以通過加上 transient 關(guān)鍵字來處理火欧。一旦屬性被 transient 修飾就不再是對(duì)象持久化的一部分,該屬性的內(nèi)容在序列化后將無法獲得訪問振劳,transient 關(guān)鍵字只能修飾屬性變量成員而不能修飾方法和類(注意局部變量是不能被 transient 關(guān)鍵字修飾的)椎组,屬性成員如果是引用類型也需要保證實(shí)現(xiàn) Serializable 接口;此外在 Java 中對(duì)象的序列化可以通過實(shí)現(xiàn)兩種接口來實(shí)現(xiàn)历恐,若實(shí)現(xiàn)的是 Serializable 接口則所有的序列化將會(huì)自動(dòng)進(jìn)行寸癌,若實(shí)現(xiàn)的是 Externalizable 接口則沒有任何東西可以自動(dòng)序列化,需要在 writeExternal 方法中進(jìn)行手工指定所要序列化的變量弱贼,這與是否被 transient 修飾無關(guān)蒸苇。
問:對(duì)于 transient 修飾的屬性如何在不刪除修飾符的情況下讓其可以序列化?
答:本題其實(shí)就是在考察實(shí)現(xiàn) Serializable 接口情況下通過 writeObject() 與 readObject()方法進(jìn)行自定義序列化的機(jī)制吮旅。具體實(shí)現(xiàn)如下:
public class Item {
public String name;
public String id;
public School() {
}
public School(String name, String id) {
this.name = name;
this.id = id;
}
}
public class Info implements Serializable {
...
transient private Item item = null;
...
private void writeObject(ObjectOutputStream out) throws IOException {
//invoke default serialization method
out.defaultWriteObject();
if (item == null) {
Item = new Item();
}
out.writeObject(item.name);
out.writeObject(item.id);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
//invoke default serialization method
in.defaultReadObject();
String name = (String) in.readObject();
String id = (String) in.readObject();
item = new Item(name, id);
}
}
上面在 writeObject() 方法中先調(diào)用了 ObjectOutputStream 的 defaultWriteObject() 方法溪烤,該方法會(huì)執(zhí)行默認(rèn)的序列化機(jī)制(忽略 item 字段),然后再調(diào)用 writeXXX() 方法顯示地將每個(gè)字段寫入到 ObjectOutputStream 中;readObject() 方法的作用是對(duì)象的讀取檬嘀,其原理與 writeObject() 方法相同槽驶。必須要注意的是 writeObject() 與 readObject() 都是 private 方法,其在 ObjectOutputStream 的 writeSerialData() 方法和 ObjectInputStream 的 readSerialData() 方法中通過反射進(jìn)行調(diào)用鸳兽。
問:簡單說說 Externalizable 與 Serializable 有什么區(qū)別掂铐?
答:使用 transient 還是用 writeObject() 和 readObject() 方法都是基于 Serializable 接口的序列化;JDK 提供的另一個(gè)序列化接口 Externalizable 繼承自 Serializable贸铜,使用該接口后基于 Serializable 接口的序列化機(jī)制就會(huì)失效(包括 transient堡纬,因?yàn)?Externalizable 不會(huì)主動(dòng)序列化),當(dāng)使用該接口時(shí)序列化的細(xì)節(jié)需要由我們自己去實(shí)現(xiàn)蒿秦,另外使用 Externalizable 主動(dòng)進(jìn)行序列化時(shí)當(dāng)讀取對(duì)象時(shí)會(huì)調(diào)用被序列化類的無參構(gòu)方法去創(chuàng)建一個(gè)新的對(duì)象烤镐,然后再將被保存對(duì)象的字段值分別填充到新對(duì)象中,所以實(shí)現(xiàn) Externalizable 接口的類必須提供一個(gè)無參 public 的構(gòu)造方法棍鳖。關(guān)于 Externalizable 的實(shí)例如下:
public class Info implements Externalizable {
private String name;
private int age;
public Info() {
}//必須定義無參構(gòu)造方法
public Info(String name, int age) {
this.name = name;
this.age = age;
} //實(shí)現(xiàn)此方法反序列化時(shí)使用
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = (String) in.readObject();
this.age = in.readInt();
} //實(shí)現(xiàn)此方法序列化時(shí)使用
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(this.name);
out.writeInt(this.age);
}
}
特別注意使用 Externalizable 方式時(shí)必須提供無參構(gòu)造方法炮叶,且 readExternal 方法必須按照與 writeExternal 方法寫入值時(shí)相同的順序和類型來讀取屬性值。
問:Serializable 序列化中自定義 readObjectNoData() 方法有什么作用渡处?
答:這個(gè)方法主要用來保證通過繼承擴(kuò)容后對(duì)老版本的兼容性镜悉,適用場景如下:比如類 Person 被序列化到硬盤后存儲(chǔ)為文件 old.txt,接著 Person 被修改繼承自 Animal医瘫,為了保證用新的 Person 反序列化老版本 old.txt 文件且 Animal 類的成員有默認(rèn)值則可以在 Animal 類中定義 readObjectNoData 方法返回成員的默認(rèn)值侣肄,具體可以參見 ObjectInputStream 類中的 readSerialData 方法判斷。
問:Java 序列化中 writeReplace() 方法有什么作用醇份?
答:Serializable 除了提供 writeObject 和 readObject 標(biāo)記方法外還提供了另外兩個(gè)標(biāo)記方法可以實(shí)現(xiàn)序列化對(duì)象的替換(即 writeReplace 和 readResolve)稼锅,序列化類一旦實(shí)現(xiàn)了 writeReplace 方法后則在序列化時(shí)就會(huì)先調(diào)用 writeReplace 方法將當(dāng)前對(duì)象替換成另一個(gè)對(duì)象(該方法會(huì)返回替換后的對(duì)象),接著系統(tǒng)將再次調(diào)用另一個(gè)對(duì)象的 writeReplace 方法僚纷,直到該方法不再返回另一個(gè)對(duì)象為止矩距,程序最后將調(diào)用該對(duì)象的 writeObject() 方法來保存該對(duì)象的狀態(tài)。通過下面例子可以說明上面這段話(AdapterBean 只是用來說明問題怖竭,實(shí)際應(yīng)用中可能是轉(zhuǎn)為 Map 或者列表等其他結(jié)構(gòu)):
class AdapterBean implements Serializable {
private String name;
private int age;
public AdapterBean(String name, int age) {
this.name = name;
this.age = age;
}
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
System.out.println("AdapterBean writeObject.");
}
private void readObject(java.io.ObjectInputStream in) throws Exception {
in.defaultReadObject();
System.out.println("AdapterBean readObject.");
}
}
class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
System.out.println("Person writeObject.");
}
private void readObject(java.io.ObjectInputStream in) throws Exception {
in.defaultReadObject();
System.out.println("Person readObject.");
}
private Object writeReplace() throws ObjectStreamException {
System.out.println("Person writeReplace.");
return new AdapterBean(name, age);
}
}
public class Main {
public static void main(String[] args) throws IOException, Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.serial"));
Person p = new Person("工匠若水", 27);
oos.writeObject(p);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.serial"));
System.out.println(((AdapterBean) ois.readObject()).toString());
}
}
上面程序的輸出結(jié)果如下:
Person writeReplace.
AdapterBean writeObject.
AdapterBean readObject.
AdapterBean@24459efb
特別說明锥债,實(shí)現(xiàn)了 writeReplace 的序列化類就不要再實(shí)現(xiàn) writeObject 了,因?yàn)樵擃惖?writeObject 方法就不會(huì)被調(diào)用了痊臭;實(shí)現(xiàn) writeReplace 的返回對(duì)象必須是可序列話的對(duì)象哮肚;通過 writeReplace 序列化替換的對(duì)象在反序列化中無論實(shí)現(xiàn)哪個(gè)方法都是無法恢復(fù)原對(duì)象的(即對(duì)象被徹底替換了),也就是說使用 ObjectInputStream 讀取的對(duì)象只能是被替換后的對(duì)象广匙,要想恢復(fù)只能在讀取后自己手動(dòng)構(gòu)造恢復(fù)绽左;所以 writeObject 只和 readObject 配合使用,一旦實(shí)現(xiàn)了 writeReplace 在寫入時(shí)進(jìn)行替換就不再需要 writeObject 和 readObject 了艇潭,故替換就是徹底的自定義了,比 writeObject 和 readObject 自定義更徹底。
問:Java 序列化中 readResolve() 方法有什么作用蹋凝?
答:同上 Serializable 除過提供了 writeObject 和 readObject 標(biāo)記方法外還提供了另外兩個(gè)標(biāo)記方法可以實(shí)現(xiàn)序列化對(duì)象的替換(即 writeReplace 和 readResolve)鲁纠,readResolve() 方法可以實(shí)現(xiàn)保護(hù)性復(fù)制整個(gè)對(duì)象,緊挨著序列化類實(shí)現(xiàn)的 readObject() 之后被調(diào)用鳍寂,該方法的返回值會(huì)代替原來反序列化的對(duì)象改含,而原來序列化類中 readObject() 反序列化的對(duì)象將會(huì)立即丟棄。readObject() 方法在序列化單例類時(shí)尤其有用迄汛,單例序列化都應(yīng)該提供 readResolve() 方法捍壤,這樣才可以保證反序列化的對(duì)象依然正常。同理給個(gè)直觀例子如下:
final class Singleton implements Serializable {
private Singleton() {
}
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
}
public class Main {
public static void main(String[] args) throws IOException, Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.serial"));
Singleton p = Singleton.getInstance();
oos.writeObject(p);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.serial"));
Singleton p1 = (Singleton) ois.readObject();
System.out.println(p == p1);
}
}
上面代碼運(yùn)行結(jié)果為 true鞍爱,如果去掉 readResolve 實(shí)現(xiàn)則結(jié)果為 false鹃觉。
所以 readResolve 方法可以保護(hù)性恢復(fù)對(duì)象(同時(shí)也可以替換對(duì)象),調(diào)用該方法之前會(huì)先調(diào)用序列化類的 readObject 反序列化得到對(duì)象睹逃,在該方法中可以正常通過 this 訪問到剛才反序列化得到的對(duì)象內(nèi)容盗扇,然后可以根據(jù)這些內(nèi)容進(jìn)行一定處理返回一個(gè)對(duì)象,所以其最重要的應(yīng)用就是保護(hù)性恢復(fù)單例對(duì)象(當(dāng)然使用枚舉類的單例就天生支持此特性)沉填。
問:Java 序列化存儲(chǔ)傳輸為什么不安全疗隶?怎么解決?
答:因?yàn)樾蛄谢M(jìn)制格式完全編寫在文檔中且完全可逆翼闹,所以只需將二進(jìn)制序列化流的內(nèi)容轉(zhuǎn)儲(chǔ)到控制臺(tái)就可以看清類及其包含的內(nèi)容斑鼻,故序列化對(duì)象中的任何 private 字段幾乎都是以明文的方式出現(xiàn)在序列化流中,如果我們傳輸?shù)男蛄谢瘮?shù)據(jù)中途被截獲猎荠,截獲方通過反序列化就可以獲得里面的數(shù)據(jù)(敏感數(shù)據(jù)的泄露)坚弱,甚至對(duì)里面的數(shù)據(jù)進(jìn)行修改然后發(fā)送給接收方(無法確保數(shù)據(jù)來源的安全性),從而產(chǎn)生了序列化安全問題法牲。
要解決序列化安全問題的核心原理就是避免在序列化中傳遞敏感數(shù)據(jù)史汗,所以可以使用關(guān)鍵字 transient 修飾敏感數(shù)據(jù)的變量,或者通過自定義序列化相關(guān)流程對(duì)數(shù)據(jù)進(jìn)行簽名加密機(jī)制再存儲(chǔ)或者傳輸(最簡單譬如女生年齡可以在序列化時(shí)進(jìn)行移位操作拒垃,反序列化時(shí)進(jìn)行反向移位復(fù)原操作停撞,或者使用一些加密算法處理)。