最近在家里修习 Java 这项技能,估计快点满技能点儿了,很开心。不过遇到了一个问题,困扰了我一阵子。问题是这样的,我要写 Android App,与服务器交互。大家都知道 Javascript 不知为什么占领了浏览器脚本的全部份额,紧接着 Json 成为了 Web 服务器与浏览器传输数据的事实标准。大家也知道 Java 不知为什么成为了 Android App 开发的推荐做法,而 Java 听上去就有着浓浓的“企业级”味道,自有一个序列化和一大堆 RPC 标准。Json 跟 Java 听上去就格格不入,但我要把 Java 对象变成 Json 传出去,再收到 Json 变回 Java 对象!
先展示一下我的工具叭:我使用 android.util.JsonWriter
和 android.util.JsonReader
,它们是做什么的呢?简单说,就是把 Json 字符串解析成 AST。比如我们可以用 reader.beginObject()
, reader.nextName()
, reader.nextInt()
去读取 Json 字符串里的左大括号,属性名和整型值,而不必考虑中间有几个空格,整数怎么一位一位读这些细节。或者说这个就是可以造出来的最低级的工具了。值得注意的是,我只会向同一个方向读写。
然后我先不想太多,先开始写代码。数组很好处理,读的时候先 beginArray()
读左中括号,然后在 hasNext()
返回 true
的时候用 nextXxx()
读下一个值(Xxx
可能是 Double
, Int
, Long
, String
或 Null
),如果返回 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
返回新对象,它每次新建一个该对象的空白实例,不停地读属性名并喂给接收属性名那个方法,直到所有属性都读完了,这时看看对象需要知道的属性是不是都读进来了,要是没有就返回空或报异常,要是都读进来了就返回这个实例。考虑到上一篇文章所述和方便重用,我把工厂方法写在一个内部类里,并要求它们继承自 JsonCreator
。JsonCreator
是个类,里面写着上述工厂方法的代码,并要求继承者实现 newBlankInstance
和 isComplete
,前者是返回空白实例的方法,后者是判断是否读了所有要知道的属性的方法。readProperty
不写在 JsonCreator
里的原因是否则需要实现者手工维护 this
指针。然后我写一个接口 Jsonable
,要求实现者实现 writeToJson
和 readProperty
方法。接口定义中包含一个内部类叫 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。
网友评论