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

2018-09-18  本文已影响0人  难以置信的优雅

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


image.png

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

image.png

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

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

image.png

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


image.png

  这个实现使用两个comparator构造方法在类初始化时构建了一个comparator。第一个是comparingInt。它是静态方法,接受一个键提取函数,映射对象引用到int类型的键,返回comparator,该comparator根据键对实例进行排序。在先前的例子中,comparingInt采用lambda表达式从PhoneNumber中提取区域代码,并返回Comparator<PhoneNumber>,根据区号排序。注意到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或比较方法,依赖于两个值之间的差异为负值,如果第一个值比第二个小,如果两个值相等,值为零,第一个更大则为正值,这是一个例子:


image.png

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


image.png

或comparator构造方法:


image.png

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

上一篇 下一篇

猜你喜欢

热点阅读