[Java] 可比较器 (Comparable) 和比较器 (Comparator) 的笔记.
为了什么
-
- 大小比較をする java.util.Comparator と java.lang.Comparable に関するクラス・メソッドの整理
Comparator や Comparable を実装する場合の注意点を整理
基本的说话
可比较的,比较器共享
-
- いわゆる「宇宙船演算子」 <=> と同じようなもの→ Wikipedia: 宇宙船演算子
大小比較する
ソート、バイナリサーチする場合、max/min などを求める場合、ツリー構造( TreeSet , TreeMap )を作る際などにも使われる
基本的に equals() と矛盾しないことが推奨される(もし equals() と互換性がない場合には、その旨をjavadocに明記すること)
基本的に Serializable を実装することが推奨される
「Comparable」是指可比较的意思。
-
- 自分自身と、別のインスタンスの大小比較をする
Comparable を実装していると「自然な順序付け」ができる
比较器是指
- 2 つのオブジェクトの大小比較をする
便利的方法 lì de
- java.util.Collections#reverseOrder()
有一些接受参数的,一些不接受参数的。
接受参数的将返回指定比较器的逆序比较器。
不接受参数的将返回自然顺序的逆序比较器。
// 自然な順序の逆順
Comparator<String> c1 = Collections.reverseOrder();
// 指定された Comparator の逆順
Comparator<String> c2 = Collections.reverseOrder(c1);
从Java 8开始,Comparator中也添加了与相同目的的方法。
- java.util.Comparator#reverseOrder()
返回与前述的无参数 Collections#reverseOrder()相同的自然顺序的逆序比较器。我认为 Comparator 系列的方法都被集成到了 Comparator 接口中。
// 自然な順序の逆順
Comparator<String> c3= Comparator.reverseOrder();
- java.util.Comparator#reversed()
返回与前面提到的具有参数的Collections#reverseOrder()相反的Comparator。不同之处在于它不针对传入的实例进行操作,而是实例本身的方法。
// 自分の逆順
Comparator<String> c4 = c3.reversed();
- java.util.Comparator#naturalOrder()
可以获取一个自然顺序的Comparator。
- java.util.Comparator#comparing()
生成Comparator用于从object中提取值并进行比较。通常使用lambda表达式或方法引用来指定。
public class Bean {
private String data1;
public String getData1() {
return data1;
}
}
// ...
Comparator<Bean> c5 = Comparator.comparing(Bean::getData1);
也有原始版本的comparingInt(), comparingLong(), comparingDouble()等等。
- java.util.Comparator#thenComparing()
可以通过组合比较器,当第一个比较器的结果为0(表示相等)时,使用第二个比较器的结果。
你还可以组合三个以上的比较器。
public class Bean {
private String data1;
private String data2;
public String getData1() {
return data1;
}
public String getData2() {
return data2;
}
}
// ...
Comparator<Bean> c6 = Comparator.comparing(Bean::getData1)
.thenComparing(Bean::getData2);
java.util.Comparator#nullsFirst() , nullsLast()
将传递的Comparator参数变为null安全。这两个方法的区别在于将null条目放置在非null条目的前面(nullsFirst)还是后面(nullsLast)。
- java.lang.String#CASE_INSENSITIVE_ORDER
在String类中,有一个用于忽略大小写的大小比较的Comparator已经作为常量准备好了。
对于 Map.Entry 的比较
Java 8 提供了通过 Map.Entry 的键或值进行比较的简便方法。我认为这可能是为了在 Stream 等场景下使用。
java.util.Map.Entry#comparingByKey()
java.util.Map.Entry#comparingByKey(Comparator<? super K>)
java.util.Map.Entry#comparingByValue()
java.util.Map.Entry#comparingByValue(Comparator<? super K>)
java.util.Objects#compare()
在Objects类中,有一个名为compare()的方法可以用于检查对象的大小。实现了Comparable接口的对象可以进行比较。
在实现独立类的 Comparable 或 Comparator 时,比较成员变量时非常方便。
此外,在使用 lambda 表达式创建 Comparator 时也非常方便。
java.lang.Integer#compare() , java.lang.Long#compare() など
类似的方法也适用于布尔、字节、字符、短整型、整型、长整型、浮点型、双精度等原始类型的包装类。
请注意
与equals()方法相符
在实现Comparable或Comparator接口时,并不一定需要产生与equals()方法不一致的结果。
实际上,即使在标准库中,有一些实现了Comparable接口的类也会返回与equals()方法不兼容的结果(例如java.math.BigDecimal)。
然而,建议返回的结果不与equals()相矛盾。
原因在于一些标准库,如TreeSet和TreeMap,将Comparable(或Comparator)返回为0和equals()返回为true视为相同的处理方式。
假设实现了Comparable接口的类有三个成员变量data1,data2和data3,并且equals()方法使用这三个成员变量进行判定。在这种情况下(即使只有data1和data2作为排序条件已经足够),在compareTo()方法中也需要使用相同的三个成员变量进行判定。
否则,会出现在 data1 和 data2 相同时,但 data3 不同的情况下,将其视为键重复并丢弃其中一个的情况。
public final class Bean implements Comparable<Bean>, Serializable {
private static final long serialVersionUID = 1;
private String data1;
private String data2;
private String data3;
@Override
public int hashCode() {
return Objects.hash(data1, data2, data3);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other instanceof Bean) {
Bean that = (Bean) other;
return Objects.equals(this.data1, that.data1)
&& Objects.equals(this.data2, that.data2)
&& Objects.equals(this.data3, that.data3);
}
return false;
}
@Override
public int compareTo(Bean that) {
Comparator<String> naturalOrder = Comparator.naturalOrder();
int rc = Objects.compare(data1, that.data1, naturalOrder);
if (rc == 0) {
rc = Objects.compare(data2, that.data2, naturalOrder);
if (rc == 0) {
rc = Objects.compare(data3, that.data3, naturalOrder);
}
}
return rc;
}
}
使其可以进行序列化。
在实现比较器时,同样建议将其基本类型设为可序列化。
为了确保在设置了比较器(Comparator)后对TreeMap或TreeSet等进行序列化时不会出现问题。
生成的Comparator是标准库生成的,当然是可序列化的。
但需要注意的是,普通的lambda表达式或者方法引用等(除非进行交叉类型转换),是不可序列化的。
public final class Bean implements Comparable<Bean>, Serializable {
private static final long serialVersionUID = 1;
private static final Comparator<Bean> COMPARATOR = Comparator.comparing(Bean::getData1)
.thenComparing(Bean::getData2)
.thenComparing(Bean::getData3);
private String data1;
private String data2;
private String data3;
public String getData1() {
return data1;
}
public String getData2() {
return data2;
}
public String getData3() {
return data3;
}
@Override
public int compareTo(Bean that) {
return COMPARATOR.compare(this, that);
}
}
例如,如上所述生成的 COMPARATOR 是不可序列化的。
如果将其设为 public 并从外部使用,看起来似乎没有问题,但如果尝试序列化 COMPARATOR,会发生异常。
在这个例子中,我认为将 COMPARATOR 保持为私有,并使用Comparator#naturalOrder()来处理是更安全的。
当然,在需要在现场完成操作的情况下,如对数据进行排序、大小检查等情况下,即使是不可序列化的lambda表达式也没有问题。
注意继承
繼承造成的親子關係會使equals()和大小比較變得非常複雜(尤其是對稱性的確保)。
通常,僅需將數據放入的Bean(如DTO等)通常不需要進行繼承並添加項目。
如果这样的话,您可以将其定义为终极类,并将父类设为 java.lang.Object,从而确保可以可靠地避免这些困难问题。
其他
Note: The Chinese translation provided is the direct equivalent of the Japanese word “sono ta” (その他), which means “other” or “remaining.”
对比结果的观察方式 de
Comparable和Comparator的返回值有三种类型:正数、零、负数。
记忆方法有几种,但我认为以下的记忆方式最简单。
-
- ゼロと比較する
-
- ゼロは比較演算子の右側に置く
compare() と 0 の間の比較演算子は、a と b の間においたものと考える
傻瓜
Comparator#comparingBoolean() がないのがちょっと面倒。ラムダ式で書けばいいのだけど。
nullセーフ+自然順序や、nullセーフ名+comparing() など頻発する。毎回書くのは煩雑な気がする
ラムダは全部、Serializeable にしておけば良いのに…。