[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(a, b) > 0a > b非負compare(a, b) >= 0a >= b零compare(a, b) ==0a == b非正compare(a, b) <= 0a <= b負compare(a, b) < 0a < b
    • ゼロと比較する

 

    • ゼロは比較演算子の右側に置く

compare() と 0 の間の比較演算子は、a と b の間においたものと考える

傻瓜

Comparator#comparingBoolean() がないのがちょっと面倒。ラムダ式で書けばいいのだけど。
nullセーフ+自然順序や、nullセーフ名+comparing() など頻発する。毎回書くのは煩雑な気がする
ラムダは全部、Serializeable にしておけば良いのに…。

广告
将在 10 秒后关闭
bannerAds