Java 的 idiom(3)

这部分讨论一些 Object 公有的方法,往往重写这些函数是比较 non-trivial 的,慎重慎重。

重写 equals

一般来说只有类似“值”这种概念才需要重写这个函数,一般的其实都是比较引用,而不是比较“值”,同时需要说一下的是一般值最好弄成 immutable(Java 没有从语言上支持这个东西的概念,只能通过去掉 setter 等一些会修改内部状态的方法做到,而 C++ 可以利用 const 修饰)。同时这类对象一般都应该弄成 final(不产生子类),因为父类定义好了合适的 equal 后,子类对其 override 会产生很奇怪的现象。某些情况下,如果藏得比较好(private 或者 package private)的类,其使用不会被误碰。某些类你完全不希望被使用 equals 的话应该重写,在里面抛出 AssertionError。

那么对于一个逻辑上需要 equals 的类,我们应该遵循一些什么合约呢?其实数学里面对等价这个事情有很清楚的描述:自返、对称、传递,对程序来说还需要 consistent(前后一致)。那么一般写这个函数我们可以按照下面的步骤来做:

  • 先和自己比较 == this
  • 然后看看类型是否正确 instanceof
  • 类型转换到当前类型
  • 对所有 significant 的成员进行比较(这里有个问题是万一其中某个成员为 null 怎么办?guava 提供了一个 Objects.equals 能帮你简化这个比较)或者简单的写成 foo == null? o.foo == null : foo.equal (o.foo) ;

注意需要 Override 的函数签名是 equals (Object o) 而不是别的类型(没办法… java 就要这么来),通常 equals 重写了之后你也得重写 hashCode。这个方法某种意义上和 operator== 对等,但是 C++ 有 static type checking,你可以只为“有必要”的类进行比较,而其他的 case 编译器会帮你拒绝。

重写 hashCode

至于为什么要重写 hashCode 其实这个逻辑很傻,因为没准谁把这个类放在 hash table 里面做 key,这样一来似乎相等的两个玩意为啥最后对应在 HashMap 里面两个记录呢?Okay 开始觉得有点必要了。不过窃以为这里 Java 又犯了事,因为 hashCode 只返回 int。先不管这个,似乎我们需要满足的 contract 就是如果“值”一样,那么 hash 也应该一样,有了这个 sense 其实无非就是想方法为一个对象产生合理的 hash,同时保证“值”一样的时候 hash 一样就行。我们知道基本类型的 hash 已经有了(55 primitive 不是对象,结果只能通过对应的对象版本,比如 Long.valueOf (a).hashCode (),别哭哈,谁叫你用 java 呢!),因此一般做法就是对成员分别调用 hashCode,然后将其拼接起来,一个策略就是先取个质数,记为 r,对每个成分获得的 hash 为 f,另外找个质数 c,每加入一个成员就如下更新 r \gets r f + c,对应的 f 也有一些取巧的策略,如 boolean 就取 01,短的直接当 int,长的取高位与低位的异或,浮点数还是通过 Float 和 Double 转换的策略(尽量用取 bit 的操作)来吧… 如果计算一个 hash code 实在过于 heavy 可以用 lazy 策略。

比较一下 C++ 里面的 hash,就说 boost.hash 吧,与此比较还是好多了。返回值类似但是是 std::size_t,通过模板特化,我们可以很自然获得基本类型和一些特殊情形的 hash 值,对 POD,我们甚至可以直接用对 char[] 的 hash 来做得到需要的结果,为一个自定义的类型提供 hash,我们只需要在 boost.hash 的 namespace 里面特化对应的 hash_value 函数即可,这是一种 non-intrusive 的设计,而 Java 会导致 hash 本身和对象、HashMap 耦合在一起。更广义的我们可以将 hash function 当做一般的 functor 从而通过 type inference 在 compile-time 拿到某种 hash 的值的类型,而并不局限在 std::size_t 上。

记得重写 toString

这个函数主要是能帮助你将对象输出到 log 等地方,重写它有助于让这些转换后的表示更直观。但是切记不要用这个来做 serialization(对应有 Serializable 接口)。记得在文档中介绍 toString 的格式以便他人理解。通常表示里面的成分最好都能通过一些 getter 获得对应的信息。

谨慎的处理 clone

这个是 protected 方法,你可能说实现了又如何(我想 clone 也没法调用啊,难道到子类里面去提升到 public?没错…)?事实上 Java 甚至提供了一个叫 Clonable 的接口,里面却什么方法都没有(嗯,没有方法!连 protected 都没有),有时候我被人说写 code 不好懂,可是看看 JDK 这玩意也不见得写的多么好,怎么就觉得和 STL 之类的差了这么多…

clone 其实没有什么 contract 一般来说同一个 type 的就行了,从语义上来看,两者应该是代表同一个东西的两个对象,对应于内存里面不同的两个 copy,而不是两个引用。虽然 Object 定义返回类型为 Object,但是子类现在(JDK 5 之后)可以使用更具体的类来 override 返回类型了(总算没丢脸)。

那么什么时候我们如何使用 Clonable 呢?如果你的类里面只有 primitive type,那么恭喜你,直接 super.clone() 就 work 了(你得处理 CloneNotSupportedException,这是个 checked exception),但是如果有引用,那只会 copy 引用而非对象本身。对于引用我们往往需要调用对应的 clone 方法(nnd 它没提供…,我比较奇怪为啥不是说此方法是 protected 无权限访问呢?),如果这个对象的成员是 final 的引用,那么恭喜你,这也不能用了… 这又是 Java 的一大问题 clone 的设计与 final 不兼容(咋觉得写 Effective Java 的人其实是想 bs 一下 Java)。

在处理链表等结构时,需要递归的调用对应的 deep copy 函数,其 clone 实现也比较 tricky。C++ 和 Java 都可以通过 copy constructor/factory 来实现类似 clone 的行为。

另外让人费解的是 Java 在 override 一个函数的时候,似乎对 throws 的异常只允许更细致,也就是说如果父类抛出 Exception,那么子类最多抛出 Exception 的子类,而不能抛出非子类异常(参见这里),这里通过 Clonable 将 clone 方法弄成 public 的时候,居然可以去掉这个 throws… 这是一个 general 的 rule 吗?

考虑实现 Comparable

注意这个是个 generic 的 interface(好事… 不过也是坏事!),返回值是 int(天哪我两个 long 比较难道还要转换到 int?)

总觉得 Java 实现的这个 Comparable 有点怪,像 sort 等需要进行比较的算法需要一个序怎么办?Java 说那还不简单,于是又弄了一个 Comparator 的接口(包括两个对象的比较,equals,啥?为啥还要 equals…)。还是说 Comparable,需要实现 comparedTo,往往是类似 different 之后取符号的结果,一般认为返回 0 应该和 equals 表意一致。比如 TreeSet 之类的会依赖这个接口,如果与 equals 不一致可能会导致“值”一样的出现了两个 entry。那么有多个成员的时候往往需要决定一個比较的顺序。

看看 C++ 做的,sort 是个 template,默认使用 operator<,也接受类似  std::less_than 这种 functor,同时可以通过 boost.operators 提供的 mixin。为啥 Java 搞得乌七八糟两个 interface 呢?

—————–
And he builded an altar there, and called upon the name of the LORD, and pitched his tent there: and there Isaac’s servants digged a well.

Advertisements
Java 的 idiom(3)

一个有关“Java 的 idiom(3)”的想法

    1. zt 说:

      好吧 jc 你说对了,难道 instanceof 之后再加 getClass 看是不是相等么?

      不过你的例子看起来也能接受,就是不对称了,或者把需要 equals 的类弄成 final 的,一般都是“值”,不存在继承什么的需要,这样你的 case 也就不存在了,娃哈哈

      1. guojc 说:

        光getClass 也不行, 要这么写
        在界面上添加 canEqual(c:Object)方法,由子类决定是否可以跟父类相等。

        if (b.asInstanceOf(self.getClass) ){
        if (self.getClass==b.getClass){
        self.content==b.content
        }else{
        b.canEqual(self)&&self.content==b.content
        }

        这个问题主要出在容器类里面,要是传递容器例如hashSet这类,你不这么搞就等着好戏吧。
        java的generic绝对是设计的大败笔。

      2. zt 说:

        不过总感觉要是“值”类型搞继承有点怪啊,要继承了肯定是能比较的;除非“非值”,但是这样的话要能比较就比较奇怪了

      3. guojc 说:

        主要是有给值类型添加方法的问题和扩展值类型的问题。 特别是把两个类似的接口给黏起来时。

发表评论

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