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

transient 關(guān)鍵字的定義
說起 transient 關(guān)鍵字,不得不提對(duì)象的 序列化 的,因?yàn)槲覀兂3P枰诰W(wǎng)絡(luò)上以對(duì)象(數(shù)據(jù))的二進(jìn)制方式傳輸數(shù)據(jù),這里涉及到發(fā)送方序列化對(duì)象,接收方反序列化對(duì)象的過程。
那什么是序列化/反序列化?
“
Java中對(duì)象的序列化指的是將對(duì)象轉(zhuǎn)換成以字節(jié)序列的形式來表示,這些字節(jié)序列包含了對(duì)象的數(shù)據(jù)和信息,一個(gè)序列化后的對(duì)象可以被寫到數(shù)據(jù)庫或文件中,也可用于網(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è)過程就是反序列化。
在對(duì)象的序列化/反序列化過程中,我們經(jīng)常有這種需求,就是非必要字段不必進(jìn)行序列化。
例如有一個(gè)對(duì)象有三個(gè)字段 field1、field2、field3,發(fā)送方不想讓字段 field3 被序列化,因?yàn)檫@里面可能涉及到一些敏感信息不想被接收方知道,那有沒有辦法解決這個(gè)問題呢?
其實(shí)聰明的 Java 作者早就為我們量身定做了 transient 關(guān)鍵字!
簡(jiǎn)單來說,被 transient 關(guān)鍵字修飾過的成員屬性不能被序列化,transient 關(guān)鍵字只能修飾變量,而不能修飾方法和類。
transient 關(guān)鍵字的約定
約定一、只能修飾變量而不能修飾方法和類。注意本地變量是不能被
transient關(guān)鍵字修飾的。約定二、被
transient關(guān)鍵字修飾過的屬性不能被序列化,也就是說被transient修飾過的屬性,在對(duì)對(duì)象序列化后,是無法訪問到該屬性的。約定三、靜態(tài)變量不管有無被
transient修飾過,不能被序列化。
transient 關(guān)鍵字的使用場(chǎng)景
首先,我們看個(gè)例子,有個(gè)產(chǎn)品對(duì)象 Product,包括價(jià)格、數(shù)量、總價(jià)三個(gè)字段,那么總價(jià)可以通過 價(jià)格 乘以 數(shù)量 推導(dǎo)出來。
我們以查詢某個(gè)產(chǎn)品 API 接口為例,通過產(chǎn)品 ID,查詢返回一個(gè)產(chǎn)品對(duì)象。
public class Product {
private int amounts;
private int price;
private int sum;
}
通過 Gson 序列化后把 json 數(shù)據(jù)返回給前端,這時(shí)的 sum 字段是沒有經(jīng)過 transient 修飾過的,所以能夠正常序列化。
{"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)是沒有打印出 sum 字段的。
{"amounts":3,"price":2}
我們看到,sum 屬性被 transient 修飾后,是不會(huì)被 Gson 序列化輸出的,這里就引出了使用 transient 關(guān)鍵字一個(gè)很重要的概念:對(duì)象屬性推導(dǎo)。
對(duì)象屬性推導(dǎo)
“如果一個(gè)對(duì)象的屬性值可以通過其他屬性或者方法推理出來的,那么該屬性就沒必要被序列化了。
借此我們以 Gson 來分析被 transient 修飾過的屬性不能被序列化過程。
首先,調(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);
}
通過適配器 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)鍵字修飾過的屬性篩選出可序列化的屬性
通過 excludeField() 方法,剔除被 transient 修飾過的屬性,其規(guī)則是通過位運(yùn)算 "&" 判斷 modifiers 屬性與對(duì)象屬性的 field.getModifiers() 的值是否一致,來證明該屬性是否被 transient 修飾過,如果是為真,表示剔除該屬性,不進(jìn)行序列化。
public boolean excludeField(Field field, boolean serialize) {
// 通過 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 修飾過屬性以及如何過濾掉的完整過程。
被 transient 關(guān)鍵字修飾過得變量真的不能被序列化嘛?
想要解決這個(gè)問題,首先還要再重提一下對(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è)是反序列化。里面的代碼和一開始給出的差不多,只不過,User1 里面少了 age 這個(gè)屬性。
然后看一下結(jié)果:

結(jié)果基本上驗(yàn)證了我們的猜想,也就是說,實(shí)現(xiàn)了 Externalizable 接口,哪一個(gè)屬性被序列化是我們手動(dòng)去指定的,即使是 transient 關(guān)鍵字修飾也不起作用。
transient 關(guān)鍵字總結(jié)
通過常用的
Gson方式來驗(yàn)證tranient關(guān)鍵字不能序列化的使用場(chǎng)景。通過實(shí)現(xiàn)了
Externalizable接口,如果手動(dòng)去指定屬性序列化的,即使是transient關(guān)鍵字修飾也不起作用。另外,還可以通過
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
作者簡(jiǎn)介:編筐少年,一枚簡(jiǎn)單的北漂程序員。喜歡用簡(jiǎn)單的文字記錄工作與生活中的點(diǎn)點(diǎn)滴滴,愿與你一起分享程序員靈魂深處真正的內(nèi)心獨(dú)白。我的微信號(hào):WooolaDunzung,公眾號(hào)【猿芯】輸入 1024 ,有份驚喜送給你哦。
< END >
Linux 文件搜索神器 find 實(shí)戰(zhàn)詳解,建議收藏!
搞清楚這 10 幾個(gè)后端面試問題,工作穩(wěn)了!
歡迎關(guān)注微信公眾號(hào):互聯(lián)網(wǎng)全棧架構(gòu),收取更多有價(jià)值的信息。
