John Jiang

a cup of Java, cheers!
https://github.com/johnshajiang/blog

   :: 首页 ::  :: 联系 :: 聚合  :: 管理 ::
  131 随笔 :: 1 文章 :: 530 评论 :: 0 Trackbacks
"一致性相等"的陷阱
关于Object类中的equals()方法与Comparable接口中的compareTo()方法之间有何种关联,之前还真没考虑过。通过java.net看到此文之后,收获了一点儿新知识,希望大家也能如此。(2012.12.09最后更新)

    方法equals()与Comparable接口中的compareTo()方法是Java中最基本的两个方法之一,然而它们的定义却围绕着"与相等一致"这一有趣的概念。

equals()方法

    Java中的equals()方法既明确,又模糊。Java清楚地定义了如何准确地检验一个equals()方法是可用的。一个恰当的equals()方法必须是自反的,对称的,可传递的,一致的,并能处理null引用。
    然而equals()方法又是不清晰的。Javadoc说到,该方法指定了其它对象是"等于"这个对象的。注意,"等于"是放在引号中的。此处的关键就是,它没有定义如何去判定这种相等性。
  • 对象的一致性(==)默认是继承自Object类
  • 对象的整体可观测的状态,例如,若两个对象是相等的,那么在应用的其它部分可以用一个对象去替代另一个对象。
  • 对象信息中的某些部分,如ID,使得检验对象相等性在逻辑上是有意义的。
compareTo()方法
     Comparable接口定义了可比较性的概念。Javadoc指出compareTo()方法"强制设定了每个实现了该接口的类的对象的全部顺序"。
     实现了Comparable接口的类有一个天然的排序,这可便于存储,也能在不使用单独的Comparator的情况下,用于像TreeSet和TreeMap这样的集合对象。
     该接口的定义明晰,它要求其实现必须确保对称性与传递性,就像equals()方法那样。

一致性/非一致性相等
Comparable接口有如下描述
    类C的天然排序意味着要与equals()方法保持一致,只有当且仅当e1.compareTo(e2) == 0与e1.equals(e2)有相同的布尔值。
    基本上,这就要求由compareTo()定义的相等性与equals()方法定义的相等性具有相同的概念(除去有null的情况)。乍一看,该要求很简单,但实际上它有其复杂性,后面将会讨论到。
    当考虑到操作符重载时,这种定义就特别有用。若我们假设有一种类Java语言,在这种语言中,==并不表示对象的同一性,而是通过方法去进行比较,大于/小于操作符也是如此,问题是调什么样的方法。在类Java语言中大于/小于天然地就要基于compareTo()方法,而==则要调用equals()方法。
// our new Java-like language
if (a < b) return "Less";      // translation ignoring nulls: if (a.compareTo(b) < 0)
if (a > b) return "Greater";   // translation ignoring nulls: if (a.compareTo(b) > 0)
if (a == b) return "Equal";    // translation ignoring nulls: if (a.equals(b))
throw new Exception("Impossible assuming no nulls?");
    但如果compareTo()方法不是"一致性相等",那么上述代码将会抛出异常,因为当a.equals(b)为false时,a.compareTo(b)会返回0。
    在集合,如TreeMap,中还会发生其它问题:
// Foo class is "inconsistent with equals"
assert foo1.equals(foo2) == false;
assert foo1.compareTo(foo2) == 0;
 
TreeMap
<Foo, String> map = 
map.put(foo1, 
"a");
map.put(foo2, 
"b");
    当使用equals()方法时,这两个对象不相等,但使用compareTo()时,它们却相等。在这种情况下,该Map的元素个数将为1,而非0。
    由于这些"一致性相等"的问题,Javadoc说道"强烈建议(尽管并不要求)天然排序规则要与equals()方法保持一致"。
    JDK中的许多类为了符合"一致性相等"这一规范而实现了Comparable接口。这些类包括Byte,Short,Integer,Long,Character和String。

还有些更有趣的类:
    BigDecimal--肯定是"非一致性相等",比如4.00与4.0不一致,但进行比较时,认为它们是一样的。
    Double/Float--该类显式地提供了排序规则,并为正零和负零,以及NaN都提供了相等性检查,以确保它的compareTo()方法符合"一致性相等"。
    CharSet--该类基于ID或名称。equals()方法对待字段串是大小写敏感的,但compareTo()方法却不这样。虽然名称一般会符合某种标准,但这是一种值得怀疑的"一致性"。
    *Buffer(nio)--该簇类的比较基于缓冲存放的内容,在我的测试中equals()和compareTo()是"一致的"。
    Rdn(ldap)--该类的比较基于状态的标准化格式,因此也是"一致性相等"。
    ObjectStreamField(序列化)--该类的比较基于名称,但会首先对基本数据类型进行排序。因为没有覆盖equals()方法,所以是"非一致性相等"。
    ...
    注意:对于大多数的例子,我都不得不查看其源代码或编写测试程序以确定该类是不是符合"一致性相等"。这儿有一个不错地清理Javadoc和检验UUID equals()方法的Adopt-a-JDK任务。

JSR-310
    一直看到许多关于BigDecimal的问题,已有计划将JSR-310中的类改造成"一致性相等",最近的一些帖子显示这将造成多么大的争议。
    基本上,为某些类定义equals()和compareTo()看起来很容易。LocalDate表示某单一日历系统中的某个日期,所以它有一个显而易见的排序算法和相等规则。LocalTime则表示某个时刻,所以它也有一个明显的排序算法和相等规则。Instant表示时间线上的某个时刻,那么它的排序与相等也是显见的。
    但在其它的情况下,这就不是那么显而易见了。考虑这样一个类OffsetDateTime:
dt1 = OffsetDateTime.parse("2012-11-05T06:00+01:00");
dt2 
= OffsetDateTime.parse("2012-11-05T07:00+02:00");
    这样的两个日期-时刻对象代表时间线上一个相同的时刻点,但它们有不同的本地时,而且相对的UTC/格林威治时间的偏移量也不相同。
    那么就有一个问题要留给读者们...你更倾向于如下哪种观点...
    1. dt1不等于dt2,compareTo()分别比较本地时与偏移量,使用"一致性相等"(使用独立的Comparator基于时刻对其进行排序)。
    2. dt1不等于dt2,compareTo()基于时间线的上时刻点,使用"非一致性相等"。
    3. dt1等于dt2,compareTo()基于时间线的上时刻点,使用"一致性相等"。
    4. dt1等于dt2,且不实现Comparable接口。
    5. dt1不等于dt2,且不实现Comparable接口。
    我个人更倾向于让dt1.equals(dt2)返回true这种方案,但我仍持开放态度。
    顺便地,也可以将这个问题提给BigDecimal,如果你能修改这个类,使其符合"一致性相等",你会修改它的equals()方法,还是compareTo()方法?

posted on 2012-12-06 23:14 John Jiang 阅读(2120) 评论(3)  编辑  收藏 所属分类: JavaSEJava翻译

评论

# re: "一致性相等"的陷阱(译) 2012-12-07 16:20 牌具
受教了。以后不会掉陷阱了  回复  更多评论
  

# re: "一致性相等"的陷阱(译) 2012-12-16 15:36 来如风
我的理解,equals、 是为了测试业务相等规则,比如购物车中两条物品完全相同,包括名称,数量,这就是equals,而compared 则用于业务对象排序,比如按照某个属性在内存中排序,因为替它返回三种结果  回复  更多评论
  

# re: "一致性相等"的陷阱(译) 2012-12-17 13:00 Sha Jiang
@来如风
文章关注于compareTo()返回0的这种情况  回复  更多评论
  


只有注册用户登录后才能发表评论。


网站导航: