《Effective Java》学习笔记14 考虑实现Comparable接口

本栏是博主根据如题教材进行Java进阶时所记的笔记,包括对原著的概括、理解,教材代码的报错和运行情况。十分建议看过原著遇到费解地方再来参考或与博主讨论。致敬作者Joshua Bloch和各路翻译者们,以及为我提供可参考博文的博主们。

考虑实现Comparable接口

Comparable接口

{@link  Comparable#compareTo(Object)}方法虽然非常常用,但并没有放在Object类里面,Comparable接口中唯一的方法。这个方法不仅可以实现类似equals的简单等同性比较,还能执行顺序比较,而且支持泛型。实现Comparable接口,就表示它的实例具有内在的顺序关系。

基本使用

对于实现了该接口的对象来说,排序、搜索、计算极值、自动调整顺序等操作都变得很简单。比如当实例数组a所在的类实现了{@link Comparable}接口时,就可以用{@link java.util.Arrays#sort},这样进行排序:Arrays.sort(a);,一句话完事儿。或者可以看这个关于TreeSet的例子{@link WordList},通过compareTo实现了去重。

/**
 * 这个类用于展示实现了{@link Comparable}接口的{@link String}
 * 类型可以很方便地实现去重等操作。设置断点追踪可以看到,通过addAll->
 * add->put->compare->compareTo,最终调用了泛型类(这里是String)的compareTo
 * 方法,然后决定是新添加元素,还是修改原有元素的值。
 *
 * @author LightDance
 */
public class WordList {

    public static void main(String[] args) {
        Set<String> set = new TreeSet<>();
        String [] stringList = new String[5];
        for (int i = 0; i < stringList.length; i++) {
            stringList[i] = "bibibi";
        }
        stringList[1] = "alalal";

        stringList[4] = "bicidi";

        Collections.addAll(set ,stringList);

        System.out.println(set);
    }
}

implementing Comparable接口后,我们自己编写的类就可以与所有依赖这个接口的通用算法和泛型算法协作,而不需要自己动手从头实现,省去了很多不必要的工作。Java中所有的值类(value class)和枚举类型都实现了这个接口。当我们在编写带有明显顺序特征的类的时候,比如涉及字母顺序、时间顺序、数字顺序的类,就应该implements Comparable一下。

通用规约

compareTo()的通用规约跟equals()很像:

  • 将对象和指定对象进行比较,根据两者大小关系返回同一个正整数、零或者负整数(通常用-1,0,1),如果两者无法进行比较则抛出{@link ClassCastException}异常
  • 这里用sgn表示数学中的sig-num函数

     规约如下:

  1. 自己实现的compareTo()方法对任意x,y应有sgn(x.compareTo(y)) = - sgn(y.compareTo(x)),否则就扔一个异常出来
  2. 这种比较关系应该是可以传递的:(x.compareTo(y) > 0 && y.compare(z) > 0 )则应有 (x.compare(z) > 0)对任意满足条件的x,y,z均成立
  3. 当sgn(x.compare(y)) == 0 时,应对任意的z有sgn(x.compare(z)) == sgn(y.compare(z))
  4. 强烈建议(x.compare(y) == 0) == x.equals(y) .虽然并不是强制要求,但如果实现该接口的类违反了这个规则,就有必要在文档中明确地予以说明,比如:“注意:该类的内置排序放方法与equals()方法不一致”这样。这种情况下需要非常小心。

对于第4条的补充说明如下:

如果是一个单独的类出现这种情况可能还好,但如果一个有序集合出现了这种情况就会十分混乱,它们很可能无法遵守集合接口(Collection,Map,Set等)的通用规约。因为这些集合接口的通用规约是基于equals()方法定的,而其等同性测试却基于compareTo()方法。比如这个关于BigDecimal 的例子:

/**
 * 用于说明compareTo(),equals()两个方法不一致时对集合类造成的影响.
 *
 * 由打印结果可知,equals的结果为false,
 * 但compareTo的结果为0,两者实际上是相等的,
 * 导致set中存在了两个值相等的元素。
 *
 * @author LightDance
 */
public class BigDecimalTest {
    public static void main(String[] args) {
        HashSet set = new HashSet();
        BigDecimal decimal1 = new BigDecimal("1.0");
        BigDecimal decimal2 = new BigDecimal("1.00");

        System.out.println(decimal1.equals(decimal2));
        System.out.println(decimal1.compareTo(decimal2));

        set.add(decimal1);
        set.add(decimal2);
        System.out.println(set);
    }

}

与equals()方法不同的是,compareTo()无需对不同的类进行比较。实例的类型不同时扔个{@link ClassCastException}就好。虽然通用规约确实允许实现跨类比较,但这些一般会在被比较对象所实现的接口中定义说明一下。

跟hashCode()类似,如果随意违反compareTo()的通用规约,就会影响那些依赖于该方法的类正常工作,例如{@link java.util.TreeSet},{@link java.util.TreeMap},{@link java.util.Collections},{@link java.util.Arrays}等靠这个方法实现排序和搜索的类。

compareTo()同样要遵循自反性、传递性、对称性,因此在equals中的说明与“权宜之计”换到这里也同样适用:无法在子类中添加新的成员变量的同时保持compareTo通用规约,除非放弃面向对象的优势;可以编写一个不相关的类,持有需要扩展的类的一个实例,然后添加新的成员变量,通过“视图方法”(view method)更自由地实现compareTo()方法

方法编写

编写compareTo()方法和编写equals()挺像的,但有几处关键的区别。由于Comparable接口是参数化的(parameterized),并且在接口中规定了要返回的类型(通过泛型),因此无需考虑类型检测、类型转换问题,如果类型不一致可以直接扔{@link ClassCastException}异常;如果传进来个空指针(null)就应该在试图访问它的内部数据之前扔{@link NullPointerException}出来。

compareTo()方法的侧重点在于顺序关系而非是否相等,比较时应递归地、按顺序地调用两实例中所对应的、各成员变量的compareTo()方法。如果某个成员变量没有实现{@link Comparable}接口,那么可以考虑使用比较器(Comparator).可以考虑自己编写一个Comparator,也可以用现成的({@link java.util.Comparator})。比如讲equals时举的那个不分大小写字符串比较的例子。这里要注意,实现Comparator接口时需要传入一个泛型,即implements Comparator<T>,然后其只能跟同样implements Comparator<T>的类进行比较,一般会要求T相同。

注意事项

注意,《Effective Java 2nd》或之前的版本中,会建议大于号(>)或者小于号(<)比较整型(int,long等),用{@link Float#compare(float, float)}或者{@link Double#compare(double, double)}比较浮点型。但在Java7中,所有基本类型的装箱类都已有了静态的比较方法,比如{@link Integer#compare(int, int)}。这种情况下,使用大于小于显然更为冗长,且容易出错。

当有多个需要比较的字段时,顺序很重要。从最重要的那个开始,按一定的比较方式逐个比较,直到某一对字段的比较结果中出现非零值,或者全部字段比较完毕。比如:

/**
 * 用于演示多个字段的顺序比较
 *
 * @author LightDance
 */
public class PhoneNumberCompare {

    public static void main(String[] args) {
        PhoneNumber pn1 = new PhoneNumber((short) 339,(short)448,(short)5566);
        PhoneNumber pn2 = new PhoneNumber((short) 339,(short)448,(short)4566);

        System.out.println(pn1.compareTo(pn2));
    }
}

在Java8中,{@link java.util.Comparator}接口配置了一系列构造方法,很方便就可以搞一个比较器
(comparator)出来,然后通过这个比较器实例实现compareTo()方法。它用很少的性能代价换去了简洁易读的代码。
当使用这种方式时,可以考虑配套使用Java的静态导入(static import)工具,这样可以让程序更加清晰,
比如下面中的比较方法。

import java.util.Comparator;
import java.util.function.ToIntFunction;
//静态导入简化代码
import static java.util.Comparator.comparingInt;

/**
 * 使用了{@link java.util.Comparator}实现compareTo()的电话号码类
 *
 * 它先后调用了{@link Comparator#comparingInt(ToIntFunction)}
 * 和{@link Comparator#thenComparingInt(ToIntFunction)},通过lambda表达式提取泛型类中的int型字段,
 * 对两个实例进行比较,然后返回一个Comparator<PhoneNumber2>类型的比较器,这样就可以自由地进行任意顺序、
 * 任意个数的字段比较。而调用也十分简单,如果没什么特殊需求的话,由于Java8中引入了接口中方法的默认实现,
 * 因而可以直接调用其默认的compare(x,y)方法进行比较。
 *
 * 另外,我们无需指定传递给Comparator作参数的方法的返回类型(比如{@link #COMPARATOR}中没有使用强制转换),
 * 因为Java的类型推断现在已经可以帮我们解决这个了。
 *
 * 对于long和double来说,Comparator同样也有类似于comparingInt和thenComparingInt的对应方法;
 * 并且,comparingInt或者comparingDouble都同样适用于更短的short型或者float型变量。
 */
class PhoneNumber2 implements Comparable<PhoneNumber2> {
    private short areaCode;
    private short prefix;
    private short lineNum;

    /**比较器,通过方法引用进一步简化代码*/
    public static final Comparator<PhoneNumber2> COMPARATOR =
            comparingInt(PhoneNumber2::getAreaCode)
                    .thenComparingInt(PhoneNumber2::getPrefix)
                    .thenComparingInt(PhoneNumber2::getLineNum);


    public short getAreaCode() {
        return areaCode;
    }

    public void setAreaCode(short areaCode) {
        this.areaCode = areaCode;
    }

    public short getPrefix() {
        return prefix;
    }

    public void setPrefix(short prefix) {
        this.prefix = prefix;
    }

    public short getLineNum() {
        return lineNum;
    }

    public void setLineNum(short lineNum) {
        this.lineNum = lineNum;
    }

    public PhoneNumber2(short areaCode, short prefix, short lineNum) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNum = lineNum;
    }

    @Override
    public String toString() {
        return areaCode + "-" + prefix + "-" + lineNum;
    }

    @Override
    public int compareTo(PhoneNumber2 pn) {
        return COMPARATOR.compare(this , pn);
    }

}

但是,有时候会看到类似这种基于两者差值的实现方式:

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

严重不推荐使用这种方式,因为这种方式存在整形溢出和违反IEEE754浮点数标准的风险,而其效率也未见得比前面介绍的几种方法快多少。

   /**用于对比一下的Comparator*/
    static Comparator<Object> hashCodeComparator2 = new Comparator<Object>() {
        @Override
        public int compare(Object o1, Object o2) {
            return Integer.compare(o1.hashCode(), o2.hashCode());
        }
    };

总结

总之,希望自己的类能够实现合理的排序等操作时,应该最先考虑{@link Comparable}接口;当使用{@link Comparator}进行排序时,首先考虑comparing()方法,而不要用大于或者小于号。

 

 

全代码git地址:点我点我

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值