前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Effective Java(第三版)——条目十四:考虑实现Comparable接口

Effective Java(第三版)——条目十四:考虑实现Comparable接口

作者头像
每天学Java
发布2020-06-01 17:26:50
6460
发布2020-06-01 17:26:50
举报
文章被收录于专栏:每天学Java每天学Java

今天就周五了,提前跟小伙伴们说一声周末愉快,有加班的小伙伴们,嘿嘿,加班愉快。今天给大家带来的是,考虑实现Comparable接口

在文章中如果有红色括号括起来的,是本人自己的理解,望大家注意这一点哦

01

Comparable接口

与本章讨论的其他方法不同,compareTo方法并没有在Object类中声明。 相反,它是Comparable接口中的唯一方法。 它与Object类的equals方法在性质上是相似的,除了它允许在简单的相等比较之外的顺序比较,它是泛型的。 通过实现Comparable接口,一个类表明它的实例有一个自然顺序(有没有想到在 Stream(二) 这篇文章里面,我们说到Stream有一方法sorted()就是按照自然顺序排序的)。 对实现Comparable接口的对象数组排序非常简单,如下所示:

代码语言:javascript
复制
Arrays.sort(a);

它很容易查找,计算极端数值,以及维护Comparable对象集合的自动排序。例如,在下面的代码中,依赖于String类实现了Comparable接口,去除命令行参数输入重复的字符串,并按照字母顺序排序:

代码语言:javascript
复制
public class WordList {
    public static void main(String[] args) {
        Set<String> s = new TreeSet<>();
        Collections.addAll(s, args);
        System.out.println(s);
    }
}

通过实现Comparable接口,可以让你的类与所有依赖此接口的通用算法和集合实现进行互操作。 只需少量的努力就可以获得明显的效果。 几乎Java平台类库中的所有值类以及所有枚举类型(条目 34)都实现了Comparable接口。 如果你正在编写具有明显自然顺序(如字母顺序,数字顺序或时间顺序)的值类,则应该实现Comparable接口:

代码语言:javascript
复制
public interface Comparable<T> {
    int compareTo(T t);
}

compareTo方法的通用约定与equals相似:

将此对象与指定的对象按照排序进行比较。 返回值可能为负整数,零或正整数,因为此对象对应小于,等于或大于指定的对象。 如果指定对象的类型与此对象不能进行比较,则引发ClassCastException异常(类转换异常)。

下面的描述中,符号sgn(expression)表示数学中的 signum 函数,它根据表达式的值为负数、零、正数,对应返回-1、0和1

1.实现类必须确保所有x和y都满足sgn(x.compareTo(y)) == -sgn(y. compareTo(x))。 (这意味着当且仅当y.compareTo(x)抛出异常时,x.compareTo(y)必须抛出异常。) 2.实现类还必须确保该关系是可传递的:(x. compareTo(y) > 0 && y.compareTo(z) > 0)意味着x.compareTo(z) > 0 3.最后,对于所有的z,实现类必须确保[x.compareTo(y) == 0意味着sgn(x.compareTo(z)) == sgn(y.compareTo(z)) 4.强烈推荐,但不要求这样写:(x.compareTo(y) == 0) == (x.equals(y))。 一般来说,任何实现了Comparable接口的类违反了这个条件都应该清楚地说明这个事实。 推荐的语言是“注意:这个类有一个自然顺序,与equals不一致”。

与equals方法一样,不要因为上述约定的数学特性2而退缩。这个约定并不像看起来那么复杂。 与equals方法不同,equals方法在所有对象上施加了全局等价关系,compareTo不必跨越不同类型的对象:当遇到不同类型的对象时,compareTo被允许抛出ClassCastException异常。 通常,这正是它所做的。 约定确实允许进行不同类型间比较,这种比较通常在由被比较的对象实现的接口中定义。

如一个违反hashCode约定的类可能会破坏依赖于哈希的其他类一样,违反compareTo约定的类可能会破坏依赖于比较的其他类。 依赖于比较的类,包括排序后的集合TreeSet和TreeMap类,以及包含搜索和排序算法的实用程序类Collections和Arrays

我们来看看compareTo约定的规定。 第一条规定,如果反转两个对象引用之间的比较方向,则会发生预期的事情:如果第一个对象小于第二个对象,那么第二个对象必须大于第一个; 如果第一个对象等于第二个,那么第二个对象必须等于第一个; 如果第一个对象大于第二个,那么第二个必须小于第一个。 第二项约定说,如果一个对象大于第二个对象,而第二个对象大于第三个对象,则第一个对象必须大于第三个对象。 最后一条规定,所有比较相等的对象与任何其他对象相比,都必须得到相同的结果

这三条规定的一个结果是,compareTo方法所实施的平等测试必须遵守equals方法约定所施加的相同限制:自反性,对称性和传递性。 因此,同样需要注意的是:除非你愿意放弃面向对象抽象(条目 10)的好处,否则无法在保留compareTo约定的情况下使用新的值组件继承可实例化的类。 同样的解决方法也适用。 如果要将值组件添加到实现Comparable的类中,请不要继承它;编写一个包含第一个类实例的不相关的类。 然后提供一个返回包含实例的“视图”方法。 这使你可以在包含类上实现任何compareTo方法,同时客户端在需要时,把包含类的实例视同以一个类的实例

compareTo约定的最后一段是一个强烈的建议,而不是一个真正的要求,只是声明compareTo方法施加的相等性测试,通常应该返回与equals方法相同的结果。 如果遵守这个约定,则compareTo方法施加的顺序被认为与equals相一致。 如果违反,顺序关系被认为与equals不一致。 其compareTo方法施加与equals不一致顺序关系的类仍然有效,但包含该类元素的有序集合可能不服从相应集合接口(Collection,Set或Map)的一般约定。 这是因为这些接口的通用约定是用equals方法定义的,但是排序后的集合使用compareTo强加的相等性测试来代替equals。 如果发生这种情况,虽然不是一场灾难,但仍是一件值得注意的事情

例如,考虑BigDecimal类,其compareTo方法与equals不一致。 如果你创建一个空的HashSet实例,然后添加new BigDecimal("1.0")和new BigDecimal("1.00"),则该集合将包含两个元素,因为与equals方法进行比较时,添加到集合的两个BigDecimal实例是不相等的。 但是,如果使用TreeSet而不是HashSet执行相同的过程,则该集合将只包含一个元素,因为使用compareTo方法进行比较时,两个BigDecimal实例是相等的。 (有关详细信息,请参阅BigDecimal文档。)

编写compareTo方法与编写equals方法类似,但是有一些关键的区别。 因为Comparable接口是参数化的,compareTo方法是静态类型的,所以你不需要输入检查或者转换它的参数。 如果参数是错误的类型,那么调用将不会编译。 如果参数为null,则调用应该抛出一个NullPointerException异常,并且一旦该方法尝试访问其成员,它就会立即抛出这个异常。

在compareTo方法中,比较属性的顺序而不是相等。 要比较对象引用属性,请递归调用compareTo方法。 如果一个属性没有实现Comparable,或者你需要一个非标准的顺序,那么使用Comparator接口。 可以编写自己的比较器或使用现有的比较器,如在条目 10中的CaseInsensitiveString类的compareTo方法中:

代码语言:javascript
复制
// Single-field Comparable with object reference field
public final class CaseInsensitiveString
        implements Comparable<CaseInsensitiveString> {
    public int compareTo(CaseInsensitiveString cis) {
        return String.CASE_INSENSITIVE_[ORDER.compare(s](http://ORDER.compare(s), cis.s);
    }
    ... // Remainder omitted
}

请注意,CaseInsensitiveString类实现了Comparable <CaseInsensitiveString>接口。 这意味着CaseInsensitiveString引用只能与另一个CaseInsensitiveString引用进行比较。 当声明一个类来实现Comparable接口时,这是正常模式

在本书第二版中,曾经推荐如果比较整型基本类型的属性,使用关系运算符“<” 和 “>”,对于浮点类型基本类型的属性,使用Double.compare和[Float.compare静态方法。在Java 7中,静态比较方法被添加到Java的所有包装类中。 在compareTo方法中使用关系运算符“<” 和“>”是冗长且容易出错的,不再推荐

如果一个类有多个重要的属性,那么比较他们的顺序是至关重要的。 从最重要的属性开始,逐步比较所有的重要属性。 如果比较结果不是零(零表示相等),则表示比较完成; 只是返回结果。 如果最重要的字段是相等的,比较下一个重要的属性,依此类推,直到找到不相等的属性或比较剩余不那么重要的属性。 以下是条目 11中PhoneNumber类的compareTo方法,演示了这种方法

代码语言:javascript
复制
// Multiple-field Comparable with primitive fields
public int compareTo(PhoneNumber pn) {
    int result = [Short.compare(areaCode](http://Short.compare(areaCode), pn.areaCode);
    if (result == 0)  {
        result = [Short.compare(prefix](http://Short.compare(prefix), pn.prefix);
        if (result == 0)
            result = [Short.compare(lineNum](http://Short.compare(lineNum), pn.lineNum);
    }
    return result;
}

在Java 8中Comparator接口提供了一系列比较器方法,可以使比较器流畅地构建。 这些比较器可以用来实现compareTo方法,就像Comparable接口所要求的那样。 许多程序员更喜欢这种方法的简洁性,尽管它的性能并不出众:在我的机器上排序PhoneNumber实例的数组速度慢了大约10%。 在使用这种方法时,考虑使用Java的静态导入,以便可以通过其简单名称来引用比较器静态方法,以使其清晰简洁。 以下是PhoneNumber的compareTo方法的使用方法.

代码语言:javascript
复制
// Comparable with comparator construction methods
private static final Comparator<PhoneNumber> COMPARATOR =
        comparingInt((PhoneNumber pn) -> pn.areaCode)
          .thenComparingInt(pn -> pn.prefix)
          .thenComparingInt(pn -> pn.lineNum);

public int compareTo(PhoneNumber pn) {
    return [COMPARATOR.compare(this](http://COMPARATOR.compare(this), pn);
}

此实现在类初始化时构建比较器,使用两个比较器构建方法。第一个是comparingInt方法。它是一个静态方法,它使用一个键提取器函数式接口( key extractor function)作为参数,将对象引用映射为int类型的键,并返回一个根据该键排序的实例的比较器。在前面的示例中,comparingInt方法使用lambda表达式,它从PhoneNumber中提取区域代码,并返回一个Comparator<PhoneNumber>,根据它们的区域代码来排序电话号码。注意,lambda表达式显式指定了其输入参数的类型(PhoneNumber pn)。事实证明,在这种情况下,Java的类型推断功能不够强大,无法自行判断类型,因此我们不得不帮助它以使程序编译

如果两个电话号码实例具有相同的区号,则需要进一步细化比较,这正是第二个比较器构建方法,即thenComparingInt方法做的。 它是Comparator上的一个实例方法,接受一个int类型键提取器函数式接口( key extractor function)作为参数,并返回一个比较器,该比较器首先应用原始比较器,然后使用提取的键来打破连接。 你可以按照喜欢的方式多次调用thenComparingInt方法,从而产生一个字典顺序。 在上面的例子中,我们将两个调用叠加到thenComparingInt,产生一个排序,它的二级键是prefix,而其三级键是lineNum。 请注意,我们不必指定传递给thenComparingInt的任何一个调用的键提取器函数式接口的参数类型:Java的类型推断足够聪明,可以自己推断出参数的类型

Comparator类具有完整的构建方法。对于long和double基本类型,也有对应的类似于comparingInt和thenComparingInt的方法,int版本的方法也可以应用于取值范围小于 int的类型上,如short类型,如PhoneNumber实例中所示。对于double版本的方法也可以用在float类型上。这提供了所有Java的基本数字类型的覆盖。

也有对象引用类型的比较器构建方法。静态方法comparing有两个重载方式。第一个方法使用键提取器函数式接口并按键的自然顺序。第二种方法是键提取器函数式接口和比较器,用于键的排序。thenComparing方法有三种重载。第一个重载只需要一个比较器,并使用它来提供一个二级排序。第二次重载只需要一个键提取器函数式接口,并使用键的自然顺序作为二级排序。最后的重载方法同时使用一个键提取器函数式接口和一个比较器来用在提取的键上

有时,你可能会看到compareTo或compare方法依赖于两个值之间的差值,如果第一个值小于第二个值,则为负;如果两个值相等则为零,如果第一个值大于,则为正值。这是一个例子:

代码语言:javascript
复制
// BROKEN difference-based comparator - violates transitivity!

static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
        return o1.hashCode() - o2.hashCode();
    }
};

不要使用这种技术!它可能会导致整数最大长度溢出和IEEE 754浮点运算失真的危险[JLS 15.20.1,15.21.1]。 此外,由此产生的方法不可能比使用上述技术编写的方法快得多。 使用静态compare方法

代码语言:javascript
复制
**// Comparator based on static compare method**
static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
        return Integer.compare(o1.hashCode(), o2.hashCode());
    }
};

产生的或者使用Comparator的构建方法

代码语言:javascript
复制
// Comparator based on Comparator construction method
static Comparator<Object> hashCodeOrder =
        Comparator.comparingInt(o -> o.hashCode());

总而言之,无论何时实现具有合理排序的值类,你都应该让该类实现Comparable接口,以便在基于比较的集合中轻松对其实例进行排序,搜索和使用。 比较compareTo方法的实现中的字段值时,请避免使用"<"和">"运算符。 相反,使用包装类中的静态compare方法或Comparator接口中的构建方法。


跟小伙伴们分享一下这一周的推送安排,大家可以重点关注自己喜欢的文章(已经发布的大家可以直接点击链接进去哦):

九月10号周一:探究Java8的Stream(一)

九月11号周二:探究Java8的Stream(二)

九月12号周三:关系型数据库之oracle

九月13号周四:探究Java8的Optional 类

九月14号周五:Effective Java(第三版)——条目十四:考虑实现Comparable接口

那么今天小程序更新的题库是什么呢?

今天小程序更新的题目是:

1.同步和异步的区别和联系

2. 什么叫脏数据

3. 什么叫不可重复读

4.订票系统案例,某航班只有一张机票,假定有1w个人打开你的网站来订票,如何解决并发问题(可扩展到任何高并发网站要考虑的并发读写问题)

5.股票交易系统、银行系统,大数据量你是如何考虑的

6.常见的提高高并发下访问的效率的手段

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-09-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 每天学Java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云开发 CloudBase
云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档