Java 對(duì)象的 Json 化與反 Json 化

最近在家里修習(xí) Java 這項(xiàng)技能浆西,估計(jì)快點(diǎn)滿(mǎn)技能點(diǎn)兒了,很開(kāi)心顽腾。不過(guò)遇到了一個(gè)問(wèn)題近零,困擾了我一陣子。問(wèn)題是這樣的抄肖,我要寫(xiě) Android App久信,與服務(wù)器交互。大家都知道 Javascript 不知為什么占領(lǐng)了瀏覽器腳本的全部份額漓摩,緊接著 Json 成為了 Web 服務(wù)器與瀏覽器傳輸數(shù)據(jù)的事實(shí)標(biāo)準(zhǔn)裙士。大家也知道 Java 不知為什么成為了 Android App 開(kāi)發(fā)的推薦做法,而 Java 聽(tīng)上去就有著濃濃的“企業(yè)級(jí)”味道管毙,自有一個(gè)序列化和一大堆 RPC 標(biāo)準(zhǔn)腿椎。Json 跟 Java 聽(tīng)上去就格格不入,但我要把 Java 對(duì)象變成 Json 傳出去夭咬,再收到 Json 變回 Java 對(duì)象啃炸!

先展示一下我的工具叭:我使用 android.util.JsonWriterandroid.util.JsonReader,它們是做什么的呢卓舵?簡(jiǎn)單說(shuō)南用,就是把 Json 字符串解析成 AST。比如我們可以用 reader.beginObject(), reader.nextName(), reader.nextInt() 去讀取 Json 字符串里的左大括號(hào)掏湾,屬性名和整型值裹虫,而不必考慮中間有幾個(gè)空格,整數(shù)怎么一位一位讀這些細(xì)節(jié)忘巧『憬纾或者說(shuō)這個(gè)就是可以造出來(lái)的最低級(jí)的工具了。值得注意的是砚嘴,我只會(huì)向同一個(gè)方向讀寫(xiě)十酣。

然后我先不想太多,先開(kāi)始寫(xiě)代碼际长。數(shù)組很好處理耸采,讀的時(shí)候先 beginArray() 讀左中括號(hào),然后在 hasNext() 返回 true 的時(shí)候用 nextXxx() 讀下一個(gè)值(Xxx 可能是 Double, Int, Long, StringNull)工育,如果返回 false 了虾宇,就調(diào)用 endArray() 讀右中括號(hào),保證狀態(tài)適合接著讀東西如绸。至于讀進(jìn)來(lái)的東西嘱朽,隨便找個(gè) Collection 扔進(jìn)去就好旭贬。寫(xiě)數(shù)組就更好寫(xiě)了,先 beginArray() 寫(xiě)左中括號(hào)搪泳,然后每次用 value(val) 寫(xiě)值稀轨,最后 endArray() 寫(xiě)右中括號(hào)就好了。
對(duì)象有點(diǎn)麻煩但也不難處理岸军,讀的時(shí)候先 beginObject() 讀左大括號(hào)奋刽,然后在 hasNext() 返回 true 的時(shí)候用 nextName() 讀下一個(gè)屬性名,然后用 switch ... case ... 判斷這個(gè)屬性應(yīng)該讀進(jìn)哪個(gè)變量艰赞,并用 nextXxx() 把值讀進(jìn)這個(gè)變量佣谐,最后不要忘記 default 情況要 skipValue(),不然下面操作讀取器的狀態(tài)就不對(duì)了方妖,直到 hasNext() 返回 false狭魂,用 endObject()讀右大括號(hào)。寫(xiě)的話(huà)比較好辦吁断,就先 beginObject() 寫(xiě)左大括號(hào)趁蕊,然后不停地 name(name) value(val) 寫(xiě)屬性,完了 endObject() 寫(xiě)右大括號(hào)仔役。一個(gè)對(duì)象有一個(gè)接收 JsonReader 的構(gòu)造函數(shù)和一個(gè)接收 JsonWriter 的方法來(lái)做這一套過(guò)程掷伙。

啊聽(tīng)起來(lái)非常好,不過(guò)麻煩事兒是有繼承又兵。有繼承后有兩個(gè)問(wèn)題任柜,一是繼承的對(duì)象 Json 數(shù)據(jù)該長(zhǎng)成什么樣子,二是這個(gè)數(shù)據(jù)該怎么解析沛厨。對(duì)問(wèn)題一我思考一段時(shí)間(腦殘兒童 QAQ)決定答案是 Json 的結(jié)構(gòu)應(yīng)該和 Java 保持一致宙地,或者說(shuō)就是如果 Java 里用了繼承,那么新增的屬性應(yīng)該和原來(lái)的屬性放在同一個(gè)對(duì)象里逆皮,如果 Java 用了委托宅粥,新增的屬性就應(yīng)該放在一個(gè)新的對(duì)象里作為原來(lái)對(duì)象的屬性。對(duì)問(wèn)題二我又思考了一段時(shí)間(TAT 腦殘兒童不許笑)發(fā)現(xiàn)所謂 Json 就是一個(gè)序列化機(jī)制罷了电谣。那看看前輩是怎么寫(xiě)序列化的秽梅,于是我拿來(lái)了 Parcelable,發(fā)現(xiàn)它們的做法是先寫(xiě)祖宗的屬性剿牺,然后寫(xiě)自己的屬性企垦,讀的時(shí)侯先在構(gòu)造函數(shù)里調(diào)祖宗的構(gòu)造函數(shù)讀祖宗的屬性,然后讀自己的屬性晒来。我試圖把這個(gè)辦法搬到 Json 上钞诡。果然碰到問(wèn)題了:祖宗構(gòu)造函數(shù)和自己構(gòu)造函數(shù)都要讀大括號(hào),然而我沒(méi)有帶那么多大括號(hào)可以讀。不過(guò)我很快想到了解決辦法:構(gòu)造函數(shù)永遠(yuǎn)永遠(yuǎn)不讀大括號(hào)荧降,把大括號(hào)留給調(diào)用構(gòu)造函數(shù)的人讀接箫,這樣就好啦(嗯大括號(hào)熟么的在這種不需要數(shù)據(jù)結(jié)構(gòu)自描述的情況下就是冗余米有錯(cuò))√芘祝可是這樣子以后我會(huì)被人抽的列牺,因?yàn)?Json 對(duì)象不保證順序,要是逼著寫(xiě) Javascript 的在 Json 化時(shí)候必須保證順序的話(huà)我相信它會(huì)毫不猶豫去跳樓的拗窃。

于是我想到了一個(gè)辦法,就是給基類(lèi)寫(xiě)一個(gè)接口泌辫,里面有一個(gè)方法随夸。當(dāng)基類(lèi)讀 Json 讀到一個(gè)屬性自己不認(rèn)識(shí)的時(shí)候,就把屬性名和 JsonReader 對(duì)象傳給這個(gè)方法震放。這樣宾毒,子類(lèi)只要寫(xiě)一個(gè)內(nèi)部類(lèi)實(shí)現(xiàn)這個(gè)接口,在方法中看看這個(gè)屬性認(rèn)識(shí)不認(rèn)識(shí)殿遂,認(rèn)識(shí)就讀進(jìn)來(lái)诈铛,不認(rèn)識(shí)就遞歸地給自己的子類(lèi)的這個(gè)接口處理。

不過(guò)這么做還是有問(wèn)題墨礁。問(wèn)題是基類(lèi)的部分控制代碼每次寫(xiě)個(gè)基類(lèi)都得寫(xiě)一遍幢竹,得抽出來(lái)。然后我又想了好長(zhǎng)時(shí)間恩静,發(fā)現(xiàn)其實(shí)根本問(wèn)題是我有一堆屬性焕毫,我得給每個(gè)屬性在繼承樹(shù)上找到它應(yīng)該在的位置。這樣說(shuō)來(lái)驶乾,每次讀取屬性名的操作就不應(yīng)該由基類(lèi)來(lái)做了邑飒,應(yīng)該由外面的函數(shù)來(lái)做。這些類(lèi)們只實(shí)現(xiàn)一個(gè)方法 readProperty:接收一個(gè)屬性名和一個(gè) JsonReader级乐,要是自己認(rèn)得疙咸,就讀出屬性值,否則把屬性名和 JsonReader 照樣傳給父類(lèi)(如果有的話(huà))或跳過(guò)值(如果父類(lèi)沒(méi)有 readProperty)风科。然后再寫(xiě)一個(gè)工廠方法叫 newInstanceFromJson撒轮,接收 JsonReader 返回新對(duì)象,它每次新建一個(gè)該對(duì)象的空白實(shí)例丐重,不停地讀屬性名并喂給接收屬性名那個(gè)方法腔召,直到所有屬性都讀完了,這時(shí)看看對(duì)象需要知道的屬性是不是都讀進(jìn)來(lái)了扮惦,要是沒(méi)有就返回空或報(bào)異常臀蛛,要是都讀進(jìn)來(lái)了就返回這個(gè)實(shí)例。考慮到上一篇文章所述和方便重用浊仆,我把工廠方法寫(xiě)在一個(gè)內(nèi)部類(lèi)里客峭,并要求它們繼承自 JsonCreatorJsonCreator 是個(gè)類(lèi)舔琅,里面寫(xiě)著上述工廠方法的代碼,并要求繼承者實(shí)現(xiàn) newBlankInstanceisComplete洲劣,前者是返回空白實(shí)例的方法备蚓,后者是判斷是否讀了所有要知道的屬性的方法。readProperty 不寫(xiě)在 JsonCreator 里的原因是否則需要實(shí)現(xiàn)者手工維護(hù) this 指針囱稽。然后我寫(xiě)一個(gè)接口 Jsonable郊尝,要求實(shí)現(xiàn)者實(shí)現(xiàn) writeToJsonreadProperty 方法。接口定義中包含一個(gè)內(nèi)部類(lèi)叫 JsonCreator 的定義战惊,然后在文檔中要求所有實(shí)現(xiàn) Jsonable 的類(lèi)都要定義一個(gè)靜態(tài)成員叫 JsonCREATOR流昏,它是 JsonCreator 的一個(gè)子類(lèi),其中實(shí)現(xiàn)了 newBlankInstance吞获。

嗯其實(shí)我知道 Gson 可以很隨意地把這件事做掉况凉,但是我沒(méi)有用 Gson,原因有兩個(gè)各拷。一個(gè)是我喜歡造輪子刁绒,另外一個(gè)是 Gson 這種東西一定會(huì)用一些反射,我不想用反射撤逢,我想只用語(yǔ)言的靜態(tài)特性(比如找父類(lèi)不用反射膛锭,用 super)。

但是這樣子又有點(diǎn)兒?jiǎn)栴}蚊荣,每一個(gè) readProperty 里其實(shí)是一個(gè)拿字符串做的 switch...case... 語(yǔ)句初狰,而 Java 里它的實(shí)現(xiàn)可以認(rèn)為是查一次哈希表。這樣一個(gè)屬性如果一個(gè)類(lèi)不認(rèn)識(shí)互例,那么它就會(huì)去問(wèn)它父類(lèi)奢入,父類(lèi)要再不認(rèn)識(shí)就會(huì)問(wèn)父類(lèi)的父類(lèi)……這樣問(wèn)下去可能一直要查掉整個(gè)繼承鏈的長(zhǎng)度,好慢的感覺(jué)媳叨。

不過(guò)這個(gè)問(wèn)題是沒(méi)辦法的事兒腥光,我們可以把它抽象一下:有 N 個(gè)類(lèi),它們組成一個(gè)有根森林(邊代表繼承關(guān)系)糊秆;有 M 個(gè)屬性武福,每個(gè)屬性可以被某些類(lèi)擁有;現(xiàn)在每次給一個(gè)類(lèi)和一個(gè)屬性痘番,查詢(xún)從該類(lèi)到根的路徑上捉片,距該類(lèi)最近的有該屬性的類(lèi)平痰。這個(gè)問(wèn)題還有兩個(gè)版本,就是可不可以動(dòng)態(tài)添加類(lèi)和屬性伍纫,動(dòng)態(tài)語(yǔ)言對(duì)應(yīng)著可以動(dòng)態(tài)添加類(lèi)和屬性的版本宗雇,靜態(tài)語(yǔ)言則相反。這個(gè)問(wèn)題里預(yù)處理對(duì)應(yīng)著編譯時(shí)莹规,而查詢(xún)處理對(duì)應(yīng)著運(yùn)行時(shí)赔蒲。

暴力的方法有兩個(gè)。一是給每個(gè)節(jié)點(diǎn)存它擁有的屬性良漱,然后每次查詢(xún)從某個(gè)類(lèi)開(kāi)始逐個(gè)查父節(jié)點(diǎn)一直到根舞虱,預(yù)處理復(fù)雜度 O(M + N),查詢(xún)復(fù)雜度 O(N) (最壞情況深度等于類(lèi)的個(gè)數(shù)母市,并且認(rèn)為判斷表里是否有一個(gè)屬性是 O(1) 的)砾嫉。另一個(gè)是給每個(gè)節(jié)點(diǎn)存一個(gè)表,里面存著它擁有每個(gè)屬性的最近祖先是誰(shuí)窒篱,查詢(xún)時(shí)直接查表即可,預(yù)處理復(fù)雜度 O(MN)舶沿,查詢(xún)復(fù)雜度 O(1)墙杯。而且前一個(gè)方法動(dòng)態(tài)添加刪除類(lèi)和屬性效率較高。

OI 里很流行 log 記號(hào)括荡,帶 log 的算法我只想了一個(gè)高镐。先考慮每個(gè)屬性只被一個(gè)類(lèi)擁有的情況,這時(shí)我們可以預(yù)處理每個(gè)類(lèi)的深度和它的 1, 2, 4, 8, ... 輩祖宗是誰(shuí)畸冲,這樣可以在 O(log N) 時(shí)間內(nèi)找到它的 N 輩祖宗嫉髓。查詢(xún)時(shí)我們找到這個(gè)類(lèi),找到擁有這個(gè)屬性的那個(gè)類(lèi)邑闲,如果“這個(gè)類(lèi)”比“那個(gè)類(lèi)”深度更淺算行,那就找不到,否則求它們的深度差苫耸,找“這個(gè)類(lèi)”的“深度差”輩祖宗州邢,看是不是“那個(gè)類(lèi)”。然后考慮每個(gè)屬性被多個(gè)類(lèi)擁有的情況褪子,先找到這個(gè)類(lèi)量淌,然后找到擁有這個(gè)屬性的 DFS 序恰小于這個(gè)類(lèi)的那個(gè)類(lèi)。然后把“那個(gè)類(lèi)”到根路徑上所有有這個(gè)屬性的類(lèi)做成一個(gè)表嫌褪,在這個(gè)表里找到深度最深的是“這個(gè)類(lèi)”的祖宗的類(lèi)呀枢。尋找可以用二分的辦法,因?yàn)槿绻粋€(gè)類(lèi)是“這個(gè)類(lèi)”的祖宗笼痛,那么它的祖宗也是“這個(gè)類(lèi)”的祖宗裙秋。二分法做起來(lái)可以這么辦:預(yù)處理出“那個(gè)類(lèi)”到根的路徑里距它 1, 2, 4, 8, ... 近的有該屬性的類(lèi)琅拌,然后就可以折半了。二分后的判斷就用之前簡(jiǎn)單情況的辦法残吩,這樣整個(gè)方法财忽,預(yù)處理的復(fù)雜度是 O((N + M) log N),查詢(xún)的復(fù)雜度是 O(log N * log M)泣侮。不過(guò)這個(gè)方法不支持動(dòng)態(tài)添加刪除類(lèi)和屬性即彪,要支持的的話(huà)應(yīng)該要?jiǎng)討B(tài)樹(shù)什么的我不會(huì) T_T。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末活尊,一起剝皮案震驚了整個(gè)濱河市隶校,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蛹锰,老刑警劉巖深胳,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異铜犬,居然都是意外死亡舞终,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門(mén)癣猾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)敛劝,“玉大人,你說(shuō)我怎么就攤上這事纷宇】涿耍” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵像捶,是天一觀的道長(zhǎng)上陕。 經(jīng)常有香客問(wèn)我,道長(zhǎng)拓春,這世上最難降的妖魔是什么释簿? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮痘儡,結(jié)果婚禮上辕万,老公的妹妹穿的比我還像新娘。我一直安慰自己沉删,他們只是感情好渐尿,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著矾瑰,像睡著了一般砖茸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上殴穴,一...
    開(kāi)封第一講書(shū)人閱讀 49,730評(píng)論 1 289
  • 那天凉夯,我揣著相機(jī)與錄音货葬,去河邊找鬼。 笑死劲够,一個(gè)胖子當(dāng)著我的面吹牛震桶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播征绎,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蹲姐,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了人柿?” 一聲冷哼從身側(cè)響起柴墩,我...
    開(kāi)封第一講書(shū)人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎凫岖,沒(méi)想到半個(gè)月后江咳,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡哥放,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年歼指,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甥雕。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡东臀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出犀农,到底是詐尸還是另有隱情,我是刑警寧澤宰掉,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布呵哨,位于F島的核電站,受9級(jí)特大地震影響轨奄,放射性物質(zhì)發(fā)生泄漏孟害。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一挪拟、第九天 我趴在偏房一處隱蔽的房頂上張望挨务。 院中可真熱鬧,春花似錦玉组、人聲如沸谎柄。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)朝巫。三九已至,卻和暖如春石景,著一層夾襖步出監(jiān)牢的瞬間劈猿,已是汗流浹背拙吉。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留揪荣,地道東北人筷黔。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像仗颈,于是被迫代替她去往敵國(guó)和親佛舱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理揽乱,服務(wù)發(fā)現(xiàn)名眉,斷路器,智...
    卡卡羅2017閱讀 134,626評(píng)論 18 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,732評(píng)論 25 707
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法凰棉,類(lèi)相關(guān)的語(yǔ)法损拢,內(nèi)部類(lèi)的語(yǔ)法,繼承相關(guān)的語(yǔ)法撒犀,異常的語(yǔ)法福压,線(xiàn)程的語(yǔ)...
    子非魚(yú)_t_閱讀 31,597評(píng)論 18 399
  • 貝微微和肖奈從網(wǎng)友俠侶結(jié)為現(xiàn)實(shí)情侶,從初戀到步入結(jié)婚殿堂或舞,絕不是一句“郎才女貌很般配”就可以概括的荆姆。就算貝微微是大...
    windy天意晚晴閱讀 3,907評(píng)論 8 73
  • 文/宋麗 1 那天,所有的碎枝末節(jié)一齊向她涌來(lái)映凳。 丑陋廉潔的大帆布袋里胆筒,窩藏著她用過(guò)兩年的舊被褥,拖著行李的手還是...
    宋小麗閱讀 347評(píng)論 0 1