面試長(zhǎng)知識(shí)了!Java 關(guān)鍵字 transient 竟然還能這么用
前言
最近在看 HashMap 源代碼的時(shí)候,發(fā)現(xiàn)鏈表 table 數(shù)組采用了transient 關(guān)鍵字,筆者當(dāng)時(shí)感覺(jué)對(duì) transient 關(guān)鍵字即陌生但又有似曾相識(shí),所以花了一些時(shí)間簡(jiǎn)要的總結(jié)了下使用transient 關(guān)鍵字的一些基本常識(shí),希望對(duì)你們也有些幫助,讓我們一起進(jìn)步,一起牛逼吧。

transient 關(guān)鍵字的定義
說(shuō)起 transient 關(guān)鍵字,不得不提對(duì)象的 序列化 的,因?yàn)槲覀兂3P枰诰W(wǎng)絡(luò)上以對(duì)象(數(shù)據(jù))的二進(jìn)制方式傳輸數(shù)據(jù),這里涉及到發(fā)送方序列化對(duì)象,接收方反序列化對(duì)象的過(guò)程。
那什么是序列化/反序列化?
“
Java中對(duì)象的序列化指的是將對(duì)象轉(zhuǎn)換成以字節(jié)序列的形式來(lái)表示,這些字節(jié)序列包含了對(duì)象的數(shù)據(jù)和信息,一個(gè)序列化后的對(duì)象可以被寫到數(shù)據(jù)庫(kù)或文件中,也可用于網(wǎng)絡(luò)傳輸。一般地,當(dāng)我們使用緩存cache(內(nèi)存空間不夠有可能會(huì)本地存儲(chǔ)到硬盤)或遠(yuǎn)程調(diào)用rpc(網(wǎng)絡(luò)傳輸)的時(shí)候,經(jīng)常需要讓實(shí)體類實(shí)現(xiàn)Serializable接口,目的就是為了讓其可序列化。當(dāng)然,序列化后的最終目的是為了反序列化,恢復(fù)成原先的Java對(duì)象實(shí)例。所以序列化后的字節(jié)序列都是可以恢復(fù)成Java對(duì)象的,這個(gè)過(guò)程就是反序列化。
在對(duì)象的序列化/反序列化過(guò)程中,我們經(jīng)常有這種需求,就是非必要字段不必進(jìn)行序列化。
例如有一個(gè)對(duì)象有三個(gè)字段 field1、field2、field3,發(fā)送方不想讓字段 field3 被序列化,因?yàn)檫@里面可能涉及到一些敏感信息不想被接收方知道,那有沒(méi)有辦法解決這個(gè)問(wèn)題呢?
其實(shí)聰明的 Java 作者早就為我們量身定做了 transient 關(guān)鍵字!
簡(jiǎn)單來(lái)說(shuō),被 transient 關(guān)鍵字修飾過(guò)的成員屬性不能被序列化,transient 關(guān)鍵字只能修飾變量,而不能修飾方法和類。
transient 關(guān)鍵字的約定
約定一、只能修飾變量而不能修飾方法和類。注意本地變量是不能被
transient關(guān)鍵字修飾的。約定二、被
transient關(guān)鍵字修飾過(guò)的屬性不能被序列化,也就是說(shuō)被transient修飾過(guò)的屬性,在對(duì)對(duì)象序列化后,是無(wú)法訪問(wèn)到該屬性的。約定三、靜態(tài)變量不管有無(wú)被
transient修飾過(guò),不能被序列化。
transient 關(guān)鍵字的使用場(chǎng)景
首先,我們看個(gè)例子,有個(gè)產(chǎn)品對(duì)象 Product,包括價(jià)格、數(shù)量、總價(jià)三個(gè)字段,那么總價(jià)可以通過(guò) 價(jià)格 乘以 數(shù)量 推導(dǎo)出來(lái)。
我們以查詢某個(gè)產(chǎn)品 API 接口為例,通過(guò)產(chǎn)品 ID,查詢返回一個(gè)產(chǎn)品對(duì)象。
public?class?Product?{
?private?int?amounts;
?private?int?price;
?private?int?sum;
}
通過(guò) Gson 序列化后把 json 數(shù)據(jù)返回給前端,這時(shí)的 sum 字段是沒(méi)有經(jīng)過(guò) transient 修飾過(guò)的,所以能夠正常序列化。
{"amounts":3,"price":2,"sum":6}
假設(shè)我們的產(chǎn)品對(duì)象 Product 的 sum 屬性加上 transient 關(guān)鍵字修飾:
public?class??產(chǎn)品對(duì)象?Product?的?sum?{
?private?int?amounts;
?private?int?price;
?private?transient?int?sum;
}
然后我們?cè)囍跏蓟?Product ,并用 Gson 的 toJson() 方法序列化輸出 json 格式的結(jié)果。
public?static?void?main(String[]?args)?{
?Product?p?=?new?Product();
?p.setAmounts(3);
?p.setPrice(2);
?p.setSum(p.getAmounts()?*?p.getPrice());
?String?json?=?new?Gson().toJson(p);
?System.out.println(json);
}
這時(shí)控制臺(tái)是沒(méi)有打印出 sum 字段的。
{"amounts":3,"price":2}
我們看到,sum 屬性被 transient 修飾后,是不會(huì)被 Gson 序列化輸出的,這里就引出了使用 transient 關(guān)鍵字一個(gè)很重要的概念:對(duì)象屬性推導(dǎo)。
對(duì)象屬性推導(dǎo)
“如果一個(gè)對(duì)象的屬性值可以通過(guò)其他屬性或者方法推理出來(lái)的,那么該屬性就沒(méi)必要被序列化了。
借此我們以 Gson 來(lái)分析被 transient 修飾過(guò)的屬性不能被序列化過(guò)程。
首先,調(diào)用 Gson 的 toJson() 方法,傳入 Product 對(duì)象。
new?Gson().toJson(product)
根據(jù)傳入的產(chǎn)品對(duì)象,獲取 Product 對(duì)象的 class 類型:typeOfSrc,最后 找到對(duì)應(yīng)的對(duì)象解析適配器工廠。
toJson(Object?src,?Type?typeOfSrc,?JsonWriter?writer)
TypeAdapter<?>?adapter?=?getAdapter(TypeToken.get(typeOfSrc));
for?(TypeAdapterFactory?factory?:?factories)?{
?//?得到ReflectiveTypeAdapterFactory
?TypeAdapter<T>?candidate?=?factory.create(this,?type);
}
通過(guò)適配器 ReflectiveTypeAdapterFactory 工廠的 create() 方法,我們找到 getBoundFields 方法。
new?Adapter<T>(constructor,?getBoundFields(gson,?type,?raw));
for?(Field?field?:?fields)?{
?boolean?serialize?=?excludeField(field,?true);
?boolean?deserialize?=?excludeField(field,?false);
?...
}
這個(gè)方法做了兩件事情:
- 剔除被
transient關(guān)鍵字修飾過(guò)的屬性 - 篩選出可序列化的屬性
通過(guò) excludeField() 方法,剔除被 transient 修飾過(guò)的屬性,其規(guī)則是通過(guò)位運(yùn)算 "&" 判斷 modifiers 屬性與對(duì)象屬性的 field.getModifiers() 的值是否一致,來(lái)證明該屬性是否被 transient 修飾過(guò),如果是為真,表示剔除該屬性,不進(jìn)行序列化。
public?boolean?excludeField(Field?field,?boolean?serialize)?{
?//?通過(guò)?if?判斷?modifiers?屬性
?//?private?int?modifiers?=?Modifier.TRANSIENT?|?Modifier.STATIC;
?if?((modifiers?&?field.getModifiers())?!=?0)?{
???return?true;
?}?
}
另外根據(jù) modifiers 屬性定義 Modifier.TRANSIENT | Modifier.STATIC 兩種類型,一種是 tranient,另一種是 static 靜態(tài)類型。
Modifier.STATIC:靜態(tài)類型
由約定三、我們知道,靜態(tài)變量不會(huì)被序列化。
代碼 debug 到此,我們已經(jīng)知道 Gson 是如何證明對(duì)象是否存在被 transient 修飾過(guò)屬性以及如何過(guò)濾掉的完整過(guò)程。
被 transient 關(guān)鍵字修飾過(guò)得變量真的不能被序列化嘛?
想要解決這個(gè)問(wèn)題,首先還要再重提一下對(duì)象的序列化方式,Java 序列化提供兩種方式。
一種是實(shí)現(xiàn) Serializable 接口,另一種是實(shí)現(xiàn) Exteranlizable 接口。
實(shí)現(xiàn) Exteranlizable 接口需要重寫 writeExternal 和 readExternal 方法,它的效率比 Serializable 高一些,并且可以決定哪些屬性需要序列化(即使是 transient 修飾的),但是對(duì)大量對(duì)象,或者重復(fù)對(duì)象,則效率低。
從上面的這兩種序列化方式,我想你已經(jīng)看到了,使用 Exteranlizable 接口實(shí)現(xiàn)序列化時(shí),我們自己指定那些屬性是需要序列化的,即使是 transient 修飾的。下面就驗(yàn)證一下
首先我們定義 User1 類:這個(gè)類是被 Externalizable 接口修飾的

然后我們就可以測(cè)試了

上面,代碼分了兩個(gè)方法,一個(gè)是序列化,一個(gè)是反序列化。里面的代碼和一開(kāi)始給出的差不多,只不過(guò),User1 里面少了 age 這個(gè)屬性。
然后看一下結(jié)果:

結(jié)果基本上驗(yàn)證了我們的猜想,也就是說(shuō),實(shí)現(xiàn)了 Externalizable 接口,哪一個(gè)屬性被序列化是我們手動(dòng)去指定的,即使是 transient 關(guān)鍵字修飾也不起作用。
transient 關(guān)鍵字總結(jié)
通過(guò)常用的
Gson方式來(lái)驗(yàn)證tranient關(guān)鍵字不能序列化的使用場(chǎng)景。通過(guò)實(shí)現(xiàn)了
Externalizable接口,如果手動(dòng)去指定屬性序列化的,即使是transient關(guān)鍵字修飾也不起作用。另外,還可以通過(guò)
java的io包下的ObjectInputStream和ObjectOutputStream兩個(gè)對(duì)象輸入輸出流也可以驗(yàn)證,這里就不再做贅述,感興趣的朋友可以在網(wǎng)上找找例子。
參考
- https://www.cnblogs.com/chenpt/p/9415249.html
- https://blog.csdn.net/u012723673/article/details/80699029
- https://baijiahao.baidu.com/s?id=1636557218432721275&wfr=spider&for=pc
再來(lái)工具篇,10個(gè)解放雙手實(shí)用在線工具
