Java 對象的 Json 化與反 Json 化

最近在家里修習 Java 這項技能,估計快點滿技能點兒了,很開心。不過遇到了一個問題,困擾了我一陣子。問題是這樣的,我要寫 Android App,與服務器交互。大家都知道 Javascript 不知為什么占領了瀏覽器腳本的全部份額,緊接著 Json 成為了 Web 服務器與瀏覽器傳輸數據的事實標準。大家也知道 Java 不知為什么成為了 Android App 開發的推薦做法,而 Java 聽上去就有著濃濃的“企業級”味道,自有一個序列化和一大堆 RPC 標準。Json 跟 Java 聽上去就格格不入,但我要把 Java 對象變成 Json 傳出去,再收到 Json 變回 Java 對象!

先展示一下我的工具叭:我使用 android.util.JsonWriterandroid.util.JsonReader,它們是做什么的呢?簡單說,就是把 Json 字符串解析成 AST。比如我們可以用 reader.beginObject(), reader.nextName(), reader.nextInt() 去讀取 Json 字符串里的左大括號,屬性名和整型值,而不必考慮中間有幾個空格,整數怎么一位一位讀這些細節。或者說這個就是可以造出來的最低級的工具了。值得注意的是,我只會向同一個方向讀寫。

然后我先不想太多,先開始寫代碼。數組很好處理,讀的時候先 beginArray() 讀左中括號,然后在 hasNext() 返回 true 的時候用 nextXxx() 讀下一個值(Xxx 可能是 Double, Int, Long, StringNull),如果返回 false 了,就調用 endArray() 讀右中括號,保證狀態適合接著讀東西。至于讀進來的東西,隨便找個 Collection 扔進去就好。寫數組就更好寫了,先 beginArray() 寫左中括號,然后每次用 value(val) 寫值,最后 endArray() 寫右中括號就好了。
對象有點麻煩但也不難處理,讀的時候先 beginObject() 讀左大括號,然后在 hasNext() 返回 true 的時候用 nextName() 讀下一個屬性名,然后用 switch ... case ... 判斷這個屬性應該讀進哪個變量,并用 nextXxx() 把值讀進這個變量,最后不要忘記 default 情況要 skipValue(),不然下面操作讀取器的狀態就不對了,直到 hasNext() 返回 false,用 endObject()讀右大括號。寫的話比較好辦,就先 beginObject() 寫左大括號,然后不停地 name(name) value(val) 寫屬性,完了 endObject() 寫右大括號。一個對象有一個接收 JsonReader 的構造函數和一個接收 JsonWriter 的方法來做這一套過程。

啊聽起來非常好,不過麻煩事兒是有繼承。有繼承后有兩個問題,一是繼承的對象 Json 數據該長成什么樣子,二是這個數據該怎么解析。對問題一我思考一段時間(腦殘兒童 QAQ)決定答案是 Json 的結構應該和 Java 保持一致,或者說就是如果 Java 里用了繼承,那么新增的屬性應該和原來的屬性放在同一個對象里,如果 Java 用了委托,新增的屬性就應該放在一個新的對象里作為原來對象的屬性。對問題二我又思考了一段時間(TAT 腦殘兒童不許笑)發現所謂 Json 就是一個序列化機制罷了。那看看前輩是怎么寫序列化的,于是我拿來了 Parcelable,發現它們的做法是先寫祖宗的屬性,然后寫自己的屬性,讀的時侯先在構造函數里調祖宗的構造函數讀祖宗的屬性,然后讀自己的屬性。我試圖把這個辦法搬到 Json 上。果然碰到問題了:祖宗構造函數和自己構造函數都要讀大括號,然而我沒有帶那么多大括號可以讀。不過我很快想到了解決辦法:構造函數永遠永遠不讀大括號,把大括號留給調用構造函數的人讀,這樣就好啦(嗯大括號熟么的在這種不需要數據結構自描述的情況下就是冗余米有錯)。可是這樣子以后我會被人抽的,因為 Json 對象不保證順序,要是逼著寫 Javascript 的在 Json 化時候必須保證順序的話我相信它會毫不猶豫去跳樓的。

于是我想到了一個辦法,就是給基類寫一個接口,里面有一個方法。當基類讀 Json 讀到一個屬性自己不認識的時候,就把屬性名和 JsonReader 對象傳給這個方法。這樣,子類只要寫一個內部類實現這個接口,在方法中看看這個屬性認識不認識,認識就讀進來,不認識就遞歸地給自己的子類的這個接口處理。

不過這么做還是有問題。問題是基類的部分控制代碼每次寫個基類都得寫一遍,得抽出來。然后我又想了好長時間,發現其實根本問題是我有一堆屬性,我得給每個屬性在繼承樹上找到它應該在的位置。這樣說來,每次讀取屬性名的操作就不應該由基類來做了,應該由外面的函數來做。這些類們只實現一個方法 readProperty:接收一個屬性名和一個 JsonReader,要是自己認得,就讀出屬性值,否則把屬性名和 JsonReader 照樣傳給父類(如果有的話)或跳過值(如果父類沒有 readProperty)。然后再寫一個工廠方法叫 newInstanceFromJson,接收 JsonReader 返回新對象,它每次新建一個該對象的空白實例,不停地讀屬性名并喂給接收屬性名那個方法,直到所有屬性都讀完了,這時看看對象需要知道的屬性是不是都讀進來了,要是沒有就返回空或報異常,要是都讀進來了就返回這個實例。考慮到上一篇文章所述和方便重用,我把工廠方法寫在一個內部類里,并要求它們繼承自 JsonCreatorJsonCreator 是個類,里面寫著上述工廠方法的代碼,并要求繼承者實現 newBlankInstanceisComplete,前者是返回空白實例的方法,后者是判斷是否讀了所有要知道的屬性的方法。readProperty 不寫在 JsonCreator 里的原因是否則需要實現者手工維護 this 指針。然后我寫一個接口 Jsonable,要求實現者實現 writeToJsonreadProperty 方法。接口定義中包含一個內部類叫 JsonCreator 的定義,然后在文檔中要求所有實現 Jsonable 的類都要定義一個靜態成員叫 JsonCREATOR,它是 JsonCreator 的一個子類,其中實現了 newBlankInstance

嗯其實我知道 Gson 可以很隨意地把這件事做掉,但是我沒有用 Gson,原因有兩個。一個是我喜歡造輪子,另外一個是 Gson 這種東西一定會用一些反射,我不想用反射,我想只用語言的靜態特性(比如找父類不用反射,用 super)。

但是這樣子又有點兒問題,每一個 readProperty 里其實是一個拿字符串做的 switch...case... 語句,而 Java 里它的實現可以認為是查一次哈希表。這樣一個屬性如果一個類不認識,那么它就會去問它父類,父類要再不認識就會問父類的父類……這樣問下去可能一直要查掉整個繼承鏈的長度,好慢的感覺。

不過這個問題是沒辦法的事兒,我們可以把它抽象一下:有 N 個類,它們組成一個有根森林(邊代表繼承關系);有 M 個屬性,每個屬性可以被某些類擁有;現在每次給一個類和一個屬性,查詢從該類到根的路徑上,距該類最近的有該屬性的類。這個問題還有兩個版本,就是可不可以動態添加類和屬性,動態語言對應著可以動態添加類和屬性的版本,靜態語言則相反。這個問題里預處理對應著編譯時,而查詢處理對應著運行時。

暴力的方法有兩個。一是給每個節點存它擁有的屬性,然后每次查詢從某個類開始逐個查父節點一直到根,預處理復雜度 O(M + N),查詢復雜度 O(N) (最壞情況深度等于類的個數,并且認為判斷表里是否有一個屬性是 O(1) 的)。另一個是給每個節點存一個表,里面存著它擁有每個屬性的最近祖先是誰,查詢時直接查表即可,預處理復雜度 O(MN),查詢復雜度 O(1)。而且前一個方法動態添加刪除類和屬性效率較高。

OI 里很流行 log 記號,帶 log 的算法我只想了一個。先考慮每個屬性只被一個類擁有的情況,這時我們可以預處理每個類的深度和它的 1, 2, 4, 8, ... 輩祖宗是誰,這樣可以在 O(log N) 時間內找到它的 N 輩祖宗。查詢時我們找到這個類,找到擁有這個屬性的那個類,如果“這個類”比“那個類”深度更淺,那就找不到,否則求它們的深度差,找“這個類”的“深度差”輩祖宗,看是不是“那個類”。然后考慮每個屬性被多個類擁有的情況,先找到這個類,然后找到擁有這個屬性的 DFS 序恰小于這個類的那個類。然后把“那個類”到根路徑上所有有這個屬性的類做成一個表,在這個表里找到深度最深的是“這個類”的祖宗的類。尋找可以用二分的辦法,因為如果一個類是“這個類”的祖宗,那么它的祖宗也是“這個類”的祖宗。二分法做起來可以這么辦:預處理出“那個類”到根的路徑里距它 1, 2, 4, 8, ... 近的有該屬性的類,然后就可以折半了。二分后的判斷就用之前簡單情況的辦法,這樣整個方法,預處理的復雜度是 O((N + M) log N),查詢的復雜度是 O(log N * log M)。不過這個方法不支持動態添加刪除類和屬性,要支持的的話應該要動態樹什么的我不會 T_T。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,776評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,592評論 25 707
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,707評論 18 399
  • 貝微微和肖奈從網友俠侶結為現實情侶,從初戀到步入結婚殿堂,絕不是一句“郎才女貌很般配”就可以概括的。就算貝微微是大...
    windy天意晚晴閱讀 3,947評論 8 73
  • 文/宋麗 1 那天,所有的碎枝末節一齊向她涌來。 丑陋廉潔的大帆布袋里,窩藏著她用過兩年的舊被褥,拖著行李的手還是...
    宋小麗閱讀 353評論 0 1