Item 14: Consider implementing C
Unlike the other methods discussed in this chapter, the compareTo method is not declared in Object. Rather, it is the sole method in the Comparable interface. It is similar in character to Object’s equals method, except that it permits order comparisons in addition to simple equality comparisons, and it is generic. By implementing Comparable, a class indicates that its instances have a natural ordering. Sorting an array of objects that implement Comparable is as simple as this:
与本章讨论的其他方法不同,compareTo 方法不是在 Object 中声明的。相反,它是 Comparable 接口中的唯一方法。它在性质上类似于 Object 的 equals 方法,除了简单的相等比较之外,它还允许顺序比较,而且它是通用的。一个类实现 Comparable,表明实例具有自然顺序。对实现 Comparable 的对象数组进行排序非常简单:
Arrays.sort(a);
It is similarly easy to search, compute extreme values, and maintain automatically sorted collections of Comparable objects. For example, the following program, which relies on the fact that String implements Comparable, prints an alphabetized list of its command-line arguments with duplicates eliminated:
类似地,搜索、计算极值和维护 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);
}
}
By implementing Comparable, you allow your class to interoperate with all of the many generic algorithms and collection implementations that depend on this interface. You gain a tremendous amount of power for a small amount of effort. Virtually all of the value classes in the Java platform libraries, as well as all enum types (Item 34), implement Comparable. If you are writing a value class with an obvious natural ordering, such as alphabetical order, numerical order, or chronological order, you should implement the Comparable interface:
通过让类实现 Comparable,就可与依赖于此接口的所有通用算法和集合实现进行互操作。你只需付出一点点努力就能获得强大的功能。实际上,Java 库中的所有值类以及所有枚举类型(Item-34)都实现了 Comparable。如果编写的值类具有明显的自然顺序,如字母顺序、数字顺序或时间顺序,则应实现 Comparable 接口:
public interface Comparable<T> {
int compareTo(T t);
}
The general contract of the compareTo method is similar to that of equals:
compareTo 方法的一般约定类似于 equals 方法:
Compares this object with the specified object for order. Returns a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object. Throws ClassCastException if the specified object’s type prevents it from being compared to this object.
将一个对象与指定的对象进行顺序比较。当该对象小于、等于或大于指定对象时,对应返回一个负整数、零或正整数。如果指定对象的类型阻止它与该对象进行比较,则抛出 ClassCastException。
In the following description, the notation sgn(expression) designates the mathematical signum function, which is defined to return -1, 0, or 1,according to whether the value of expression is negative, zero, or positive.
在下面的描述中,sgn(expression)
表示数学中的符号函数,它被定义为:根据传入表达式的值是负数、零或正数,对应返回 -1、0 或 1。
- The implementor must ensure that sgn(x.compareTo(y)) == -sgn(y. compareTo(x)) for all x and y. (This implies that x.compareTo(y) must throw an exception if and only if y.compareTo(x) throws an exception.)
实现者必须确保所有 x 和 y 满足 sgn(x.compareTo(y)) == -sgn(y)
(这意味着 x.compareTo(y)
当且仅当 y.compareTo(x)
抛出异常时才抛出异常)。
- The implementor must also ensure that the relation is transitive: (x.compareTo(y) > 0 && y.compareTo(z) > 0) implies x.compareTo(z) > 0.
实现者还必须确保关系是可传递的:(x.compareTo(y) > 0 && y.compareTo(z) > 0)
意味着 x.compareTo(z) > 0
。
- Finally, the implementor must ensure that x.compareTo(y) == 0 implies that sgn(x.compareTo(z)) == sgn(y.compareTo(z)),for all z.
最后,实现者必须确保 x.compareTo(y) == 0
时,所有的 z 满足 sgn(x.compareTo(z)) == sgn(y.compareTo(z))
。
- It is strongly recommended, but not required, that (x.compareTo(y)== 0) == (x.equals(y)). Generally speaking, any class that implements the Comparable interface and violates this condition should clearly indicate this fact. The recommended language is “Note: This class has a natural ordering that is inconsistent with equals.”
强烈建议 (x.compareTo(y)== 0) == (x.equals(y))
成立,但不是必需的。一般来说,任何实现 Comparable 接口并违反此条件的类都应该清楚地注明这一事实。推荐使用的表述是「注意:该类的自然顺序与 equals 不一致。」
Don’t be put off by the mathematical nature of this contract. Like the equals contract (Item 10), this contract isn’t as complicated as it looks. Unlike the equals method, which imposes a global equivalence relation on all objects,compareTo doesn’t have to work across objects of different types: when confronted with objects of different types, compareTo is permitted to throw ClassCastException. Usually, that is exactly what it does. The contract does permit intertype comparisons, which are typically defined in an interface implemented by the objects being compared.
不要被这些约定的数学性质所影响。就像 equals 约定(Item-10)一样,这个约定并不像看起来那么复杂。与 equals 方法不同,equals 方法对所有对象都施加了全局等价关系,compareTo 不需要跨越不同类型的对象工作:当遇到不同类型的对象时,compareTo 允许抛出 ClassCastException。通常,它就是这么做的。该约定确实允许类型间比较,这种比较通常在被比较对象实现的接口中定义。
Just as a class that violates the hashCode contract can break other classes that depend on hashing, a class that violates the compareTo contract can break other classes that depend on comparison. Classes that depend on comparison include the sorted collections TreeSet and TreeMap and the utility classes Collections and Arrays, which contain searching and sorting algorithms.
就像违反 hashCode 约定的类可以破坏依赖 hash 的其他类一样,违反 compareTo 约定的类也可以破坏依赖 Comparable 的其他类。依赖 Comparable 的类包括排序集合 TreeSet 和 TreeMap,以及实用工具类 Collections 和 Arrays,它们都包含搜索和排序算法。
Let’s go over the provisions of the compareTo contract. The first provision says that if you reverse the direction of a comparison between two object references, the expected thing happens: if the first object is less than the second,then the second must be greater than the first; if the first object is equal to the second, then the second must be equal to the first; and if the first object is greater than the second, then the second must be less than the first. The second provision says that if one object is greater than a second and the second is greater than a third, then the first must be greater than the third. The final provision says that all objects that compare as equal must yield the same results when compared to any other object.
让我们看一下 compareTo 约定的细节。第一个规定指出,如果你颠倒两个对象引用之间的比较的方向,就应当发生这样的情况:如果第一个对象小于第二个对象,那么第二个对象必须大于第一个;如果第一个对象等于第二个对象,那么第二个对象一定等于第一个对象;如果第一个对象大于第二个对象,那么第二个对象一定小于第一个对象。第二个规定指出,如果一个对象大于第二个,第二个大于第三个,那么第一个对象一定大于第三个对象。最后一个规定指出,所有 compareTo 结果为相等的对象分别与任何其他对象相比,必须产生相同的结果。
One consequence of these three provisions is that the equality test imposed by a compareTo method must obey the same restrictions imposed by the equals con-tract: reflexivity, symmetry, and transitivity. Therefore, the same caveat applies: there is no way to extend an instantiable class with a new value component while preserving the compareTo contract, unless you are willing to forgo the benefits of object-oriented abstraction (Item 10). The same workaround applies, too. If you want to add a value component to a class that implements Comparable, don’t extend it; write an unrelated class containing an instance of the first class. Then provide a “view” method that returns the contained instance. This frees you to implement whatever compareTo method you like on the containing class, while allowing its client to view an instance of the containing class as an instance of the contained class when needed.
这三种规定的一个结果是,由 compareTo 方法进行的相等性检验必须遵守由 equals 约定进行的相同的限制:反身性、对称性和传递性。因此,同样的警告也适用于此:除非你愿意放弃面向对象的抽象优点(Item-10),否则无法在保留 compareTo 约定的同时使用新值组件扩展可实例化类。同样的解决方案也适用。如果要向实现 Comparable 的类中添加值组件,不要继承它;编写一个不相关的类,其中包含第一个类的实例。然后提供返回所包含实例的「视图」方法。这使你可以自由地在包含类上实现你喜欢的任何 compareTo 方法,同时允许它的客户端在需要时将包含类的实例视为包含类的实例。
The final paragraph of the compareTo contract, which is a strong suggestion rather than a true requirement, simply states that the equality test imposed by the compareTo method should generally return the same results as the equals method. If this provision is obeyed, the ordering imposed by the compareTo method is said to be consistent with equals. If it’s violated, the ordering is said to be inconsistent with equals. A class whose compareTo method imposes an order that is inconsistent with equals will still work, but sorted collections containing elements of the class may not obey the general contract of the appropriate collection interfaces (Collection, Set, or Map). This is because the general contracts for these interfaces are defined in terms of the equals method, but sorted collections use the equality test imposed by compareTo in place of equals. It is not a catastrophe if this happens, but it’s something to be aware of.
compareTo 约定的最后一段是一个强烈的建议,而不是一个真正的要求,它只是简单地说明了 compareTo 方法所施加的同等性检验通常应该与 equals 方法返回相同的结果。如果遵守了这一规定,则 compareTo 方法所施加的排序与 equals 方法一致。如果违反这条建议,那么它的顺序就与 equals 不一致。如果一个类的 compareTo 方法强加了一个与 equals 不一致的顺序,那么这个类仍然可以工作,但是包含该类元素的有序集合可能无法遵守集合接口(Collection、Set 或 Map)的一般约定。这是因为这些接口的一般约定是根据 equals 方法定义的,但是有序集合使用 compareTo 代替了 equals 实施同等性检验。如果发生这种情况,这不是一场灾难,但这是需要注意的。
For example, consider the BigDecimal class, whose compareTo method is inconsistent with equals. If you create an empty HashSet instance and then add new BigDecimal("1.0") and new BigDecimal("1.00"),the set will contain two elements because the two BigDecimal instances added to the set are unequal when compared using the equals method. If,however, you perform the same procedure using a TreeSet instead of a HashSet, the set will contain only one element because the two BigDecimal instances are equal when compared using the compareTo method. (See the BigDecimal documentation for details.)
例如,考虑 BigDecimal 类,它的 compareTo 方法与 equals 不一致。如果你创建一个空的 HashSet 实例,然后添加 new BigDecimal("1.0")
和 new BigDecimal("1.00")
,那么该 HashSet 将包含两个元素,因为添加到该集合的两个 BigDecimal 实例在使用 equals 方法进行比较时结果是不相等的。但是,如果你使用 TreeSet 而不是 HashSet 执行相同的过程,那么该集合将只包含一个元素,因为使用 compareTo 方法比较两个 BigDecimal 实例时结果是相等的。(有关详细信息,请参阅 BigDecimal 文档。)
Writing a compareTo method is similar to writing an equals method, but there are a few key differences. Because the Comparable interface is parameterized, the compareTo method is statically typed, so you don’t need to type check or cast its argument. If the argument is of the wrong type, the invocation won’t even compile. If the argument is null, the invocation should throw a NullPointerException, and it will, as soon as the method attempts to access its members.
编写 compareTo 方法类似于编写 equals 方法,但是有一些关键的区别。因为 Comparable 接口是参数化的,compareTo 方法是静态类型的,所以不需要进行类型检查或强制转换它的参数。如果参数类型错误,则该调用将不能编译。如果参数为 null,则调用应该抛出 NullPointerException,并且在方法尝试访问其成员时抛出该异常。
In a compareTo method, fields are compared for order rather than equality.To compare object reference fields, invoke the compareTo method recursively. If a field does not implement Comparable or you need a nonstandard ordering, use a Comparator instead. You can write your own comparator or use an existing one, as in this compareTo method for CaseInsensitiveString in Item 10:
在 compareTo 方法中,字段是按顺序而不是按同等性来比较的。要比较对象引用字段,要递归调用 compareTo 方法。如果一个字段没有实现 Comparable,或者需要一个非标准的排序,那么应使用 Comparator。可以编写自定义的比较器,或使用现有的比较器,如 Item-10 中 CaseInsensitiveString 的 compareTo 方法:
// 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, cis.s);
} ... // Remainder omitted
}
Note that CaseInsensitiveString implements Comparable<CaseInsensitiveString>
. This means that a CaseInsensitiveString reference can be compared only to another CaseInsensitiveString reference. This is the normal pattern to follow when declaring a class to implement Comparable.
注意 CaseInsensitiveString 实现了 Comparable<CaseInsensitiveString>
。这意味着 CaseInsensitiveString 引用只能与另一个 CaseInsensitiveString 引用进行比较。这是在声明实现 Comparable 的类时要遵循的常规模式。
Prior editions of this book recommended that compareTo methods compare integral primitive fields using the relational operators < and >, and floating point primitive fields using the static methods Double.compare and Float.compare. In Java 7, static compare methods were added to all of Java’s boxed primitive classes. Use of the relational operators < and > in compareTo methods is verbose and error-prone and no longer recommended.
本书的旧版本建议 compareTo 方法使用关系运算符 < 和 > 来比较整数基本类型字段,使用静态方法 Double.compare
和 Float.compare
来比较浮点基本类型字段。在 Java 7 中,静态比较方法被添加到所有 Java 的包装类中。在 compareTo 方法中使用关系运算符 < 和 > 冗长且容易出错,因此不再推荐使用。
If a class has multiple significant fields, the order in which you compare them is critical. Start with the most significant field and work your way down. If a comparison results in anything other than zero (which represents equality),you’re done; just return the result. If the most significant field is equal, compare the next-most-significant field, and so on, until you find an unequal field or compare the least significant field. Here is a compareTo method for the PhoneNumber class in Item 11 demonstrating this technique:
如果一个类有多个重要字段,那么比较它们的顺序非常关键。从最重要的字段开始,一步步往下。如果比较的结果不是 0(用 0 表示相等),那么就完成了;直接返回结果。如果最重要的字段是相等的,就比较下一个最重要的字段,以此类推,直到找到一个不相等的字段或比较到最不重要的字段为止。下面是 Item-11 中 PhoneNumber 类的 compareTo 方法,演示了这种技术:
// Multiple-field Comparable with primitive fields
public int compareTo(PhoneNumber pn) {
int result = Short.compare(areaCode, pn.areaCode);
if (result == 0) {
result = Short.compare(prefix, pn.prefix);
if (result == 0)
result = Short.compare(lineNum, pn.lineNum);
}
return result;
}
In Java 8, the Comparator interface was outfitted with a set of comparator construction methods, which enable fluent construction of comparators. These comparators can then be used to implement a compareTo method, as required by the Comparable interface. Many programmers prefer the conciseness of this approach, though it does come at a modest performance cost: sorting arrays of PhoneNumber instances is about 10% slower on my machine. When using this approach, consider using Java’s static import facility so you can refer to static comparator construction methods by their simple names for clarity and brevity. Here’s how the compareTo method for PhoneNumber looks using this approach:
在 Java 8 中,Comparator 接口配备了一组比较器构造方法,可以流畅地构造比较器。然后可以使用这些比较器来实现 Comparator 接口所要求的 compareTo 方法。许多程序员更喜欢这种方法的简明,尽管它存在一些性能成本:在我的机器上,PhoneNumber 实例的数组排序要慢 10% 左右。在使用这种方法时,请考虑使用 Java 的静态导入功能,这样你就可以通过静态比较器构造方法的简单名称来引用它们,以获得清晰和简洁。下面是 PhoneNumber 类的 compareTo 方法改进后的样子:
// 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, pn);
}
译注 1:示例代码默认使用了静态导入:import static java.util.Comparator.comparingInt;
译注 2:comparingInt 及 thenComparingInt 的文档描述
static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor)
Accepts a function that extracts an int sort key from a type T, and returns a Comparator<T> that compares by that sort key.
The returned comparator is serializable if the specified function is also serializable.
接受从类型 T 中提取 int 排序 key 的函数,并返回与该排序 key 进行比较的 Comparator<T>。
如果指定的函数是可序列化的,则返回的比较器也是可序列化的。
Type Parameters:
T - the type of element to be compared
Parameters:
keyExtractor - the function used to extract the integer sort key
Returns:
a comparator that compares by an extracted key
Throws:
NullPointerException - if the argument is null
Since:
1.8
default Comparator<T> thenComparingInt(ToIntFunction<? super T> keyExtractor)
Returns a lexicographic-order comparator with a function that extracts a int sort key.
Implementation Requirements:
This default implementation behaves as if thenComparing(comparingInt(keyExtractor)).
返回具有提取 int 排序 key 的函数的字典顺序比较器。
实现要求:
此默认实现的行为类似于 thenComparing(comparingInt(keyExtractor))。
Parameters:
keyExtractor - the function used to extract the integer sort key
Returns:
a lexicographic-order comparator composed of this and then the int sort key
Throws:
NullPointerException - if the argument is null.
Since:
1.8
This implementation builds a comparator at class initialization time, using two comparator construction methods. The first is comparingInt. It is a static method that takes a key extractor function that maps an object reference to a key of type int and returns a comparator that orders instances according to that key.In the previous example, comparingInt takes a lambda () that extracts the area code from a PhoneNumber and returns a Comparator<PhoneNumber>
that orders phone numbers according to their area codes. Note that the lambda explicitly specifies the type of its input parameter (PhoneNumber pn). It turns out that in this situation, Java’s type inference isn’t powerful enough to figure the type out for itself, so we’re forced to help it in order to make the program compile.
这个实现在类初始化时使用两个比较器构造方法构建一个比较器。第一个是 comparingInt。它是一个静态方法,接受一个 key 提取器函数,该函数将对象引用映射到 int 类型的 key ,并返回一个比较器,比较器根据该 key 对实例进行排序。在上述的示例中,comparingInt 使用 lambda 表达式从 PhoneNumber 中提取 areaCode,并返回 Comparator<PhoneNumber>
,按区号来排序电话号码。注意,lambda 表达式显式地指定其输入参数的类型为 PhoneNumber。事实证明,在这种情况下,Java 的类型推断并没有强大到足以自己判断类型,因此我们不得不帮助它来编译程序。
If two phone numbers have the same area code, we need to further refine the comparison, and that’s exactly what the second comparator construction method,thenComparingInt, does. It is an instance method on Comparator that takes an int key extractor function, and returns a comparator that first applies the original comparator and then uses the extracted key to break ties. You can stack up as many calls to thenComparingInt as you like, resulting in a lexicographic ordering. In the example above, we stack up two calls to thenComparingInt, resulting in an ordering whose secondary key is the prefix and whose tertiary key is the line number. Note that we did not have to specify the parameter type of the key extractor function passed to either of the calls to thenComparingInt: Java’s type inference was smart enough to figure this one out for itself.
如果两个电话号码有相同的区号,我们需要进一步改进比较,这正是第二个 comparator 构造方法 thenComparingInt 所做的。它是 Comparator 上的一个实例方法,它接受一个 int 类型的 key 提取函数,并返回一个比较器,该比较器首先应用原始比较器,然后使用提取的 key 来断开连接。你可以任意堆叠对 thenComparingInt 的调用,从而形成字典顺序。在上面的例子中,我们将两个对 thenComparingInt 的调用叠加起来,得到一个排序,它的第二个 key 是 prefix,而第三个 key 是 lineNum。注意,我们不必指定传递给两个调用 thenComparingInt 的 key 提取器函数的参数类型:Java 的类型推断足够智能,可以自行解决这个问题。
The Comparator class has a full complement of construction methods.There are analogues to comparingInt and thenComparingInt for the primitive types long and double. The int versions can also be used for narrower integral types, such as short, as in our PhoneNumber example. The double versions can also be used for float. This provides coverage of all of Java’s numerical primitive types.
Comparator 类具有完整的构造方法。对于 long 和 double 的基本类型,有类似 comparingInt 和 thenComparingInt 的方法。int 版本还可以用于范围更小的整数类型,如 PhoneNumber 示例中的 short。double 版本也可以用于 float。Comparator 类提供的构造方法覆盖了所有 Java 数值基本类型。
There are also comparator construction methods for object reference types.The static method, named comparing, has two overloadings. One takes a key extractor and uses the keys’ natural order. The second takes both a key extractor and a comparator to be used on the extracted keys. There are three overloadings of the instance method, which is named thenComparing. One overloading takes only a comparator and uses it to provide a secondary order. A second overloading takes only a key extractor and uses the key’s natural order as a secondary order. The final overloading takes both a key extractor and a comparator to be used on the extracted keys.
也有对象引用类型的比较器构造方法。静态方法名为 compare,它有两个重载。一个是使用 key 提取器并使用 key 的自然顺序。第二种方法同时使用 key 提取器和比较器对提取的 key 进行比较。实例方法有三种重载,称为 thenComparing。一个重载只需要一个比较器并使用它来提供一个二级顺序。第二个重载只接受一个 key 提取器,并将 key 的自然顺序用作二级顺序。最后的重载需要一个 key 提取器和一个比较器来对提取的 key 进行比较。
Occasionally you may see compareTo or compare methods that rely on the fact that the difference between two values is negative if the first value is less than the second, zero if the two values are equal, and positive if the first value is greater. Here is an example:
有时候,你可能会看到 compareTo 或 compare 方法,它们依赖于以下事实:如果第一个值小于第二个值,则两个值之间的差为负;如果两个值相等,则为零;如果第一个值大于零,则为正。下面是一个例子:
// BROKEN difference-based comparator - violates transitivity!
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
};
Do not use this technique. It is fraught with danger from integer overflow and IEEE 754 floating point arithmetic artifacts [JLS 15.20.1, 15.21.1]. Furthermore,the resulting methods are unlikely to be significantly faster than those written using the techniques described in this item. Use either a static compare method:
不要使用这种技术。它充满了来自整数溢出和 IEEE 754 浮点运算构件的危险 [JLS 15.20.1, 15.21.1]。此外,生成的方法不太可能比使用本项目中描述的技术编写的方法快得多。应使用静态比较方法:
// 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());
}
};
or a comparator construction method:
或比较器构造方法:
// Comparator based on Comparator construction method
static Comparator<Object> hashCodeOrder = Comparator
.comparingInt(o -> o.hashCode());
In summary, whenever you implement a value class that has a sensible ordering, you should have the class implement the Comparable interface so that its instances can be easily sorted, searched, and used in comparison-based collections. When comparing field values in the implementations of the compareTo methods, avoid the use of the < and > operators. Instead, use the static compare methods in the boxed primitive classes or the comparator construction methods in the Comparator interface.
总之,无论何时实现具有排序性质的值类,都应该让类实现 Comparable 接口,这样就可以轻松地对实例进行排序、搜索,并与依赖于此接口的集合实现进行互操作。在 compareTo 方法的实现中比较字段值时,避免使用 < 和 > 操作符,应使用包装类中的静态比较方法或 Comparator 接口中的 comparator 构造方法。
Back to contents of the chapter(返回章节目录)
- Previous Item(上一条目):Item 13: Override clone judiciously(明智地覆盖 clone 方法)
- Next Item(下一条目):Chapter 4 Introduction(章节介绍)