解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界
与其他方法不同,compareTo
并非 Object
类中声明的,而是 Comparable
接口的唯一方法。compareTo
方法与 equals
类似,但它不仅支持相等性比较,还允许顺序比较,同时它是泛型的。通过实现 Comparable
接口,一个类表明其实例具有自然顺序。这使得对实现 Comparable
的对象数组进行排序变得非常简单:
Arrays.sort(a);
使用 Comparable
接口,可以轻松地搜索、计算极值或维护自动排序的集合。例如,以下程序利用 String
实现了 Comparable
,它打印出一个按字母顺序排列的命令行参数列表,并去除重复项:
public class WordList {public static void main(String[] args) {Set<String> s = new TreeSet<>();Collections.addAll(s, args);System.out.println(s);}
}
通过实现 Comparable
,你的类可以与依赖该接口的各种通用算法和集合实现进行互操作。实现 Comparable
所需的工作量非常小,但却带来了巨大的收益。几乎所有 Java 平台库中的值类以及所有枚举类型(详见【条目34】)都实现了 Comparable
。如果你编写的值类有明显的自然排序,例如字母顺序、数字顺序或时间顺序,那么你应该实现 Comparable
接口。
Comparable
接口声明如下:
public interface Comparable<T> {int compareTo(T t);
}
compareTo
方法的通用合同
compareTo
方法的通用合同类似于 equals
的合同:
- 比较当前对象与指定对象的顺序,返回负整数、零或正整数,分别表示当前对象小于、等于或大于指定对象。
- 如果两个对象具有不同类型,通常会抛出
ClassCastException
。
这个合同的数学符号表示如下:
- 对所有
x
和y
,应确保sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
,这意味着x.compareTo(y)
只有在y.compareTo(x)
抛出异常时才会抛出异常。 - 应确保传递性:如果
x.compareTo(y) > 0 && y.compareTo(z) > 0
,则x.compareTo(z) > 0
。 - 如果
x.compareTo(y) == 0
,则sgn(x.compareTo(z)) == sgn(y.compareTo(z))
对所有z
应成立。
此外,推荐但不强制要求 x.compareTo(y) == 0
等价于 x.equals(y)
。如果违背了这一点,应在类文档中注明该类的自然顺序与 equals
不一致。
比较与 equals
的一致性
compareTo
方法的比较应符合 equals
的等价性、对称性和传递性原则。如果违反这些原则,可能会导致依赖比较的类(如 TreeSet
、TreeMap
)出错。虽然不致命,但会导致结果不一致。例如,BigDecimal
类的 compareTo
方法与其 equals
方法不一致。对于 HashSet
,new BigDecimal("1.0")
和 new BigDecimal("1.00")
被视为不相等,而在 TreeSet
中则视为相等。
编写 compareTo
方法
编写 compareTo
方法类似于编写 equals
方法,但有一些关键区别。由于 Comparable
是参数化接口,因此 compareTo
方法是静态类型化的,避免了类型检查和强制转换。如果参数类型错误,代码甚至无法编译。
在 compareTo
方法中,字段是按顺序比较的。对于对象引用字段,可以递归调用 compareTo
方法。如果字段没有实现 Comparable
,或者需要非标准排序,可以使用 Comparator
。例如,下面是一个比较 CaseInsensitiveString
类的 compareTo
方法:
// 使用对象引用字段的单字段 Comparable
public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {public int compareTo(CaseInsensitiveString cis) {return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);}// 其他代码省略
}
使用比较器构造方法实现 compareTo
在 Java 8 中,Comparator
接口提供了一组构造方法来简洁地构建比较器。下面是使用比较器构造方法实现 PhoneNumber
类 compareTo
的例子:
// 使用比较器构造方法的 Comparable
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, pn);
}
这种方法通过使用 Comparator
的 comparingInt
和 thenComparingInt
方法来简洁地构建比较逻辑。
避免基于差值的比较器
不要使用基于两个值差值的比较器,例如:
// 错误的差值比较器 - 违反传递性
static Comparator<Object> hashCodeOrder = new Comparator<>() {public int compare(Object o1, Object o2) {return o1.hashCode() - o2.hashCode();}
};
这种方法容易受到整数溢出和浮点运算误差的影响。相反,应该使用静态比较方法或比较器构造方法,例如:
// 基于静态比较方法的比较器
static Comparator<Object> hashCodeOrder = new Comparator<>() {public int compare(Object o1, Object o2) {return Integer.compare(o1.hashCode(), o2.hashCode());}
};
或:
// 基于比较器构造方法的比较器
static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());
总结
当你实现具有合理顺序的值类时,应该让该类实现 Comparable
接口,以便能够轻松排序、搜索和在集合中使用。避免在 compareTo
方法中使用 <
和 >
操作符,推荐使用 Java 提供的静态比较方法或比较器构造方法。