Java 的 idiom(6)

选择 generic type

实现某些类的时候,如果确性可以使用 generic type 来表达能够更准确,就应该选择 generic type 而避免使用 Object 作为 universal 的 type。

选择 generic method

Java 的 generic method 写起来比较奇怪(因为没有对应的关键字),一般是在返回类型前面加上 <T> 等 generic type,这样就可以写带有 generic 参数的函数了(对应的类未必需要是)。有时候其实需要搞清楚到底是哪里需要 generics,比如曾经写了个一个比较弱的类 RetryCaller,它用来执行 retry logic,提供了一个 inner interface,要求需要 retry 的类提供这个 interface 的实现,在这个基础上如果截获了异常,就进行重试。重试的函数是 T retry (Callable<T> c),其实这个 retry 的类本身并没有任何需要 generics 的地方,这个 retry 方法却需要 generics,我曾幼稚的以为这里必须让类变成 generic,这样一来 code review 的时候就有人说了,你这个返回 Integer 的和返回 String 的两个任务能共用一个实例吗?我很清楚,逻辑上两个事情是分离的,但是从语法上来看却应该是两个东西(RetryCaller<Integer> 和 RetryCaller<String>),尽管我事后用分析 + test case 说服了他们,但是我也知道这是语言表达上的问题,而不是逻辑问题。现在了解了 generic method 的写法就很清楚,类本身不是 generic 而方法的确应该弄成 generic 的,这样语意上也就不存在这种模棱两可的解释和误解了。

这个技巧在实现 factory method 的时候是常用的,因为构造 generic container 的时候可以通过 type inference 节省一些繁文缛节。

当然某些情况下 type erasure 可能带来一些好处。比如 C++ 实现一个 identity function 其实是为每种类型都会产生一个版本的,这也导致有人提出一些优化,比如对所有的指针,我们可以进行特化,让他们共用一个实现,比如里面都是 void* 表示的指针,但是返回的时候将类型信息加上去。那么 Java 的话大家都是 Object,因此可以提供这样的 functor 的时候不管什么类型 identity function 都是用 Object 对应的版本,我们只需要强制类型转换一下然后关掉对应的 warning 就行了。

在实现一些 generic method 的时候我们也可以用 extends/super 等控制类型的上下界,这意味着我们提供一系列的 generic method 的时候可以在类型上进行区分限制,,比如 Collections 里面不少 generic function 可以要求 T extends Comparable<T>,这样就能实现类似 C++ 里面类似 concept checking 的概念了。

generic method 的参数设计(这里主要指参数本身也是 generic type)还是很有讲究的,比如你设计了一个 Collection,比如叫 Foo<T>,希望能将任意的 Iterable 里面的东西都加入到自己的容器,如果直接写 Iterable<T> 作为 addAll 的参数类型就会限制太大,因为如果别人有一个 List<Integer> 希望加入到 Foo<Number>,由于参数不匹配,就会产生编译错误,因此你得改成 Iterable<? extends T>。类似如果你希望将数据导出去,Collection<T> 也将不是最合理,因为你限制了存放到更广义的类的容器里面,你需要 Collection<? super T>。也就是说 generic method 的参数应该考虑是你的 consumer/producer,往往你需要在 consumer 侧通过 super 使得更广义的类能够 consume 你的结果;在 producer 侧通过 extends 使得更具体的实现也能被你接受。这称为 PECS 原则(producer extends/consumer super),也被称为 get and put principle。

当然这个原则使用起来似乎对参数比较适用,对返回类型来说需要慎重。比如你希望将 T 类型两个 Set 进行 union,返回的类型自然还是 Set<T>,可是根据以上原则,似乎输入应该写成 Set<? extends T> 而返回的是 Set<? super T>,这就有点问题了,因为这意味着 client 其实不知道你返回的是啥类型因此只能也是用 wildcard,为了让 client 明确,这里一般还是写 Set<T>。

在 type inference 有歧义解决不了的时候,你需要手工指定这个 generic type,但这发生的机会比较少。一般是在方法名前通过 <SomeClass> 指定应该使用的类的。

对于自指的 interface(类似 CRTP),如 Comparable<T>、Comparator<T> 等应该根据其实都应该是 ? extends T 使得比较是能够 cover child class 的,但是还需要意识到 Comparable 这类本身还是 producer,因此即便写一个简单的 max,它的 type 其实都非常复杂,T extends Comparable<? super T>,而参数是 ? extends T。这个类型的问题在使用 java.util.concurrent 里面的 Delayed 的时候有所体会,Delayed 本身要求实现 Comparable<Delayed>,而用户实现的 Delayed 因此不仅需要跟自己比,in general 的还需要与别的 Delayed 对象比较的。

? 引入的问题有时候可以通过内部一个 generic type 的 helper function 来辅助实现,前者提供了 client-friendly interface 而后者保证了 type safe。

运行时与编译时 type safety

往往我们还是需要运行时类型,这时就需要使用类型本身作为参数进行传递,通过这个类型对象进行需要的 casting 等,可以在语义上实现 runtime type-safe 的 heterogeneous container。Java 语言并不能保证这点。

使用 enum 而应避免 int 的常数

使用 int 常值表示 enum 类似的问题是因为它支持 +- 等算术运算,而实际这些运算并没有相应的“语义”。Java 的 enum 可以包含数据,这意味着我们可以将 enum 作为一种类似表格的东西表征一类(有限个)相关物体的属性,当然,这要求我们为这个 enum 提供对应的构造函数、成员函数和成员,用来存储这些信息。这样 Java 的 enum 可以发挥比 int 更强大的作用。在声明 enum 时后面加 () 传递的参数就是对应构造函数的参数了。

enum 还支持提供一些“方法”(指不是获得属性的方法,而是每个值可以不同实现的方法),这一般是在 enum 类声明一个 abtract 函数,然后在 enum 值(如果有构造函数是在 () 之后)后通过 {} block 实现对应的版本。这更接近于提供某个 abstract class,每个 child class 对应 enum 的值。兴许 scala 使用的 case class 就是利用了这个特性实现的。enum 的构造函数应该是私有的(保证只能产生 compile-time 声明的那些值)。

enum 的 toString 默认和 face name 一样,即 你写的如是 Foo.BAR,其中 BAR 就是程序里面的 face name,你可能为其分配一个 String representation,比如 bar,这样 toString 往往输出这个 bar,如果不提供 toString 就会输出 face name,即 BAR,因此如果你希望从 String 转换回 enum 那就最好通过 fromString 来做。另外enum 本身提供了 valueOf 是从 face name 转换到 enum 的,不建议直接覆盖它。

使用 enum 切记!

  • 一个是使用 ordinal(返回 enum 的顺序位置),最好存在某个成员里面;
  • 多用 EnumSet 而不要用 bit set,这个常见于很多 C/C++ 程序里面将一些可多选的参数放在一个 int 里面,然后通过位操作获取。EnumSet 可以为你生成本质上是 Set<YourEnum>(是 Set 的一种实现)的结构。
  • 使用 EnumMap 进行归类而避免使用 ordinal() 返回值进行归类,这常见与需要按照 enum 分成几个 partition 的问题上,EnumMap 和 EnumSet 类似,但是实现的是一个 Map
  • 写 public enum Foo 等价于继承了 Enum(可是似乎 Enum<Foo> 的定义有点怪哈…),写 T extends 的时候一般会写 T extends Enum<T> 表示这是个 enum

可扩展的 enum

借用前面的想法,我们可以弄一个 interface,然后为 enum 每个情况实现,这样我们的 enum 在这个 interface 下就是可以扩展的了,扩展并不见得是 enum,当然可以是(可以同时判断父类条件如 T extends Enum<T> & SomeInterface),这样通过 generic 就能控制接受的类型范围。

——————
And he made them a feast, and they did eat and drink.

Advertisements
Java 的 idiom(6)

发表评论

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