Java 的 idiom(5)

选择 interface 避免 abstract class

这 一点是 Java 的问题,因为单继承,使用 abstract class 意味着排它性,即即便原先的代码存在于某个 hierarchy 里面,为了使用这个库提供的功能就必须离开原先的结构,而 interface 不存在这个问题。这两者是可以混用的,常见的做法是先定义 interface,然后某种问题下需要特定的解决方案可以抽象为某抽象类,其实现或直接不实现这个 interface,而其子类实现缺失的函数。这个例子可以参看 JDK 的 collection,虽然大部分接口的实现都不是用来给人继承的,但是同时提供了 abstract class,这可以方便有需要的人通过继承这些类方便的实现那些不是很容易实现的接口。这一般称为 skeletal implementation(半架子工程,hmm..)。

接口的问题是一旦广为流传,若需要修改就会变得不可能,也就是说越 generic 的 interface 越需要谨慎设计,一旦敲定就不能多加演化了,但是 abstract class 往往还可以反复折腾。

使用接口的时候不要通过它去定义常数供实现使用(不过难道公用的常数只能放在类里面吗?)在没有多继承的情况下,一组相关的常数可能只能通过 aggregation 获得了… 也就是说你必须多写个类名,如果你不能(被推荐)用 interface 存放数据。

继承体系与 tagged class

在 C 的时代,常用 tag 来标注某些类是干啥的,有了 inheritance 和 instanceof,其实 Java 里面不是很推荐这种手工实现多态的策略。

函数对象与 strategy

functor 其实是拥有某种 signature 的实现,往往代表某种功能的实现,某些事情为了具有可扩展性,往往通过 delegate 让别的类去实现,这种也称为 strategy pattern,C++ 里面另外有个 compile-time 的 policy,本身都是一个意思。但是 Java 并没有匿名函数和 closure,导致这个概念实现起来都特别的费神。常见的做法是定义某个 interface(如 java.util.Comparator),然后你可以选择通过 anonymous inner class 在需要的时候 new 一个,但这样会每次调用有开销,如果不需要使用调用环境中对象,可以通过一个 private static 对象保存这个实现。

偏好 static 成员类

所谓的成员类指嵌套在另一个类定义里面的类,出现这个类往往是为了将外围类的某些功能分离出去,例如 builder 往往这样,使用 builder 时就会 Foo.Builder 看得很清楚是为了建立 Foo 而调用的 inner class。我们可以使用 public/protected/private 来限制对这些 inner class 的访问,这与 C++ 尚有共同,比较神奇的是 java 有点画蛇添足般的增加了 static 和 non-static 两种,区别?static 类型其实和 C++ 的比较接近,而 non-static 类型会隐含式的包含对当前对象的引用。也许你会说那我构造 static inner class 传递 this 不就一样可以达到类似的效果么?的确,但是 Java 选择了通过“语言”来支持这么一个简单的事情(grammar sugar),就我看来反而让初学者更加犯昏。

inner class 的存在从某种程度上解决了没有 friend 造成的问题。inner class 可以访问外围类的成员:static 的可以访问 static 部分,而 non-static 的可以访问包括成员的所有(从表示上看,因为 inner class 的代码在外围类里面,于是这个不存在 scope 越界的问题)。但是 C++ 的 inner class 就没有这种特权了,通过 friend、client attorney idiom,C++ 其实也还能不错的 handle 这种 delegate class,同时也更开放。

那么你可能会问了,什么情况我需要 non-static inner class,很简单一旦你的 inner class 需要对对象信息,比如写 iterator 等需要知晓对象状态的类的时候就用上了。在这个类里面 this 表示 inner class,而通过外围类名 + .this 就能访问外围类的成员了。逻辑上这个类的实例都是依附与某个外围类的实例的。一个简单的例子,这里不多说了。

避免使用 raw types

JDK 从 5 开始支持 generic,但是其实 Java 的 generic 比较 crappy,只是从 compile-time 上“试图”保证类型的合理匹配,运行时由于 type erasure 通过 generic 写的函数的 signature 就退化成为了 Object 这类东西。我们往往需要使用 parameterized type,尽量避免直接使用 raw type。特别有了 spring 这种东西,有的对象通过 injection 获得可能不满足 parameterized 的要求,这样程序就会在 runtime 抛出合理的异常;当然你可以钻空子分析一个类型混合的容器,就算这样你也应该尽量使用 Object 作为参数。

当你写一个处理未知参数类型的函数时,应该使用 ? 系(另外有 ? extends 和 ? super 表示类型的上下界),而不应该使用 Object,编译时并不认为 List<Integer> 是 List<Object> 的子类(这点和 C++ 一致),但是这点和 C++ 出现了很大的不同,C++ 因为通过 template 实现的 generics,一个能接受存放任意(同类)类型的列表还是一个 template 可以表示的概念。然而 Java 某个容器可以存放不同类的对象,如果你确实不在意里面是啥类型,你可以用 ?,这个和一个 parameterized 函数还是有点微妙的区别的,一旦指定了 type,这个函数/类就得用,那么如果两个类并不一样又试图做一些跨界操作就会导致类型不匹配(比如两个 Set,判断公用元素个数,此时如果前一个叫 T1 后一个 T2,这两者并没有关系,试图判断后者 contains T1 类型就会报编译错误,这时你也不能用 Set<Object>,否则函数的 signature 与传入的 set 就不见得一致了)。这一点很诡异,Java 会多引入一个 ? 来实现这样一个事,而 C++ 不存在这种可能。

更加诡异的是 ? 的引入会产生一些限制,我们可以看看 Collection 的定义,其中 contains 和 add 具有不同的 signature(一个直接用 Object,一个却是 parameterized type),虽然 runtime 两者的 signature 一样,但是 compile-time 却有不同的行为,比如前面的计算两个 Set 公用元素个数的代码,因为你要用 contains 而这个 take Object,这是所有对象的根,所以就算你不是 T 类的也没问题。那如果你这个 ? 类型尝试 add 就会发生很诡异的事情,逻辑上,你用 generic 试图保证 type 一致,但是 ? 却让你不知道这到底是什么界限的类型,因此 parameter type T 这里不管是什么类型都不会匹配上,除非你给的是个 null。从某个意义上来说,如果这个方法是 read-only 的,传入的可以不是 parameterized type,那么如果是个容器就可以选择用 object,而如果是需要修改状态的就得通过 type system 来 enforce 类型一致性了。如果你选择了 raw type 来写,这个 add 就过了(也许这可能是你要的呢?)。

那么剩下两点就不是诡异了,而是 exception,generic 的 class 是不带 generic 的,也就是说你只能写 Set.class 或者 instanceof Set。之后如果你希望能够做 type cast 的话也应该转换到 <?> 类型。

消除 unchecked warning

使用 raw type 转换到 parameterized type 会产生 unchecked warning,一般你或者 resolve 这个问题,或者通过 @SuppressWarnings 来避免编译产生不必要的警告,但是记得在注释里面解释这样做的原因。

使用 List 避免 array

这个选择的原因主要是 array 存在继承关系,比如 Object[]和 Long[] 是继承关系,这导致你可以将 Long[] 赋值到 Object[] 对象,而当你试图将其中的元素用别的类型进行赋值的时候不会产生编译出错,但是却会出现运行时错误(试图将别的类型赋值给 Long)。然而 List<Object> 却与 List<Long> 没有关系。这样回避了这个问题。同时有一些限制,如不允许创建 generic 的 array(编译出错)。

type erasure 的另一种说法就是 Java 的 generic 是 non-reifiable,即运行时的信息与编译时所知道的信息不一样多,唯一是 reifiable 的 generic 只有 ? 表示的,但是我们也要清楚认识到其作用(比如 Collection<?> 的 contains 和 add 的区别)。

实际很多 code 里面 List 和 array 之间的转换还是有的,不是说我们真的可以抛弃掉 array。当将 List 转换到 array 返回的是 Object[],比较偷懒的做法是使用类型转换将结果 cast back 到需要的类型。这样会产生一个 unchecked warning。特别对 synchronized List 来说通常需要通过 toArray 保证操作的原子性,那么一种去掉这个 warning 的策略就是通过别的操作获得类似的原子性,如 copy 到另一个 List,而将这个非原子操作使用 synchronized 关键字包括起来。

——————-
And they said, We saw certainly that the LORD was with thee: and we said, Let there be now an oath betwixt us, even betwixt us and thee, and let us make a covenant with thee;

Advertisements
Java 的 idiom(5)

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s