Effective Java(3rd)-Item14 考虑实现Comparable

  与本章讨论的其他方法不同,compareTo方法没有在Object中声明。相反,它是Comparable接口的唯一方法。它的特征与Object的equals方法类似,除了它允许在简单的相等比较之外的顺序比较,并且它是通用的。通过实现Comparable,类表示它的实例由自然顺序。实现了Comparable的对象数组进行排序就这么简单:
Arrays.sort(a);
  它同样易于搜索,计算极值,自动维护已排序的Comparable对象的集合。例如,下列程序,依赖于String实现了Comparable,进行在控制台打印按顺序的字母表并删除重复项:


Effective Java(3rd)-Item14 考虑实现Comparable_第1张图片
image.png

  通过实现Comparable,你就能允许你的类可以依赖这个接口的所有许多通用算法与集合实现进行交互操作。小小的努力就可以获取巨大的力量。实际上在Java平台库上所有值类以及所有的枚举类型 (item34) ,都实现了Comparable。如果你正在编写一个明显自然顺序的值类,比如字母顺序,数字顺序或时间顺序,你就应该实现Comparable接口:

Effective Java(3rd)-Item14 考虑实现Comparable_第2张图片
image.png

  compareTo方法的一般契约与equals相似:
将此对象与指定的对象作比较以获得顺序。返回负整数,零或者正整数分别作为小于,等于,大于指定的对象。如果指定的对象的类型阻止与这个对象作比较,将抛出ClassCastException。
  在以下描述中,符号sgn(表达式)指定数学符号函数,定义为根据表达式的负,零,正返回-1,0,1。

  • 实现者必须确保所有的x与y都满足sgn(x.compareTo(y)) == -sgn(y. compareTo(x))。(这意味着x.compareTo(y)仅当y.compareTo(x)抛出异常时,x.compareTo(y)必须抛出异常。)
  • 实现者必须确保关系是传递的:(x. compareTo(y) > 0 && y.compareTo(z) > 0) 意味着 x.compareTo(z) > 0.
  • 最后,实现者必须确保 x.compareTo(y) == 0 时意味着对于所有z,sgn(x.compareTo(z)) == sgn(y.compareTo(z))。
  • 强烈建议,但不要求,(x.compareTo(y) == 0) == (x.equals(y))。一般来说,任何实现了Comparable接口的类并违反了这个条件应该清楚表明这个事实。建议的语言是“注意:此类有与equals不一致的自然顺序”。
      不要被这个契约的数学性质所难住。与equals契约 (item10) 一样,这个契约不像它看起来得那么复杂。与equals方法不同,equals方法对所有对象强加全局等价关系,但是compareTo不需要跨越不同类型的对象:当面对不同类型的对象时,允许compareTo抛出ClassCastException。通常,这正是它的作用。契约确实允许交互式比较,这通过在被比较的对象实现的接口中定义。
      就好像类违反了hashCode契约会破坏其他依赖hash的类,一个类违反了compareTo契约会破坏其他依赖于比较的类。依赖于比较的类包括已排序的集合TreeSet和TreeMap和包含搜索和排序的工具类Collections和Arrays。
      让我们仔细看看compareTo契约的规定。第一个规定说如果你反转两个对象引用之间的比较方向,预期的事情就会发生:如果第一个对象小于第二个,那么第二个必须比第一个大;如果第一个对象与第二个对象相等,第二个必须与第一个相等;如果第一个对象比第二个大,第二个必须比第一个小。第二个规定说如果一个对象比第二个大,第二个比第三个大,那么第一个必须比第三个大。最后规定说的是所有对象比较相等的对象必须产生相同的结果。
      这三个规定的一个结果是compareTo方法施加的相等性测试必须遵守契约所施加的相同限制:自反性,对称性和传递性。因此,同样的警告适用:没有办法在保留compareTo契约下使用新值组件来继承一个可实例化的类,除非你希望放弃面向对象抽象的好处 (item10) .相同的解决方法也适用。如果你想要实现Comparable的类中添加值组件,不要继承它;编写一个包含第一个类的实例的无关类。然后提供一个“视图”方法返回包含的实例。这使得你在包含的类上实现任何compareTo方法,同时允许它的客户端作为包含类的实例查看包含类的实例。
      compareTo契约的最后一段是一个强烈建议而不是一个真正的要求,只是说明compareTo方法所施加的相等测试通过应该返回与equals方法相同的结果。如果遵守这个规定,compareTo方法强加的顺序与equals一致。如果违反,说明顺序和equals不一致。compareTo方法强加与equals不一致的顺序的类仍然可以工作,但是包含该类元素的已排序的集合可能不能最受相应集合接口(Collection, Set, 和 Map)的常规约定。这是因为接口的常规约定是根据equals方法定义的,但是有序集合使用compareTo的强加的相等性测试替代equals。如果发生情况,这不是灾难,但有些事情需要注意。
      例如,考虑类BigDecimal,它的compare方法就与equals方法不一致。如果你创建了一个空的HashSet实例然后添加了BigDecimal("1.0") ,然后添加BigDecimal("1.00"),set将包含两个元素,因为这两个BigDecimal实例使用equals方法比较时,是不相等的。但是,如果使用TreeSet而不是HashSet执行相同过程,set将只会包含一个元素,因为两个BigDecimal用compareTo方法比较是相等的(有关详细信息,参阅BigDecimal文档)。
      编写一个compareTo方法与编写equals方法类型,但是有一些关键差异。因为Comparable是参数化的,compareTo方法是静态类型的,所以你不需要类型检查或强制转换参数。如果参数类型错误,调用甚至不会编译。如果参数是空,调用应该会抛出NullPointerException,一旦方法尝试访问其成员,它就会抛出。
      在compareTo方法中,字段比较顺序而不是相等。为了比较对象引用字段,递归调用compareTo方法。如果字段并不实现Comparable或者你需要非标准排序,使用Comparator来代替。你可以编写你自己的Comparator或者使用线程的,正如item10中的CaseInsensitiveString的compareTo方法:
    Effective Java(3rd)-Item14 考虑实现Comparable_第3张图片
    image.png

  注意到CaseInsensitiveString实现了Comparable,这意味着一个CaseInsensitiveString引用只能与另一个CaseInsensitiveString引用比较。这是声明一个类来实现Comparable所遵循的正常模式。
  本书的先前版本建议compareTo方法使用关系符<和>比较原生整数类型,使用静态方法Double.compare和Float.compare比较浮点字段。在Java7中,静态比较方法被添加到所有Java装箱原生类中。在compareTo方法中使用关系符<和>是冗长且容易出错,不再推荐使用
  如果类有多个重要字段,比较它们的顺序至关重要。从最重要的字段开始,逐步完成。如果比较产生除了零以外的任何东西(代表相等),你就完成了;只返回结果。如果你最重要的字段相等,比较下一个最重要的字段,以此类推,直到你找到了不相等的字段或比较最不重要的字段。这是item11中PhoneNumber类的compare方法,演示了这个技术:

Effective Java(3rd)-Item14 考虑实现Comparable_第4张图片
image.png

  在java8中,Comparator接口配备了一组comparator构造方法,可以精确构建comparator。这些comparator可以被用来实现compareTo方法,这是Comparable接口所要求的。许多程序员更喜欢这种方法的简洁性,虽然它带来了一些性能成本:在我的机器上排序PhoneNumber实例慢了大约10%。使用该方法时,考虑使用Java的静态导入工具,以便于可以通过简单的名称引用静态comparator构造方法,以简化和简洁。这是使用compareTo方法的PhoneNumber的样子:


Effective Java(3rd)-Item14 考虑实现Comparable_第5张图片
image.png

  这个实现使用两个comparator构造方法在类初始化时构建了一个comparator。第一个是comparingInt。它是静态方法,接受一个键提取函数,映射对象引用到int类型的键,返回comparator,该comparator根据键对实例进行排序。在先前的例子中,comparingInt采用lambda表达式从PhoneNumber中提取区域代码,并返回Comparator,根据区号排序。注意到lambda显示指定输入的参数类型(PhoneNumber pn)。事实证明,在这种情况下,Java的类型推断不足以为自己确定类型,所以我们被迫帮它使得程序编译。
  如果两个电话号码有相同的区号,我们需要更深一步地比较,这正是第二个comparator构造方法,thenComparingInt。它是Comparator上地一个实例方法,它接受一个int key提取器函数,返回一个comparator,该comparator首先应用原始comparator,然后使用提取的key来断开关系。你可以累积调用更多请求只要你喜欢,从而产生字典顺序。在上面的示例中,我们类似两个thenComparingInt调用,产生一个排序,其二级密钥是前缀,三级密钥是行号。注意到我们没有必要指定传递给thenComparingInt的任一调用的键提取函数的参数类型:Java的类型推断足够聪明,可以自己觉得这个问题。
  Comparator类具有一个完整的构造方法。对于原始类long和double,有comparingInt和thenComparingInt的相体。int版本也可以用作较窄的整数类型,比如short,如PhoneNumber例子所示。double版本也可以为float使用。这提供了Java的数字原始类型的全覆盖。
  还有对象引用的comparator构造方法。静态方法,名为comparing,有两个重载。
  一个需要一个关键的提取器,使用键的自然顺序。第二个采取密钥提取器和comparator用于提取密钥。实例有三个重载,名为thenComparing。一次重载仅使用comparator并使用它来提供二级订单。第二次重载仅使用一个密钥提取器,并使用密钥的自然顺序作为二级订单。 最后的重载需要一个关键提取器和一个比较器用于提取的键。
  有时候你可能会看到compareTo或比较方法,依赖于两个值之间的差异为负值,如果第一个值比第二个小,如果两个值相等,值为零,第一个更大则为正值,这是一个例子:


Effective Java(3rd)-Item14 考虑实现Comparable_第6张图片
image.png

  不要使用这种技术。它充满了整数溢出的危险,以及 IEEE 754 浮点运算伪像的危险 [JLS 15.20.1, 15.21.1]。此外,方法结果不可能比本项目表述的技术编写的方法快得多。使用静态方法比较:


Effective Java(3rd)-Item14 考虑实现Comparable_第7张图片
image.png

或comparator构造方法:


image.png

  总结,当你实现一个值类有合理的排序时,你应该让类实现Comparable接口,这样它的类在基于比较的集合中容易排序,搜索和使用。使用compareTo方法实现比较字段值时,避免使用<和>操作符。相反,在装箱原生类型中使用静态比较方法或者使用comparator构造方法
本文写于2019.3.17,历时2天

你可能感兴趣的:(Effective Java(3rd)-Item14 考虑实现Comparable)